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