1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.adservices.utils;
18 
19 import android.util.ArrayMap;
20 import android.util.Pair;
21 
22 import androidx.test.core.app.ApplicationProvider;
23 
24 import com.android.adservices.LoggerFactory;
25 
26 import com.google.common.collect.ImmutableMap;
27 
28 import org.json.JSONArray;
29 import org.json.JSONException;
30 import org.json.JSONObject;
31 import org.testng.util.Strings;
32 
33 import java.io.BufferedReader;
34 import java.io.IOException;
35 import java.io.InputStream;
36 import java.io.InputStreamReader;
37 import java.util.Map;
38 import java.util.Objects;
39 
40 public class ScenarioLoader {
41 
42     private static final LoggerFactory.Logger sLogger = LoggerFactory.getFledgeLogger();
43     public static final String FIELD_MOCKS = "mocks";
44     public static final String FIELD_REQUEST = "request";
45     public static final String FIELD_RESPONSE = "response";
46     public static final String FIELD_VERIFY_CALLED = "verify_called";
47     public static final String FIELD_VERIFY_NOT_CALLED = "verify_not_called";
48     public static final String FIELD_BODY = "body";
49     public static final String FIELD_VALUE_NULL = "null";
50     public static final String FIELD_BODY_STR = "body_str";
51     public static final String FIELD_DELAY_SEC = "delay_sec";
52     public static final String FIELD_SUBSTITUTIONS = "substitutions";
53 
load(String scenarioPath, Map<String, String> substitutionVariables)54     static Scenario load(String scenarioPath, Map<String, String> substitutionVariables)
55             throws JSONException, IOException {
56         // Parsing paths requires server URL upfront.
57         JSONObject json = new JSONObject(ScenarioLoader.loadTextResource(scenarioPath));
58         Map<String, String> variables = new ArrayMap<>();
59         // These variables come from two places. First, the user-configured variables supplied in
60         // the JSON config.
61         // Secondly, there are also variables that are specific to the address of the dispatcher,
62         // such as base_url and adtech1_url. These are built-in and provided by the system, and here
63         // are specifically supplied into this method.
64         variables.putAll(substitutionVariables);
65         variables.putAll(ScenarioLoader.parseSubstitutions(json, substitutionVariables));
66         return ScenarioLoader.parseScenario(json, variables);
67     }
68 
parseScenario(JSONObject json, Map<String, String> variables)69     private static Scenario parseScenario(JSONObject json, Map<String, String> variables)
70             throws JSONException {
71         ImmutableMap.Builder<Scenario.Request, Scenario.MockResponse> builder =
72                 ImmutableMap.builder();
73         JSONArray mocks = json.getJSONArray(FIELD_MOCKS);
74         for (int i = 0; i < mocks.length(); i++) {
75             JSONObject mockObject = mocks.getJSONObject(i);
76             Pair<Scenario.Request, Scenario.MockResponse> mock = parseMock(mockObject, variables);
77             builder.put(mock.first, mock.second);
78         }
79         return new Scenario(builder.build());
80     }
81 
parseMock( JSONObject mock, Map<String, String> variables)82     private static Pair<Scenario.Request, Scenario.MockResponse> parseMock(
83             JSONObject mock, Map<String, String> variables) throws JSONException {
84         if (Objects.isNull(mock)) {
85             throw new IllegalArgumentException("mock JSON object is null.");
86         }
87 
88         JSONObject requestJson = mock.getJSONObject(FIELD_REQUEST);
89         JSONObject responseJson = mock.getJSONObject(FIELD_RESPONSE);
90         if (Objects.isNull(requestJson) || Objects.isNull(responseJson)) {
91             throw new IllegalArgumentException("request or response JSON object is null.");
92         }
93 
94         Scenario.Request request = parseRequest(requestJson, variables);
95         Scenario.MockResponse parsedMock =
96                 Scenario.MockResponse.newBuilder()
97                         .setShouldVerifyCalled(
98                                 ScenarioLoader.parseBooleanOptionalOrDefault(
99                                         FIELD_VERIFY_CALLED, mock))
100                         .setShouldVerifyNotCalled(
101                                 ScenarioLoader.parseBooleanOptionalOrDefault(
102                                         FIELD_VERIFY_NOT_CALLED, mock))
103                         .setDefaultResponse(parseResponse(responseJson, variables).build())
104                         .build();
105         sLogger.v("Setting up mock at path: " + request.getRelativePath());
106         return Pair.create(request, parsedMock);
107     }
108 
parseRequest(JSONObject json, Map<String, String> variables)109     private static Scenario.Request parseRequest(JSONObject json, Map<String, String> variables) {
110         Scenario.Request.Builder builder = Scenario.Request.newBuilder();
111         try {
112             String rawPath = getStringWithSubstitutions(json.getString("path"), variables);
113             if (!rawPath.startsWith("/")) {
114                 throw new IllegalStateException("path should start with '/' prefix: " + rawPath);
115             }
116             builder.setRelativePath(rawPath.substring(1));
117         } catch (JSONException e) {
118             throw new IllegalArgumentException("could not extract `path` from request", e);
119         }
120 
121         try {
122             builder.setHeaders(ScenarioLoader.parseHeaders(json.getJSONObject("header")));
123         } catch (JSONException e) {
124             builder.setHeaders(ImmutableMap.of());
125         }
126 
127         return builder.build();
128     }
129 
parseResponse( JSONObject json, Map<String, String> variables)130     private static Scenario.Response.Builder parseResponse(
131             JSONObject json, Map<String, String> variables) {
132         Scenario.Response.Builder builder =
133                 Scenario.Response.newBuilder().setBody(Scenarios.DEFAULT_RESPONSE_BODY);
134         try {
135             if (json.has(FIELD_BODY)) {
136                 String fileName = json.getString(FIELD_BODY);
137                 String filePath = Scenarios.SCENARIOS_DATA_JARPATH + fileName;
138                 if (!fileName.equals(FIELD_VALUE_NULL)) {
139                     builder.setBody(loadTextResourceWithSubstitutions(filePath, variables));
140                 }
141             } else if (json.has(FIELD_BODY_STR)) {
142                 builder.setBody(json.getString(FIELD_BODY_STR));
143             } else {
144                 throw new IllegalArgumentException(
145                         "response must set `body` or `body_str`: " + json);
146             }
147         } catch (JSONException e) {
148             throw new IllegalArgumentException("could not parse response: " + json);
149         }
150 
151         if (json.has("header")) {
152             try {
153                 builder.setHeaders(ScenarioLoader.parseHeaders(json.getJSONObject("header")));
154             } catch (JSONException e) {
155                 builder.setHeaders(ImmutableMap.of());
156             }
157         } else {
158             builder.setHeaders(ImmutableMap.of());
159         }
160 
161         builder.setDelaySeconds(
162                 ScenarioLoader.parseIntegerOptionalOrDefault(FIELD_DELAY_SEC, json));
163 
164         return builder;
165     }
166 
parseSubstitutions( JSONObject json, Map<String, String> substitutionVariables)167     private static ImmutableMap<String, String> parseSubstitutions(
168             JSONObject json, Map<String, String> substitutionVariables) throws JSONException {
169         ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
170         if (!json.has(FIELD_SUBSTITUTIONS)) {
171             return builder.build();
172         }
173 
174         JSONObject substitutions = json.getJSONObject(FIELD_SUBSTITUTIONS);
175         for (String key : substitutions.keySet()) {
176             try {
177                 builder.put(
178                         key,
179                         getStringWithSubstitutions(
180                                 substitutions.getString(key), substitutionVariables));
181             } catch (JSONException e) {
182                 throw new IllegalArgumentException("could not parse substitution with key: " + key);
183             }
184         }
185         return builder.build();
186     }
187 
parseBooleanOptionalOrDefault(String field, JSONObject json)188     private static boolean parseBooleanOptionalOrDefault(String field, JSONObject json) {
189         try {
190             return json.getBoolean(field);
191         } catch (JSONException e) {
192             return false;
193         }
194     }
195 
parseIntegerOptionalOrDefault(String field, JSONObject json)196     private static int parseIntegerOptionalOrDefault(String field, JSONObject json) {
197         try {
198             return json.getInt(field);
199         } catch (JSONException e) {
200             return 0;
201         }
202     }
203 
parseHeaders(JSONObject json)204     private static Map<String, String> parseHeaders(JSONObject json) {
205         ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
206         for (String key : json.keySet()) {
207             try {
208                 builder.put(key, json.getString(key));
209             } catch (JSONException ignored) {
210                 throw new IllegalArgumentException("could not parse header with key: " + key);
211             }
212         }
213         return builder.build();
214     }
215 
loadTextResource(String fileName)216     private static String loadTextResource(String fileName) throws IOException {
217         InputStream is = ApplicationProvider.getApplicationContext().getAssets().open(fileName);
218         StringBuilder builder = new StringBuilder();
219         try (BufferedReader br = new BufferedReader(new InputStreamReader(is))) {
220             String line;
221             while ((line = br.readLine()) != null) {
222                 builder.append(line).append("\n");
223             }
224         }
225         return builder.toString();
226     }
227 
getStringWithSubstitutions(String string, Map<String, String> variables)228     private static String getStringWithSubstitutions(String string, Map<String, String> variables) {
229         // Apply substitutions to string.
230         for (Map.Entry<String, String> keyValuePair : variables.entrySet()) {
231             string = string.replace(keyValuePair.getKey(), keyValuePair.getValue());
232         }
233         return string;
234     }
235 
loadTextResourceWithSubstitutions( String fileName, Map<String, String> variables)236     private static String loadTextResourceWithSubstitutions(
237             String fileName, Map<String, String> variables) {
238         String responseBody;
239         try {
240             responseBody = ScenarioLoader.loadTextResource(fileName);
241             sLogger.v("loading file: " + fileName);
242         } catch (IOException e) {
243             throw new IllegalArgumentException("failed to load fake response body: " + fileName, e);
244         }
245 
246         if (Strings.isNullOrEmpty(responseBody)) {
247             return responseBody;
248         }
249         for (Map.Entry<String, String> keyValuePair : variables.entrySet()) {
250             responseBody = responseBody.replace(keyValuePair.getKey(), keyValuePair.getValue());
251         }
252 
253         return responseBody;
254     }
255 }
256