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