• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2018 Google LLC
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions are
6  * met:
7  *
8  *     * Redistributions of source code must retain the above copyright
9  * notice, this list of conditions and the following disclaimer.
10  *     * Redistributions in binary form must reproduce the above
11  * copyright notice, this list of conditions and the following disclaimer
12  * in the documentation and/or other materials provided with the
13  * distribution.
14  *     * Neither the name of Google LLC nor the names of its
15  * contributors may be used to endorse or promote products derived from
16  * this software without specific prior written permission.
17  *
18  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29  */
30 package com.google.api.gax.httpjson.testing;
31 
32 import com.google.api.client.http.HttpMethods;
33 import com.google.api.client.http.LowLevelHttpRequest;
34 import com.google.api.client.http.LowLevelHttpResponse;
35 import com.google.api.client.testing.http.MockHttpTransport;
36 import com.google.api.client.testing.http.MockLowLevelHttpRequest;
37 import com.google.api.client.testing.http.MockLowLevelHttpResponse;
38 import com.google.api.gax.httpjson.ApiMethodDescriptor;
39 import com.google.api.gax.httpjson.ApiMethodDescriptor.MethodType;
40 import com.google.api.pathtemplate.PathTemplate;
41 import com.google.common.base.Preconditions;
42 import com.google.common.collect.ImmutableList;
43 import com.google.common.collect.ImmutableListMultimap;
44 import com.google.common.collect.LinkedListMultimap;
45 import com.google.common.collect.Multimap;
46 import java.time.Duration;
47 import java.util.LinkedList;
48 import java.util.List;
49 import java.util.Queue;
50 
51 /**
52  * Mocks an HTTPTransport. Expected responses and exceptions can be added to a queue from which this
53  * mock HttpTransport polls when it relays a response.
54  *
55  * <p>As required by {@link MockHttpTransport} this implementation is thread-safe, but it is not
56  * idempotent (as a typical service would be) and must be used with extra caution. Mocked responses
57  * are returned in FIFO order and if multiple threads read from the same MockHttpService
58  * simultaneously, they may be getting responses intended for other consumers.
59  */
60 public final class MockHttpService extends MockHttpTransport {
61   private final Multimap<String, String> requestHeaders = LinkedListMultimap.create();
62   private final List<String> requestPaths = new LinkedList<>();
63   private final Queue<HttpResponseFactory> responseHandlers = new LinkedList<>();
64   private final List<ApiMethodDescriptor> serviceMethodDescriptors;
65   private final String endpoint;
66 
67   /**
68    * Create a MockHttpService.
69    *
70    * @param serviceMethodDescriptors - list of method descriptors for the methods that this mock
71    *     server supports
72    * @param pathPrefix - the fixed portion of the endpoint URL that prefixes the methods' path
73    *     template substring.
74    */
MockHttpService(List<ApiMethodDescriptor> serviceMethodDescriptors, String pathPrefix)75   public MockHttpService(List<ApiMethodDescriptor> serviceMethodDescriptors, String pathPrefix) {
76     this.serviceMethodDescriptors = ImmutableList.copyOf(serviceMethodDescriptors);
77     this.endpoint = pathPrefix;
78   }
79 
80   @Override
buildRequest(String method, String url)81   public synchronized LowLevelHttpRequest buildRequest(String method, String url) {
82     requestPaths.add(url);
83     return new MockHttpRequest(this, method, url);
84   }
85 
86   /** Add an ApiMessage to the response queue. */
addResponse(Object response)87   public synchronized void addResponse(Object response) {
88     responseHandlers.add(new MessageResponseFactory(endpoint, serviceMethodDescriptors, response));
89   }
90 
addResponse(Object response, Duration delay)91   public synchronized void addResponse(Object response, Duration delay) {
92     responseHandlers.add(
93         new MessageResponseFactory(endpoint, serviceMethodDescriptors, response, delay));
94   }
95 
96   /** Add an expected null response (empty HTTP response body) with a custom status code. */
addNullResponse(int statusCode)97   public synchronized void addNullResponse(int statusCode) {
98     responseHandlers.add(
99         (httpMethod, targetUrl) -> new MockLowLevelHttpResponse().setStatusCode(statusCode));
100   }
101 
102   /** Add an expected null response (empty HTTP response body). */
addNullResponse()103   public synchronized void addNullResponse() {
104     addNullResponse(200);
105   }
106 
107   /** Add an Exception to the response queue. */
addException(Exception exception)108   public synchronized void addException(Exception exception) {
109     addException(400, exception);
110   }
111 
addException(int statusCode, Exception exception)112   public synchronized void addException(int statusCode, Exception exception) {
113     responseHandlers.add(new ExceptionResponseFactory(statusCode, exception));
114   }
115 
116   /** Get the FIFO list of URL paths to which requests were sent. */
getRequestPaths()117   public synchronized List<String> getRequestPaths() {
118     return requestPaths;
119   }
120 
121   /** Get the FIFO list of request headers sent. */
getRequestHeaders()122   public synchronized Multimap<String, String> getRequestHeaders() {
123     return ImmutableListMultimap.copyOf(requestHeaders);
124   }
125 
putRequestHeader(String name, String value)126   private synchronized void putRequestHeader(String name, String value) {
127     requestHeaders.put(name, value);
128   }
129 
getHttpResponse(String method, String url)130   private synchronized MockLowLevelHttpResponse getHttpResponse(String method, String url) {
131     Preconditions.checkArgument(!responseHandlers.isEmpty());
132     return responseHandlers.poll().getHttpResponse(method, url);
133   }
134 
135   /* Reset the expected response queue, the method descriptor, and the logged request paths list. */
reset()136   public synchronized void reset() {
137     responseHandlers.clear();
138     requestPaths.clear();
139     requestHeaders.clear();
140   }
141 
142   private interface HttpResponseFactory {
getHttpResponse(String httpMethod, String targetUrl)143     MockLowLevelHttpResponse getHttpResponse(String httpMethod, String targetUrl);
144   }
145 
146   private static class MockHttpRequest extends MockLowLevelHttpRequest {
147     private final MockHttpService service;
148     private final String method;
149     private final String url;
150 
MockHttpRequest(MockHttpService service, String method, String url)151     public MockHttpRequest(MockHttpService service, String method, String url) {
152       this.service = service;
153       this.method = method;
154       this.url = url;
155     }
156 
157     @Override
addHeader(String name, String value)158     public void addHeader(String name, String value) {
159       service.putRequestHeader(name, value);
160     }
161 
162     @Override
execute()163     public LowLevelHttpResponse execute() {
164       return service.getHttpResponse(method, url);
165     }
166   }
167 
168   private static class ExceptionResponseFactory implements HttpResponseFactory {
169     private final int statusCode;
170     private final Exception exception;
171 
ExceptionResponseFactory(int statusCode, Exception exception)172     public ExceptionResponseFactory(int statusCode, Exception exception) {
173       this.statusCode = statusCode;
174       this.exception = exception;
175     }
176 
177     @Override
getHttpResponse(String httpMethod, String targetUrl)178     public MockLowLevelHttpResponse getHttpResponse(String httpMethod, String targetUrl) {
179       MockLowLevelHttpResponse httpResponse = new MockLowLevelHttpResponse();
180       httpResponse.setStatusCode(statusCode);
181       httpResponse.setContent(exception.toString().getBytes());
182       httpResponse.setContentEncoding("text/plain");
183       return httpResponse;
184     }
185   }
186 
187   private static class MessageResponseFactory implements HttpResponseFactory {
188     private final List<ApiMethodDescriptor> serviceMethodDescriptors;
189     private final Object response;
190     private final String endpoint;
191     private final Duration delay;
192 
MessageResponseFactory( String endpoint, List<ApiMethodDescriptor> serviceMethodDescriptors, Object response)193     public MessageResponseFactory(
194         String endpoint, List<ApiMethodDescriptor> serviceMethodDescriptors, Object response) {
195       this(endpoint, serviceMethodDescriptors, response, Duration.ofNanos(0));
196     }
197 
MessageResponseFactory( String endpoint, List<ApiMethodDescriptor> serviceMethodDescriptors, Object response, Duration delay)198     public MessageResponseFactory(
199         String endpoint,
200         List<ApiMethodDescriptor> serviceMethodDescriptors,
201         Object response,
202         Duration delay) {
203       this.endpoint = endpoint;
204       this.serviceMethodDescriptors = ImmutableList.copyOf(serviceMethodDescriptors);
205       this.response = response;
206       this.delay = delay;
207     }
208 
209     @Override
getHttpResponse(String httpMethod, String fullTargetUrl)210     public MockLowLevelHttpResponse getHttpResponse(String httpMethod, String fullTargetUrl) {
211       // We use Thread.sleep to mimic a long server response. Most tests should not
212       // require a sleep and can return a response immediately.
213       try {
214         long delayMs = delay.toMillis();
215         if (delayMs > 0) {
216           Thread.sleep(delayMs);
217         }
218       } catch (InterruptedException e) {
219         throw new RuntimeException(e);
220       }
221       MockLowLevelHttpResponse httpResponse = new MockLowLevelHttpResponse();
222 
223       String relativePath = getRelativePath(fullTargetUrl);
224 
225       for (ApiMethodDescriptor methodDescriptor : serviceMethodDescriptors) {
226         // Check the comment in com.google.api.gax.httpjson.HttpRequestRunnable.buildRequest()
227         // method for details why it is needed.
228         String descriptorHttpMethod = methodDescriptor.getHttpMethod();
229         if (!httpMethod.equals(descriptorHttpMethod)) {
230           if (!(HttpMethods.PATCH.equals(descriptorHttpMethod)
231               && HttpMethods.POST.equals(httpMethod))) {
232             continue;
233           }
234         }
235 
236         PathTemplate pathTemplate = methodDescriptor.getRequestFormatter().getPathTemplate();
237         List<PathTemplate> additionalPathTemplates =
238             methodDescriptor.getRequestFormatter().getAdditionalPathTemplates();
239         // Server figures out which RPC method is called based on the endpoint path pattern(s).
240         if (!pathTemplate.matches(relativePath)
241             && additionalPathTemplates.stream().noneMatch(pt -> pt.matches(relativePath))) {
242           continue;
243         }
244 
245         // Emulate the server's creation of an HttpResponse from the response message
246         // instance.
247         String httpContent;
248         if (methodDescriptor.getType() == MethodType.SERVER_STREAMING) {
249           // Quick and dirty json array construction. Good enough for
250           Object[] responseArray = (Object[]) response;
251           StringBuilder sb = new StringBuilder();
252           sb.append('[');
253           for (Object responseElement : responseArray) {
254             if (sb.length() > 1) {
255               sb.append(',');
256             }
257             sb.append(methodDescriptor.getResponseParser().serialize(responseElement));
258           }
259           sb.append(']');
260           httpContent = sb.toString();
261         } else {
262           httpContent = methodDescriptor.getResponseParser().serialize(response);
263         }
264 
265         httpResponse.setContent(httpContent.getBytes());
266         httpResponse.setStatusCode(200);
267         return httpResponse;
268       }
269 
270       // Return 404 when none of this server's endpoint templates match the given URL.
271       httpResponse.setContent(
272           String.format("Method not found for path '%s'", relativePath).getBytes());
273       httpResponse.setStatusCode(404);
274       return httpResponse;
275     }
276 
getRelativePath(String fullTargetUrl)277     private String getRelativePath(String fullTargetUrl) {
278       // relativePath will be repeatedly truncated until it contains only
279       // the path template substring of the endpoint URL.
280       String relativePath = fullTargetUrl.replaceFirst(endpoint, "");
281       int queryParamIndex = relativePath.indexOf("?");
282       queryParamIndex = queryParamIndex < 0 ? relativePath.length() : queryParamIndex;
283       relativePath = relativePath.substring(0, queryParamIndex);
284 
285       return relativePath;
286     }
287   }
288 }
289