1 // Copyright 2019 The Chromium Authors 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.net.test; 6 7 import android.content.Context; 8 9 import org.chromium.net.CronetEngine; 10 import org.chromium.net.ExperimentalCronetEngine; 11 import org.chromium.net.UrlRequest; 12 13 import java.util.ArrayList; 14 import java.util.Collections; 15 import java.util.List; 16 import java.util.Map; 17 18 /** 19 * Controller for fake Cronet implementation. Allows a test to setup responses for 20 * {@link UrlRequest}s. If multiple {@link ResponseMatcher}s match a specific request, the first 21 * {@link ResponseMatcher} added takes precedence. 22 */ 23 public final class FakeCronetController { 24 // List of FakeCronetEngines so that FakeCronetEngine can be accessed when created with 25 // the {@link FakeCronetProvider}. 26 private static final List<CronetEngine> sInstances = 27 Collections.synchronizedList(new ArrayList<>()); 28 29 // List of ResponseMatchers to be checked for a response to a request in place of a server. 30 private final List<ResponseMatcher> mResponseMatchers = 31 Collections.synchronizedList(new ArrayList<>()); 32 33 /** 34 * Creates a fake {@link CronetEngine.Builder} that creates {@link CronetEngine}s that return 35 * fake {@link UrlRequests}. Once built, the {@link CronetEngine}'s {@link UrlRequest}s will 36 * retrieve responses from this {@link FakeCronetController}. 37 * 38 * @param context the Android context to build the fake {@link CronetEngine} from. 39 * @return a fake CronetEngine.Builder that uses this {@link FakeCronetController} to manage 40 * responses once it is built. 41 */ newFakeCronetEngineBuilder(Context context)42 public CronetEngine.Builder newFakeCronetEngineBuilder(Context context) { 43 FakeCronetEngine.Builder builder = new FakeCronetEngine.Builder(context); 44 builder.setController(this); 45 // FakeCronetEngine.Builder is not actually a CronetEngine.Builder, so construct one with 46 // the child of CronetEngine.Builder: ExperimentalCronetEngine.Builder. 47 return new ExperimentalCronetEngine.Builder(builder); 48 } 49 50 /** 51 * Adds a {@link UrlResponseMatcher} that will respond to the provided URL with the provided 52 * {@link FakeUrlResponse}. Equivalent to: 53 * addResponseMatcher(new UrlResponseMatcher(url, response)). 54 * 55 * @param response a {@link FakeUrlResponse} to respond with 56 * @param url a url for which the response should be returned 57 */ addResponseForUrl(FakeUrlResponse response, String url)58 public void addResponseForUrl(FakeUrlResponse response, String url) { 59 addResponseMatcher(new UrlResponseMatcher(url, response)); 60 } 61 62 /** 63 * Adds a {@link ResponseMatcher} to the list of {@link ResponseMatcher}s. 64 * 65 * @param matcher the {@link ResponseMatcher} that should be matched against a request 66 */ addResponseMatcher(ResponseMatcher matcher)67 public void addResponseMatcher(ResponseMatcher matcher) { 68 mResponseMatchers.add(matcher); 69 } 70 71 /** 72 * Removes a specific {@link ResponseMatcher} from the list of {@link ResponseMatcher}s. 73 * 74 * @param matcher the {@link ResponseMatcher} to remove 75 */ removeResponseMatcher(ResponseMatcher matcher)76 public void removeResponseMatcher(ResponseMatcher matcher) { 77 mResponseMatchers.remove(matcher); 78 } 79 80 /** Removes all {@link ResponseMatcher}s from the list of {@link ResponseMatcher}s. */ clearResponseMatchers()81 public void clearResponseMatchers() { 82 mResponseMatchers.clear(); 83 } 84 85 /** 86 * Adds a {@link FakeUrlResponse} to the list of responses that will redirect a 87 * {@link UrlRequest} to the specified URL. 88 * 89 * @param redirectLocation the URL to redirect the {@link UrlRequest} to 90 * @param url the URL that will trigger the redirect 91 */ addRedirectResponse(String redirectLocation, String url)92 public void addRedirectResponse(String redirectLocation, String url) { 93 FakeUrlResponse redirectResponse = 94 new FakeUrlResponse.Builder() 95 .setHttpStatusCode(302) 96 .addHeader("location", redirectLocation) 97 .build(); 98 addResponseForUrl(redirectResponse, url); 99 } 100 101 /** 102 * Adds an {@link FakeUrlResponse} that fails with the specified HTTP code for the specified 103 * URL. 104 * 105 * @param statusCode the code for the {@link FakeUrlResponse} 106 * @param url the URL that should trigger the error response when requested by a 107 * {@link UrlRequest} 108 * @throws IllegalArgumentException if the HTTP status code is not an error code (code >= 400) 109 */ addHttpErrorResponse(int statusCode, String url)110 public void addHttpErrorResponse(int statusCode, String url) { 111 addResponseForUrl(getFailedResponse(statusCode), url); 112 } 113 114 // TODO(kirchman): Create a function to add a response that takes a CronetException. 115 116 /** 117 * Adds a successful 200 code {@link FakeUrlResponse} that will match the specified 118 * URL when requested by a {@link UrlRequest}. 119 * 120 * @param url the URL that triggers the successful response 121 * @param body the body of the response as a byte array 122 */ addSuccessResponse(String url, byte[] body)123 public void addSuccessResponse(String url, byte[] body) { 124 addResponseForUrl(new FakeUrlResponse.Builder().setResponseBody(body).build(), url); 125 } 126 127 /** 128 * Returns the {@link CronetEngineController} for a specified {@link CronetEngine}. This method 129 * should be used in conjunction with {@link FakeCronetController.getInstances}. 130 * 131 * @param engine the fake {@link CronetEngine} to get the controller for. 132 * @return the controller for the specified fake {@link CronetEngine}. 133 */ getControllerForFakeEngine(CronetEngine engine)134 public static FakeCronetController getControllerForFakeEngine(CronetEngine engine) { 135 if (engine instanceof FakeCronetEngine) { 136 FakeCronetEngine fakeEngine = (FakeCronetEngine) engine; 137 return fakeEngine.getController(); 138 } 139 throw new IllegalArgumentException("Provided CronetEngine is not a fake CronetEngine"); 140 } 141 142 /** 143 * Returns all created fake instances of {@link CronetEngine} that have not been shut down with 144 * {@link CronetEngine.shutdown()} in order of creation. Can be used to retrieve a controller 145 * in conjunction with {@link FakeCronetController.getControllerForFakeEngine}. 146 * 147 * @return a list of all fake {@link CronetEngine}s that have been created 148 */ getFakeCronetEngines()149 public static List<CronetEngine> getFakeCronetEngines() { 150 synchronized (sInstances) { 151 return new ArrayList<>(sInstances); 152 } 153 } 154 155 /** 156 * Removes a fake {@link CronetEngine} from the list of {@link CronetEngine} instances. 157 * 158 * @param cronetEngine the instance to remove 159 */ removeFakeCronetEngine(CronetEngine cronetEngine)160 static void removeFakeCronetEngine(CronetEngine cronetEngine) { 161 sInstances.remove(cronetEngine); 162 } 163 164 /** 165 * Add a CronetEngine to the list of CronetEngines. 166 * 167 * @param engine the {@link CronetEngine} to add 168 */ addFakeCronetEngine(FakeCronetEngine engine)169 static void addFakeCronetEngine(FakeCronetEngine engine) { 170 sInstances.add(engine); 171 } 172 173 /** 174 * Gets a response for specified request details if there is one, or a "404" failed response 175 * if there is no {@link ResponseMatcher} with a {@link FakeUrlResponse} for this request. 176 * 177 * @param url the URL that the {@link UrlRequest} is connecting to 178 * @param httpMethod the HTTP method that the {@link UrlRequest} is using to connect with 179 * @param headers the headers supplied by the {@link UrlRequest} 180 * @param body the body of the fake HTTP request 181 * @return a {@link FakeUrlResponse} if there is one, or a failed "404" response if none found 182 */ getResponse( String url, String httpMethod, List<Map.Entry<String, String>> headers, byte[] body)183 FakeUrlResponse getResponse( 184 String url, String httpMethod, List<Map.Entry<String, String>> headers, byte[] body) { 185 synchronized (mResponseMatchers) { 186 for (ResponseMatcher responseMatcher : mResponseMatchers) { 187 FakeUrlResponse matchedResponse = 188 responseMatcher.getMatchingResponse(url, httpMethod, headers, body); 189 if (matchedResponse != null) { 190 return matchedResponse; 191 } 192 } 193 } 194 return getFailedResponse(404); 195 } 196 197 /** 198 * Creates and returns a failed response with the specified HTTP status code. 199 * 200 * @param statusCode the HTTP code that the returned response will have 201 * @return a {@link FakeUrlResponse} with the specified code 202 */ getFailedResponse(int statusCode)203 private static FakeUrlResponse getFailedResponse(int statusCode) { 204 if (statusCode < 400) { 205 throw new IllegalArgumentException( 206 "Expected HTTP error code (code >= 400), but was: " + statusCode); 207 } 208 return new FakeUrlResponse.Builder().setHttpStatusCode(statusCode).build(); 209 } 210 } 211