List of Input Parameters for VSP
[sdc.git] / openecomp-be / lib / openecomp-sdc-externaltesting-lib / openecomp-sdc-externaltesting-impl / src / main / java / org / openecomp / core / externaltesting / impl / ExternalTestingManagerImpl.java
1 /*
2  * Copyright © 2019 iconectiv
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 org.openecomp.core.externaltesting.impl;
18
19 import com.fasterxml.jackson.core.JsonProcessingException;
20 import com.fasterxml.jackson.databind.ObjectMapper;
21 import com.google.gson.GsonBuilder;
22 import com.google.gson.JsonObject;
23 import com.google.gson.JsonParseException;
24 import org.apache.commons.lang3.ArrayUtils;
25 import org.apache.commons.lang3.StringUtils;
26 import org.onap.sdc.tosca.services.YamlUtil;
27 import org.openecomp.core.externaltesting.api.*;
28 import org.openecomp.core.externaltesting.errors.ExternalTestingException;
29 import org.slf4j.Logger;
30 import org.slf4j.LoggerFactory;
31 import org.springframework.beans.factory.annotation.Autowired;
32 import org.springframework.core.ParameterizedTypeReference;
33 import org.springframework.http.*;
34 import org.springframework.http.client.SimpleClientHttpRequestFactory;
35 import org.springframework.util.LinkedMultiValueMap;
36 import org.springframework.util.MultiValueMap;
37 import org.springframework.web.client.HttpStatusCodeException;
38 import org.springframework.web.client.ResourceAccessException;
39 import org.springframework.web.client.RestTemplate;
40 import org.springframework.web.util.UriComponentsBuilder;
41
42 import javax.annotation.PostConstruct;
43 import java.io.FileInputStream;
44 import java.io.IOException;
45 import java.io.InputStream;
46 import java.util.*;
47 import java.util.stream.Collectors;
48
49 public class ExternalTestingManagerImpl implements ExternalTestingManager {
50
51   private Logger logger = LoggerFactory.getLogger(ExternalTestingManagerImpl.class);
52
53   private static final String HTTP_STATUS = "httpStatus";
54   private static final String CODE = "code";
55   private static final String ERROR = "error";
56   private static final String MESSAGE = "message";
57   private static final String DETAIL = "detail";
58   private static final String PATH = "path";
59
60   private static final String CONFIG_FILE_PROPERTY = "configuration.yaml";
61   private static final String CONFIG_SECTION = "externalTestingConfig";
62
63   private static final String VTP_SCENARIOS_URI = "%s/v1/vtp/scenarios";
64   private static final String VTP_TESTSUITE_URI = "%s/v1/vtp/scenarios/%s/testsuites";
65   private static final String VTP_TESTCASES_URI = "%s/v1/vtp/scenarios/%s/testcases";
66   private static final String VTP_TESTCASE_URI = "%s/v1/vtp/scenarios/%s/testsuites/%s/testcases/%s";
67   private static final String VTP_EXECUTIONS_URI = "%s/v1/vtp/executions";
68   private static final String VTP_EXECUTION_URI = "%s/v1/vtp/executions/%s";
69
70   private static final String INVALIDATE_STATE_ERROR = "Invalid State";
71   private static final String NO_ACCESS_CONFIGURATION_DEFINED = "No access configuration defined";
72
73   private TestingAccessConfig accessConfig;
74   private Map<String, RemoteTestingEndpointDefinition> endpoints = new HashMap<>();
75
76   private RestTemplate restTemplate;
77
78   private List<VariableResolver> variableResolvers;
79
80   public ExternalTestingManagerImpl(@Autowired(required=false) List<VariableResolver> variableResolvers) {
81     this.variableResolvers = variableResolvers;
82     // nothing to do at the moment.
83     restTemplate = new RestTemplate();
84   }
85
86   /**
87    * Read the configuration from the yaml file for this bean.  If we get an exception during load,
88    * don't force an error starting SDC but log a warning.  Do no warm...
89    */
90   @PostConstruct
91   public void loadConfig() {
92
93     String file = Objects.requireNonNull(System.getProperty(CONFIG_FILE_PROPERTY),
94         "Config file location must be specified via system property " + CONFIG_FILE_PROPERTY);
95     try {
96       Object rawConfig = getExternalTestingAccessConfiguration(file);
97       if (rawConfig != null) {
98         accessConfig = new ObjectMapper().convertValue(rawConfig, TestingAccessConfig.class);
99         accessConfig.getEndpoints()
100             .stream()
101             .filter(RemoteTestingEndpointDefinition::isEnabled)
102             .forEach(e -> endpoints.put(e.getId(), e));
103       }
104     }
105     catch (IOException ex) {
106       logger.warn("Unable to initialize external testing configuration.  Add '" + CONFIG_SECTION + "' to configuration.yaml with url value.  Feature will be hobbled with results hardcoded to empty values.", ex);
107     }
108   }
109
110   /**
111    * Return the configuration of this feature that we want to
112    * expose to the client.  Treated as a JSON blob for flexibility.
113    */
114   @Override
115   public String getConfig() {
116     ClientConfiguration cc = null;
117     if (accessConfig != null) {
118       cc = accessConfig.getClient();
119     }
120     if (cc == null) {
121       cc = new ClientConfiguration();
122       cc.setEnabled(false);
123     }
124     try {
125       return new ObjectMapper().writeValueAsString(cc);
126     } catch (JsonProcessingException e) {
127       logger.error("failed to write client config", e);
128       return "{\"enabled\":false}";
129     }
130   }
131
132   @Override
133   public TestTreeNode getTestCasesAsTree() {
134     TestTreeNode root = new TestTreeNode("root", "root");
135
136     // quick out in case of non-configured SDC
137     if (accessConfig == null) {
138       return root;
139     }
140
141     for (RemoteTestingEndpointDefinition ep : accessConfig.getEndpoints()) {
142       if (ep.isEnabled()) {
143         buildTreeFromEndpoint(ep, root);
144       }
145     }
146     return root;
147   }
148
149   private void buildTreeFromEndpoint(RemoteTestingEndpointDefinition ep, TestTreeNode root) {
150     try {
151       logger.debug("process endpoint {}", ep.getId());
152       getScenarios(ep.getId()).stream().filter(s ->
153           ((ep.getScenarioFilter() == null) || ep.getScenarioFilterPattern().matcher(s.getName()).matches()))
154           .forEach(s -> {
155             addScenarioToTree(root, s);
156             getTestSuites(ep.getId(), s.getName()).forEach(suite -> addSuiteToTree(root, s, suite));
157             getTestCases(ep.getId(), s.getName()).forEach(tc -> {
158               try {
159                 VtpTestCase details = getTestCase(ep.getId(), s.getName(), tc.getTestSuiteName(), tc.getTestCaseName());
160                 addTestCaseToTree(root, ep.getId(), s.getName(), tc.getTestSuiteName(), details);
161               }
162               catch (@SuppressWarnings("squid:S1166") ExternalTestingException ex) {
163                 // Not logging stack trace on purpose.  VTP was throwing exceptions for certain test cases.
164                 logger.warn("failed to load test case {}", tc.getTestCaseName());
165               }
166             });
167           });
168     }
169     catch (ExternalTestingException ex) {
170       logger.error("unable to contact testing endpoint {}", ep.getId(), ex);
171     }
172   }
173
174   private Optional<TestTreeNode> findNamedChild(TestTreeNode root, String name) {
175     if (root.getChildren() == null) {
176       return Optional.empty();
177     }
178     return root.getChildren().stream().filter(n->n.getName().equals(name)).findFirst();
179   }
180
181   /**
182    * Find the place in the tree to add the test case.
183    * @param root root of the tree.
184    * @param endpointName name of the endpoint to assign to the test case.
185    * @param scenarioName scenario to add this case to
186    * @param testSuiteName suite in the scenario to add this case to
187    * @param tc test case to add.
188    */
189   private void addTestCaseToTree(TestTreeNode root, String endpointName, String scenarioName, String testSuiteName, VtpTestCase tc) {
190     // return quickly.
191     if (tc == null) {
192       return;
193     }
194     findNamedChild(root, scenarioName)
195         .ifPresent(scenarioNode -> findNamedChild(scenarioNode, testSuiteName)
196             .ifPresent(suiteNode -> {
197               massageTestCaseForUI(tc, endpointName, scenarioName);
198               if (suiteNode.getTests() == null) {
199                 suiteNode.setTests(new ArrayList<>());
200               }
201               suiteNode.getTests().add(tc);
202             }));
203   }
204
205   private void massageTestCaseForUI(VtpTestCase testcase, String endpoint, String scenario) {
206     testcase.setEndpoint(endpoint);
207     // VTP workaround.
208     if (testcase.getScenario() == null) {
209       testcase.setScenario(scenario);
210     }
211
212     // if no inputs, return.
213     if (testcase.getInputs() == null) {
214       return;
215     }
216
217     // to work around a VTP limitation,
218     // any inputs that are marked as internal should not be sent to the client.
219     testcase.setInputs(testcase.getInputs()
220         .stream()
221         .filter(input -> (input.getMetadata() == null) ||
222             (!input.getMetadata().containsKey("internal")) ||
223             !"true".equals(input.getMetadata().get("internal").toString())).collect(Collectors.toList()));
224   }
225
226   /**
227    * Add the test suite to the tree at the appropriate place if it does not already exist in the tree.
228    * @param root root of the tree.
229    * @param scenario scenario under which this suite should be placed
230    * @param suite test suite to add.
231    */
232   private void addSuiteToTree(final TestTreeNode root, final VtpNameDescriptionPair scenario, final VtpNameDescriptionPair suite) {
233     findNamedChild(root, scenario.getName()).ifPresent(parent -> {
234       if (parent.getChildren() == null) {
235         parent.setChildren(new ArrayList<>());
236       }
237       if (parent.getChildren().stream().noneMatch(n -> StringUtils.equals(n.getName(), suite.getName()))) {
238         parent.getChildren().add(new TestTreeNode(suite.getName(), suite.getDescription()));
239       }
240     });
241   }
242
243   /**
244    * Add the scenario to the tree if it does not already exist.
245    * @param root root of the tree.
246    * @param s scenario to add.
247    */
248   private void addScenarioToTree(TestTreeNode root, VtpNameDescriptionPair s) {
249     logger.debug("addScenario {} to {} with {}", s.getName(), root.getName(), root.getChildren());
250     if (root.getChildren() == null) {
251       root.setChildren(new ArrayList<>());
252     }
253     if (root.getChildren().stream().noneMatch(n->StringUtils.equals(n.getName(),s.getName()))) {
254       logger.debug("createScenario {} in {}", s.getName(), root.getName());
255       root.getChildren().add(new TestTreeNode(s.getName(), s.getDescription()));
256     }
257   }
258
259   /**
260    * Get the list of endpoints defined to the testing manager.
261    * @return list of endpoints or empty list if the manager is not configured.
262    */
263   public List<VtpNameDescriptionPair> getEndpoints() {
264     if (accessConfig != null) {
265       return accessConfig.getEndpoints().stream()
266           .filter(RemoteTestingEndpointDefinition::isEnabled)
267           .map(e -> new VtpNameDescriptionPair(e.getId(), e.getTitle()))
268           .collect(Collectors.toList());
269     }
270     else {
271       return new ArrayList<>();
272     }
273   }
274
275   /**
276    * Get the list of scenarios at a given endpoint.
277    */
278   public List<VtpNameDescriptionPair> getScenarios(final String endpoint) {
279     String url = buildEndpointUrl(VTP_SCENARIOS_URI, endpoint, ArrayUtils.EMPTY_STRING_ARRAY);
280     ParameterizedTypeReference<List<VtpNameDescriptionPair>> t = new ParameterizedTypeReference<List<VtpNameDescriptionPair>>() {};
281     List<VtpNameDescriptionPair> rv = proxyGetRequestToExternalTestingSite(url, t);
282     if (rv == null) {
283       rv = new ArrayList<>();
284     }
285     return rv;
286   }
287
288   /**
289    * Get the list of test suites for an endpoint for the given scenario.
290    */
291   public List<VtpNameDescriptionPair> getTestSuites(final String endpoint, final String scenario) {
292     String url = buildEndpointUrl(VTP_TESTSUITE_URI, endpoint, new String[] {scenario});
293     ParameterizedTypeReference<List<VtpNameDescriptionPair>> t = new ParameterizedTypeReference<List<VtpNameDescriptionPair>>() {};
294     List<VtpNameDescriptionPair> rv = proxyGetRequestToExternalTestingSite(url, t);
295     if (rv == null) {
296       rv = new ArrayList<>();
297     }
298     return rv;
299   }
300
301   /**
302    * Get the list of test cases under a scenario.  This is the VTP API.  It would
303    * seem better to get the list of cases under a test suite but that is not supported.
304    */
305   @Override
306   public List<VtpTestCase> getTestCases(String endpoint, String scenario) {
307     String url = buildEndpointUrl(VTP_TESTCASES_URI, endpoint, new String[] {scenario});
308     ParameterizedTypeReference<List<VtpTestCase>> t = new ParameterizedTypeReference<List<VtpTestCase>>() {};
309     List<VtpTestCase> rv = proxyGetRequestToExternalTestingSite(url, t);
310     if (rv == null) {
311       rv = new ArrayList<>();
312     }
313     return rv;
314   }
315
316   /**
317    * Get a test case definition.
318    */
319   @Override
320   public VtpTestCase getTestCase(String endpoint, String scenario, String testSuite, String testCaseName) {
321     String url = buildEndpointUrl(VTP_TESTCASE_URI, endpoint, new String[] {scenario, testSuite, testCaseName});
322     ParameterizedTypeReference<VtpTestCase> t = new ParameterizedTypeReference<VtpTestCase>() {};
323     return proxyGetRequestToExternalTestingSite(url, t);
324   }
325
326   /**
327    * Return the results of a previous test execution.
328    * @param endpoint endpoint to query
329    * @param executionId execution to query.
330    * @return execution response from testing endpoint.
331    */
332   @Override
333   public VtpTestExecutionResponse getExecution(String endpoint,String executionId) {
334     String url = buildEndpointUrl(VTP_EXECUTION_URI, endpoint, new String[] {executionId});
335     ParameterizedTypeReference<VtpTestExecutionResponse> t = new ParameterizedTypeReference<VtpTestExecutionResponse>() {};
336     return proxyGetRequestToExternalTestingSite(url, t);
337   }
338
339   /**
340    * Execute a set of tests at a given endpoint.
341    * @param endpointName name of the endpoint
342    * @param testsToRun set of tests to run
343    * @return list of execution responses.
344    */
345   private List<VtpTestExecutionResponse> execute(final String endpointName, final List<VtpTestExecutionRequest> testsToRun, String requestId) {
346     if (accessConfig == null) {
347       throw new ExternalTestingException(INVALIDATE_STATE_ERROR, 500, NO_ACCESS_CONFIGURATION_DEFINED);
348     }
349
350     RemoteTestingEndpointDefinition endpoint = accessConfig.getEndpoints().stream()
351         .filter(e -> StringUtils.equals(endpointName, e.getId()))
352         .findFirst()
353         .orElseThrow(() -> new ExternalTestingException("No such endpoint", 500, "No endpoint named " + endpointName + " is defined"));
354
355     // if the endpoint requires an API key, specify it in the headers.
356     HttpHeaders headers = new HttpHeaders();
357     if (endpoint.getApiKey() != null) {
358       headers.add("X-API-Key", endpoint.getApiKey());
359     }
360     headers.setContentType(MediaType.MULTIPART_FORM_DATA);
361
362     // build the body.
363     MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
364     try {
365       // remove the endpoint from the test request since that is a FE/BE attribute
366       // add the execution profile configured for the endpoint.
367       testsToRun.forEach(t -> {
368         t.setEndpoint(null);
369         t.setProfile(t.getScenario()); // VTP wants a profile.  Use the scenario name.
370       });
371
372       body.add("executions", new ObjectMapper().writeValueAsString(testsToRun));
373     }
374     catch (Exception ex) {
375       logger.error("exception converting tests to string", ex);
376       VtpTestExecutionResponse err = new VtpTestExecutionResponse();
377       err.setHttpStatus(500);
378       err.setCode("500");
379       err.setMessage("Execution failed due to " + ex.getMessage());
380       return Collections.singletonList(err);
381     }
382
383     for(VtpTestExecutionRequest test: testsToRun) {
384       runVariableResolvers(test, body);
385     }
386
387
388     // form and send request.
389     HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
390     String url = buildEndpointUrl(VTP_EXECUTIONS_URI, endpointName, ArrayUtils.EMPTY_STRING_ARRAY);
391     UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url);
392     if (requestId != null) {
393       builder = builder.queryParam("requestId", requestId);
394     }
395     ParameterizedTypeReference<List<VtpTestExecutionResponse>> t = new ParameterizedTypeReference<List<VtpTestExecutionResponse>>() {};
396     try {
397       return proxyRequestToExternalTestingSite(builder.toUriString(), requestEntity, t);
398     }
399     catch (ExternalTestingException ex) {
400       logger.error("exception caught invoking endpoint {}", endpointName, ex);
401       VtpTestExecutionResponse err = new VtpTestExecutionResponse();
402       err.setHttpStatus(ex.getCode());
403       err.setCode(""+ex.getCode());
404       err.setMessage(ex.getTitle() + ": " + ex.getDetail());
405       return Collections.singletonList(err);
406     }
407   }
408
409   /**
410    * Execute tests splitting them across endpoints and collecting the results.
411    * @param testsToRun list of tests to be executed.
412    * @return collection of result objects.
413    */
414   @Override
415   public List<VtpTestExecutionResponse> execute(final List<VtpTestExecutionRequest> testsToRun, String requestId) {
416     if (accessConfig == null) {
417       throw new ExternalTestingException(INVALIDATE_STATE_ERROR, 500, NO_ACCESS_CONFIGURATION_DEFINED);
418     }
419
420     // partition the requests by endpoint.
421     Map<String, List<VtpTestExecutionRequest>> partitions =
422         testsToRun.stream().collect(Collectors.groupingBy(VtpTestExecutionRequest::getEndpoint));
423
424     // process each group and collect the results.
425     return partitions.entrySet().stream()
426         .flatMap(e -> execute(e.getKey(), e.getValue(), requestId).stream())
427         .collect(Collectors.toList());
428   }
429
430   /**
431    * Load the external testing access configuration from the SDC onboarding yaml configuration file.
432    * @param file filename to retrieve data from
433    * @return parsed YAML object
434    * @throws IOException thrown if failure in reading YAML content.
435    */
436   private Object getExternalTestingAccessConfiguration(String file) throws IOException {
437     Map<?, ?> configuration = Objects.requireNonNull(readConfigurationFile(file), "Configuration cannot be empty");
438     Object testingConfig = configuration.get(CONFIG_SECTION);
439     if (testingConfig == null) {
440       logger.warn("Unable to initialize external testing access configuration.  Add 'testingConfig' to configuration.yaml with url value.  Feature will be hobbled with results hardcoded to empty values.");
441     }
442
443     return testingConfig;
444   }
445
446   /**
447    * Load the onboarding yaml config file.
448    * @param file name of file to load
449    * @return map containing YAML properties.
450    * @throws IOException thrown in the event of YAML parse or IO failure.
451    */
452   private static Map<?, ?> readConfigurationFile(String file) throws IOException {
453     try (InputStream fileInput = new FileInputStream(file)) {
454       YamlUtil yamlUtil = new YamlUtil();
455       return yamlUtil.yamlToMap(fileInput);
456     }
457   }
458
459
460   /**
461    * Return URL with endpoint url as prefix.
462    * @param format format string.
463    * @param endpointName endpoint to address
464    * @param args args for format.
465    * @return qualified url.
466    */
467   private String buildEndpointUrl(String format, String endpointName, String[] args) {
468     if (accessConfig != null) {
469       RemoteTestingEndpointDefinition ep = endpoints.values().stream()
470           .filter(e -> e.getId().equals(endpointName))
471           .findFirst()
472           .orElseThrow(() -> new ExternalTestingException("No such endpoint", 500, "No endpoint named " + endpointName + " is defined")
473           );
474
475       Object[] newArgs = ArrayUtils.add(args, 0, ep.getUrl());
476       return String.format(format, newArgs);
477     }
478     throw new ExternalTestingException(INVALIDATE_STATE_ERROR, 500, NO_ACCESS_CONFIGURATION_DEFINED);
479   }
480
481   /**
482    * Proxy a get request to a testing endpoint.
483    * @param url URL to invoke.
484    * @param responseType type of response expected.
485    * @param <T> type of response expected
486    * @return instance of <T> parsed from the JSON response from endpoint.
487    */
488   private <T> T proxyGetRequestToExternalTestingSite(String url, ParameterizedTypeReference<T> responseType) {
489     return proxyRequestToExternalTestingSite(url, null, responseType);
490   }
491
492   /**
493    * Make the actual HTTP post (using Spring RestTemplate) to an endpoint.
494    * @param url URL to the endpoint
495    * @param request optional request body to send
496    * @param responseType expected type
497    * @param <T> extended type
498    * @return instance of expected type
499    */
500   private <R,T> T proxyRequestToExternalTestingSite(String url, HttpEntity<R> request, ParameterizedTypeReference<T> responseType) {
501     if (request != null) {
502       logger.debug("POST request to {} with {} for {}", url, request, responseType.getType().getTypeName());
503     }
504     else {
505       logger.debug("GET request to {} for {}", url, responseType.getType().getTypeName());
506     }
507     SimpleClientHttpRequestFactory rf =
508         (SimpleClientHttpRequestFactory) restTemplate.getRequestFactory();
509     if (rf != null) {
510       rf.setReadTimeout(10000);
511       rf.setConnectTimeout(10000);
512     }
513     ResponseEntity<T> re;
514     try {
515       if (request != null) {
516         re = restTemplate.exchange(url, HttpMethod.POST, request, responseType);
517       } else {
518         re = restTemplate.exchange(url, HttpMethod.GET, null, responseType);
519       }
520     }
521     catch (HttpStatusCodeException ex) {
522       // make my own exception out of this.
523       logger.warn("Unexpected HTTP Status from endpoint {}", ex.getRawStatusCode());
524       if ((ex.getResponseHeaders().getContentType() != null) &&
525           ((ex.getResponseHeaders().getContentType().isCompatibleWith(MediaType.APPLICATION_JSON)) ||
526               (ex.getResponseHeaders().getContentType().isCompatibleWith(MediaType.parseMediaType("application/problem+json"))))) {
527         String s = ex.getResponseBodyAsString();
528         logger.warn("endpoint body content is {}", s);
529         try {
530           JsonObject o = new GsonBuilder().create().fromJson(s, JsonObject.class);
531           throw buildTestingException(ex.getRawStatusCode(), o);
532         }
533         catch (JsonParseException e) {
534           logger.warn("unexpected JSON response", e);
535           throw new ExternalTestingException(ex.getStatusText(), ex.getStatusCode().value(), ex.getResponseBodyAsString(), ex);
536         }
537       }
538       else {
539         throw new ExternalTestingException(ex.getStatusText(), ex.getStatusCode().value(), ex.getResponseBodyAsString(), ex);
540       }
541     }
542     catch (ResourceAccessException ex) {
543       throw new ExternalTestingException("IO Error at Endpoint", 500, ex.getMessage(), ex);
544     }
545     catch (Exception ex) {
546       throw new ExternalTestingException(ex.getMessage(), 500, "Generic Exception", ex);
547     }
548     if (re != null) {
549       logger.debug("http status of {} from external testing entity {}", re.getStatusCodeValue(), url);
550       return re.getBody();
551     }
552     else {
553       logger.error("null response from endpoint");
554       return null;
555     }
556   }
557
558   /**
559    * Errors from the endpoint could conform to the expected ETSI body or not.
560    * Here we try to handle various response body elements.
561    * @param statusCode http status code in response.
562    * @param o JSON object parsed from the http response body
563    * @return Testing error body that should be returned to the caller
564    */
565   private ExternalTestingException buildTestingException(int statusCode, JsonObject o) {
566     String code = null;
567     String message = null;
568
569     if (o.has(CODE)) {
570       code = o.get(CODE).getAsString();
571     }
572     else if (o.has(ERROR)) {
573       code = o.get(ERROR).getAsString();
574     }
575     else {
576       if (o.has(HTTP_STATUS)) {
577         code = o.get(HTTP_STATUS).getAsJsonPrimitive().getAsString();
578       }
579     }
580     if (o.has(MESSAGE)) {
581       if (!o.get(MESSAGE).isJsonNull()) {
582         message = o.get(MESSAGE).getAsString();
583       }
584     }
585     else if (o.has(DETAIL)) {
586       message = o.get(DETAIL).getAsString();
587     }
588     if (o.has(PATH)) {
589       if (message == null) {
590         message = o.get(PATH).getAsString();
591       }
592       else {
593         message = message + " " + o.get(PATH).getAsString();
594       }
595     }
596     return new ExternalTestingException(code, statusCode, message);
597   }
598
599   /**
600    * Resolve variables in the request calling the built-in variable resolvers.
601    * @param item test execution request item to be resolved.
602    */
603   private void runVariableResolvers(final VtpTestExecutionRequest item, final MultiValueMap<String, Object> body) {
604     variableResolvers.forEach(vr -> {
605       if (vr.resolvesVariablesForRequest(item)) {
606         vr.resolve(item, body);
607       }
608     });
609   }
610 }