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