1 package org.robolectric.shadows.httpclient; 2 3 import java.io.IOException; 4 import java.net.URI; 5 import java.util.ArrayList; 6 import java.util.HashMap; 7 import java.util.List; 8 import java.util.Map; 9 import java.util.regex.Pattern; 10 import org.apache.http.Header; 11 import org.apache.http.HttpEntity; 12 import org.apache.http.HttpException; 13 import org.apache.http.HttpHost; 14 import org.apache.http.HttpRequest; 15 import org.apache.http.HttpResponse; 16 import org.apache.http.client.RequestDirector; 17 import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; 18 import org.apache.http.conn.ConnectTimeoutException; 19 import org.apache.http.params.HttpConnectionParams; 20 import org.apache.http.params.HttpParams; 21 import org.apache.http.protocol.HttpContext; 22 23 public class FakeHttpLayer { 24 private final List<HttpResponseGenerator> pendingHttpResponses = new ArrayList<>(); 25 private final List<HttpRequestInfo> httpRequestInfos = new ArrayList<>(); 26 private final List<HttpResponse> httpResponses = new ArrayList<>(); 27 private final List<HttpEntityStub.ResponseRule> httpResponseRules = new ArrayList<>(); 28 private HttpResponse defaultHttpResponse; 29 private boolean interceptHttpRequests = true; 30 private boolean logHttpRequests = false; 31 private List<byte[]> httpResposeContent = new ArrayList<>(); 32 private boolean interceptResponseContent; 33 getLastSentHttpRequestInfo()34 public HttpRequestInfo getLastSentHttpRequestInfo() { 35 List<HttpRequestInfo> requestInfos = getSentHttpRequestInfos(); 36 if (requestInfos.isEmpty()) { 37 return null; 38 } 39 return requestInfos.get(requestInfos.size() - 1); 40 } 41 addPendingHttpResponse(int statusCode, String responseBody, Header... headers)42 public void addPendingHttpResponse(int statusCode, String responseBody, Header... headers) { 43 addPendingHttpResponse(new TestHttpResponse(statusCode, responseBody, headers)); 44 } 45 addPendingHttpResponse(final HttpResponse httpResponse)46 public void addPendingHttpResponse(final HttpResponse httpResponse) { 47 addPendingHttpResponse(new HttpResponseGenerator() { 48 @Override 49 public HttpResponse getResponse(HttpRequest request) { 50 return httpResponse; 51 } 52 }); 53 } 54 addPendingHttpResponse(HttpResponseGenerator httpResponseGenerator)55 public void addPendingHttpResponse(HttpResponseGenerator httpResponseGenerator) { 56 pendingHttpResponses.add(httpResponseGenerator); 57 } 58 addHttpResponseRule(String method, String uri, HttpResponse response)59 public void addHttpResponseRule(String method, String uri, HttpResponse response) { 60 addHttpResponseRule(new DefaultRequestMatcher(method, uri), response); 61 } 62 addHttpResponseRule(String uri, HttpResponse response)63 public void addHttpResponseRule(String uri, HttpResponse response) { 64 addHttpResponseRule(new UriRequestMatcher(uri), response); 65 } 66 addHttpResponseRule(String uri, String response)67 public void addHttpResponseRule(String uri, String response) { 68 addHttpResponseRule(new UriRequestMatcher(uri), new TestHttpResponse(200, response)); 69 } 70 addHttpResponseRule(RequestMatcher requestMatcher, HttpResponse response)71 public void addHttpResponseRule(RequestMatcher requestMatcher, HttpResponse response) { 72 addHttpResponseRule(new RequestMatcherResponseRule(requestMatcher, response)); 73 } 74 75 /** 76 * Add a response rule. 77 * 78 * @param requestMatcher Request matcher 79 * @param responses A list of responses that are returned to matching requests in order from first to last. 80 */ addHttpResponseRule(RequestMatcher requestMatcher, List<? extends HttpResponse> responses)81 public void addHttpResponseRule(RequestMatcher requestMatcher, List<? extends HttpResponse> responses) { 82 addHttpResponseRule(new RequestMatcherResponseRule(requestMatcher, responses)); 83 } 84 addHttpResponseRule(HttpEntityStub.ResponseRule responseRule)85 public void addHttpResponseRule(HttpEntityStub.ResponseRule responseRule) { 86 httpResponseRules.add(0, responseRule); 87 } 88 setDefaultHttpResponse(HttpResponse defaultHttpResponse)89 public void setDefaultHttpResponse(HttpResponse defaultHttpResponse) { 90 this.defaultHttpResponse = defaultHttpResponse; 91 } 92 setDefaultHttpResponse(int statusCode, String responseBody)93 public void setDefaultHttpResponse(int statusCode, String responseBody) { 94 setDefaultHttpResponse(new TestHttpResponse(statusCode, responseBody)); 95 } 96 findResponse(HttpRequest httpRequest)97 private HttpResponse findResponse(HttpRequest httpRequest) throws HttpException, IOException { 98 if (!pendingHttpResponses.isEmpty()) { 99 return pendingHttpResponses.remove(0).getResponse(httpRequest); 100 } 101 102 for (HttpEntityStub.ResponseRule httpResponseRule : httpResponseRules) { 103 if (httpResponseRule.matches(httpRequest)) { 104 return httpResponseRule.getResponse(); 105 } 106 } 107 108 System.err.println("Unexpected HTTP call " + httpRequest.getRequestLine()); 109 110 return defaultHttpResponse; 111 } 112 emulateRequest(HttpHost httpHost, HttpRequest httpRequest, HttpContext httpContext, RequestDirector requestDirector)113 public HttpResponse emulateRequest(HttpHost httpHost, HttpRequest httpRequest, HttpContext httpContext, RequestDirector requestDirector) throws HttpException, IOException { 114 if (logHttpRequests) { 115 System.out.println(" <-- " + httpRequest.getRequestLine()); 116 } 117 HttpResponse httpResponse = findResponse(httpRequest); 118 if (logHttpRequests) { 119 System.out.println(" --> " + (httpResponse == null ? null : httpResponse.getStatusLine().getStatusCode())); 120 } 121 122 if (httpResponse == null) { 123 throw new RuntimeException("Unexpected call to execute, no pending responses are available. See Robolectric.addPendingResponse(). Request was: " + 124 httpRequest.getRequestLine().getMethod() + " " + httpRequest.getRequestLine().getUri()); 125 } else { 126 HttpParams params = httpResponse.getParams(); 127 128 if (HttpConnectionParams.getConnectionTimeout(params) < 0) { 129 throw new ConnectTimeoutException("Socket is not connected"); 130 } else if (HttpConnectionParams.getSoTimeout(params) < 0) { 131 throw new ConnectTimeoutException("The operation timed out"); 132 } 133 } 134 135 addRequestInfo(new HttpRequestInfo(httpRequest, httpHost, httpContext, requestDirector)); 136 addHttpResponse(httpResponse); 137 return httpResponse; 138 } hasPendingResponses()139 public boolean hasPendingResponses() { 140 return !pendingHttpResponses.isEmpty(); 141 } 142 hasRequestInfos()143 public boolean hasRequestInfos() { 144 return !httpRequestInfos.isEmpty(); 145 } 146 clearRequestInfos()147 public void clearRequestInfos() { 148 httpRequestInfos.clear(); 149 } 150 151 /** 152 * This method is not supposed to be consumed by tests. This exists solely for the purpose of 153 * logging real HTTP requests, so that functional/integration tests can verify if those were made, without 154 * messing with the fake http layer to actually perform the http call, instead of returning a mocked response. 155 * 156 * If you are just using mocked http calls, you should not even notice this method here. 157 * 158 * @param requestInfo Request info object to add. 159 */ addRequestInfo(HttpRequestInfo requestInfo)160 public void addRequestInfo(HttpRequestInfo requestInfo) { 161 httpRequestInfos.add(requestInfo); 162 } 163 hasResponseRules()164 public boolean hasResponseRules() { 165 return !httpResponseRules.isEmpty(); 166 } 167 hasRequestMatchingRule(RequestMatcher rule)168 public boolean hasRequestMatchingRule(RequestMatcher rule) { 169 for (HttpRequestInfo requestInfo : httpRequestInfos) { 170 if (rule.matches(requestInfo.httpRequest)) { 171 return true; 172 } 173 } 174 return false; 175 } 176 getSentHttpRequestInfo(int index)177 public HttpRequestInfo getSentHttpRequestInfo(int index) { 178 return httpRequestInfos.get(index); 179 } 180 getNextSentHttpRequestInfo()181 public HttpRequestInfo getNextSentHttpRequestInfo() { 182 return httpRequestInfos.size() > 0 ? httpRequestInfos.remove(0) : null; 183 } 184 logHttpRequests()185 public void logHttpRequests() { 186 logHttpRequests = true; 187 } 188 silence()189 public void silence() { 190 logHttpRequests = false; 191 } 192 getSentHttpRequestInfos()193 public List<HttpRequestInfo> getSentHttpRequestInfos() { 194 return new ArrayList<>(httpRequestInfos); 195 } 196 clearHttpResponseRules()197 public void clearHttpResponseRules() { 198 httpResponseRules.clear(); 199 } 200 clearPendingHttpResponses()201 public void clearPendingHttpResponses() { 202 pendingHttpResponses.clear(); 203 } 204 205 /** 206 * This method return a list containing all the HTTP responses logged by the fake http layer, be it 207 * mocked http responses, be it real http calls (if {code}interceptHttpRequests{/code} is set to false). 208 * 209 * It doesn't make much sense to call this method if said property is set to true, as you yourself are 210 * providing the response, but it's here nonetheless. 211 * 212 * @return List of all HTTP Responses logged by the fake http layer. 213 */ getHttpResponses()214 public List<HttpResponse> getHttpResponses() { 215 return new ArrayList<>(httpResponses); 216 } 217 218 /** 219 * As a consumer of the fake http call, you should never call this method. This should be used solely 220 * by components that exercises http calls. 221 * 222 * @param response The final response received by the server 223 */ addHttpResponse(HttpResponse response)224 public void addHttpResponse(HttpResponse response) { 225 this.httpResponses.add(response); 226 } 227 addHttpResponseContent(byte[] content)228 public void addHttpResponseContent(byte[] content) { 229 this.httpResposeContent.add(content); 230 } 231 getHttpResposeContentList()232 public List<byte[]> getHttpResposeContentList() { 233 return httpResposeContent; 234 } 235 236 /** 237 * Helper method that returns the latest received response from the server. 238 * @return The latest HTTP response or null, if no responses are available 239 */ getLastHttpResponse()240 public HttpResponse getLastHttpResponse() { 241 if (httpResponses.isEmpty()) return null; 242 return httpResponses.get(httpResponses.size()-1) ; 243 } 244 245 /** 246 * Call this method if you want to ensure that there's no http responses logged from this point until 247 * the next response arrives. Helpful to ensure that the state is "clear" before actions are executed. 248 */ clearHttpResponses()249 public void clearHttpResponses() { 250 this.httpResponses.clear(); 251 } 252 253 /** 254 * You can disable Robolectric's fake HTTP layer temporarily 255 * by calling this method. 256 * @param interceptHttpRequests whether all HTTP requests should be 257 * intercepted (true by default) 258 */ interceptHttpRequests(boolean interceptHttpRequests)259 public void interceptHttpRequests(boolean interceptHttpRequests) { 260 this.interceptHttpRequests = interceptHttpRequests; 261 } 262 isInterceptingHttpRequests()263 public boolean isInterceptingHttpRequests() { 264 return interceptHttpRequests; 265 } 266 interceptResponseContent(boolean interceptResponseContent)267 public void interceptResponseContent(boolean interceptResponseContent) { 268 this.interceptResponseContent = interceptResponseContent; 269 } 270 isInterceptingResponseContent()271 public boolean isInterceptingResponseContent() { 272 return interceptResponseContent; 273 } 274 275 public static class RequestMatcherResponseRule implements HttpEntityStub.ResponseRule { 276 private RequestMatcher requestMatcher; 277 private HttpResponse responseToGive; 278 private IOException ioException; 279 private HttpException httpException; 280 private List<? extends HttpResponse> responses; 281 RequestMatcherResponseRule(RequestMatcher requestMatcher, HttpResponse responseToGive)282 public RequestMatcherResponseRule(RequestMatcher requestMatcher, HttpResponse responseToGive) { 283 this.requestMatcher = requestMatcher; 284 this.responseToGive = responseToGive; 285 } 286 RequestMatcherResponseRule(RequestMatcher requestMatcher, IOException ioException)287 public RequestMatcherResponseRule(RequestMatcher requestMatcher, IOException ioException) { 288 this.requestMatcher = requestMatcher; 289 this.ioException = ioException; 290 } 291 RequestMatcherResponseRule(RequestMatcher requestMatcher, HttpException httpException)292 public RequestMatcherResponseRule(RequestMatcher requestMatcher, HttpException httpException) { 293 this.requestMatcher = requestMatcher; 294 this.httpException = httpException; 295 } 296 RequestMatcherResponseRule(RequestMatcher requestMatcher, List<? extends HttpResponse> responses)297 public RequestMatcherResponseRule(RequestMatcher requestMatcher, List<? extends HttpResponse> responses) { 298 this.requestMatcher = requestMatcher; 299 this.responses = responses; 300 } 301 302 @Override matches(HttpRequest request)303 public boolean matches(HttpRequest request) { 304 return requestMatcher.matches(request); 305 } 306 307 @Override getResponse()308 public HttpResponse getResponse() throws HttpException, IOException { 309 if (httpException != null) throw httpException; 310 if (ioException != null) throw ioException; 311 if (responseToGive != null) { 312 return responseToGive; 313 } else { 314 if (responses.isEmpty()) { 315 throw new RuntimeException("No more responses left to give"); 316 } 317 return responses.remove(0); 318 } 319 } 320 } 321 322 public static class DefaultRequestMatcher implements RequestMatcher { 323 private String method; 324 private String uri; 325 DefaultRequestMatcher(String method, String uri)326 public DefaultRequestMatcher(String method, String uri) { 327 this.method = method; 328 this.uri = uri; 329 } 330 331 @Override matches(HttpRequest request)332 public boolean matches(HttpRequest request) { 333 return request.getRequestLine().getMethod().equals(method) && 334 request.getRequestLine().getUri().equals(uri); 335 } 336 } 337 338 public static class UriRequestMatcher implements RequestMatcher { 339 private String uri; 340 UriRequestMatcher(String uri)341 public UriRequestMatcher(String uri) { 342 this.uri = uri; 343 } 344 345 @Override matches(HttpRequest request)346 public boolean matches(HttpRequest request) { 347 return request.getRequestLine().getUri().equals(uri); 348 } 349 } 350 351 public static class RequestMatcherBuilder implements RequestMatcher { 352 private String method, hostname, path; 353 private boolean noParams; 354 private Map<String, String> params = new HashMap<>(); 355 private Map<String, String> headers = new HashMap<>(); 356 private PostBodyMatcher postBodyMatcher; 357 358 public interface PostBodyMatcher { 359 /** 360 * Hint: you can use EntityUtils.toString(actualPostBody) to help you implement your matches method. 361 * 362 * @param actualPostBody The post body of the actual request that we are matching against. 363 * @return true if you consider the body to match 364 * @throws IOException Get turned into a RuntimeException to cause your test to fail. 365 */ matches(HttpEntity actualPostBody)366 boolean matches(HttpEntity actualPostBody) throws IOException; 367 } 368 method(String method)369 public RequestMatcherBuilder method(String method) { 370 this.method = method; 371 return this; 372 } 373 host(String hostname)374 public RequestMatcherBuilder host(String hostname) { 375 this.hostname = hostname; 376 return this; 377 } 378 path(String path)379 public RequestMatcherBuilder path(String path) { 380 if (path.startsWith("/")) { 381 throw new RuntimeException("Path should not start with '/'"); 382 } 383 this.path = "/" + path; 384 return this; 385 } 386 param(String name, String value)387 public RequestMatcherBuilder param(String name, String value) { 388 params.put(name, value); 389 return this; 390 } 391 noParams()392 public RequestMatcherBuilder noParams() { 393 noParams = true; 394 return this; 395 } 396 postBody(PostBodyMatcher postBodyMatcher)397 public RequestMatcherBuilder postBody(PostBodyMatcher postBodyMatcher) { 398 this.postBodyMatcher = postBodyMatcher; 399 return this; 400 } 401 header(String name, String value)402 public RequestMatcherBuilder header(String name, String value) { 403 headers.put(name, value); 404 return this; 405 } 406 407 @Override matches(HttpRequest request)408 public boolean matches(HttpRequest request) { 409 URI uri = URI.create(request.getRequestLine().getUri()); 410 if (method != null && !method.equals(request.getRequestLine().getMethod())) { 411 return false; 412 } 413 if (hostname != null && !hostname.equals(uri.getHost())) { 414 return false; 415 } 416 if (path != null && !path.equals(uri.getRawPath())) { 417 return false; 418 } 419 if (noParams && !uri.getRawQuery().equals(null)) { 420 return false; 421 } 422 if (params.size() > 0) { 423 Map<String, String> requestParams = ParamsParser.parseParams(request); 424 if (!requestParams.equals(params)) { 425 return false; 426 } 427 } 428 if (headers.size() > 0) { 429 Map<String, String> actualRequestHeaders = new HashMap<>(); 430 for (Header header : request.getAllHeaders()) { 431 actualRequestHeaders.put(header.getName(), header.getValue()); 432 } 433 if (!headers.equals(actualRequestHeaders)) { 434 return false; 435 } 436 } 437 if (postBodyMatcher != null) { 438 if (!(request instanceof HttpEntityEnclosingRequestBase)) { 439 return false; 440 } 441 HttpEntityEnclosingRequestBase postOrPut = (HttpEntityEnclosingRequestBase) request; 442 try { 443 if (!postBodyMatcher.matches(postOrPut.getEntity())) { 444 return false; 445 } 446 } catch (IOException e) { 447 throw new RuntimeException(e); 448 } 449 } 450 return true; 451 } 452 getHostname()453 public String getHostname() { 454 return hostname; 455 } 456 getPath()457 public String getPath() { 458 return path; 459 } 460 getParam(String key)461 public String getParam(String key) { 462 return params.get(key); 463 } 464 getHeader(String key)465 public String getHeader(String key) { 466 return headers.get(key); 467 } 468 isNoParams()469 public boolean isNoParams() { 470 return noParams; 471 } 472 getMethod()473 public String getMethod() { 474 return method; 475 } 476 } 477 478 public static class UriRegexMatcher implements RequestMatcher { 479 private String method; 480 private final Pattern uriRegex; 481 UriRegexMatcher(String method, String uriRegex)482 public UriRegexMatcher(String method, String uriRegex) { 483 this.method = method; 484 this.uriRegex = Pattern.compile(uriRegex); 485 } 486 487 @Override matches(HttpRequest request)488 public boolean matches(HttpRequest request) { 489 return request.getRequestLine().getMethod().equals(method) && 490 uriRegex.matcher(request.getRequestLine().getUri()).matches(); 491 } 492 } 493 } 494