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