2 * Copyright © 2019 iconectiv
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package org.openecomp.core.externaltesting.impl;
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;
42 import javax.annotation.PostConstruct;
43 import java.io.FileInputStream;
44 import java.io.IOException;
45 import java.io.InputStream;
47 import java.util.stream.Collectors;
49 public class ExternalTestingManagerImpl implements ExternalTestingManager {
51 private Logger logger = LoggerFactory.getLogger(ExternalTestingManagerImpl.class);
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";
60 private static final String CONFIG_FILE_PROPERTY = "configuration.yaml";
61 private static final String CONFIG_SECTION = "externalTestingConfig";
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";
70 private static final String INVALIDATE_STATE_ERROR = "Invalid State";
71 private static final String NO_ACCESS_CONFIGURATION_DEFINED = "No access configuration defined";
73 private TestingAccessConfig accessConfig;
74 private Map<String, RemoteTestingEndpointDefinition> endpoints = new HashMap<>();
76 private RestTemplate restTemplate;
78 private List<VariableResolver> variableResolvers;
80 public ExternalTestingManagerImpl(@Autowired(required=false) List<VariableResolver> variableResolvers) {
81 this.variableResolvers = variableResolvers;
82 // nothing to do at the moment.
83 restTemplate = new RestTemplate();
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...
91 public void loadConfig() {
93 String file = Objects.requireNonNull(System.getProperty(CONFIG_FILE_PROPERTY),
94 "Config file location must be specified via system property " + CONFIG_FILE_PROPERTY);
96 Object rawConfig = getExternalTestingAccessConfiguration(file);
97 if (rawConfig != null) {
98 accessConfig = new ObjectMapper().convertValue(rawConfig, TestingAccessConfig.class);
99 accessConfig.getEndpoints()
101 .filter(RemoteTestingEndpointDefinition::isEnabled)
102 .forEach(e -> endpoints.put(e.getId(), e));
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);
111 * Return the configuration of this feature that we want to
112 * expose to the client. Treated as a JSON blob for flexibility.
115 public String getConfig() {
116 ClientConfiguration cc = null;
117 if (accessConfig != null) {
118 cc = accessConfig.getClient();
121 cc = new ClientConfiguration();
122 cc.setEnabled(false);
125 return new ObjectMapper().writeValueAsString(cc);
126 } catch (JsonProcessingException e) {
127 logger.error("failed to write client config", e);
128 return "{\"enabled\":false}";
133 public TestTreeNode getTestCasesAsTree() {
134 TestTreeNode root = new TestTreeNode("root", "root");
136 // quick out in case of non-configured SDC
137 if (accessConfig == null) {
141 for (RemoteTestingEndpointDefinition ep : accessConfig.getEndpoints()) {
142 if (ep.isEnabled()) {
143 buildTreeFromEndpoint(ep, root);
149 private void buildTreeFromEndpoint(RemoteTestingEndpointDefinition ep, TestTreeNode root) {
151 logger.debug("process endpoint {}", ep.getId());
152 getScenarios(ep.getId()).stream().filter(s ->
153 ((ep.getScenarioFilter() == null) || ep.getScenarioFilterPattern().matcher(s.getName()).matches()))
155 addScenarioToTree(root, s);
156 getTestSuites(ep.getId(), s.getName()).forEach(suite -> addSuiteToTree(root, s, suite));
157 getTestCases(ep.getId(), s.getName()).forEach(tc -> {
159 VtpTestCase details = getTestCase(ep.getId(), s.getName(), tc.getTestSuiteName(), tc.getTestCaseName());
160 addTestCaseToTree(root, ep.getId(), s.getName(), tc.getTestSuiteName(), details);
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());
169 catch (ExternalTestingException ex) {
170 logger.error("unable to contact testing endpoint {}", ep.getId(), ex);
174 private Optional<TestTreeNode> findNamedChild(TestTreeNode root, String name) {
175 if (root.getChildren() == null) {
176 return Optional.empty();
178 return root.getChildren().stream().filter(n->n.getName().equals(name)).findFirst();
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.
189 private void addTestCaseToTree(TestTreeNode root, String endpointName, String scenarioName, String testSuiteName, VtpTestCase tc) {
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<>());
201 suiteNode.getTests().add(tc);
205 private void massageTestCaseForUI(VtpTestCase testcase, String endpoint, String scenario) {
206 testcase.setEndpoint(endpoint);
208 if (testcase.getScenario() == null) {
209 testcase.setScenario(scenario);
212 // if no inputs, return.
213 if (testcase.getInputs() == null) {
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()
221 .filter(input -> (input.getMetadata() == null) ||
222 (!input.getMetadata().containsKey("internal")) ||
223 !"true".equals(input.getMetadata().get("internal").toString())).collect(Collectors.toList()));
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.
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<>());
237 if (parent.getChildren().stream().noneMatch(n -> StringUtils.equals(n.getName(), suite.getName()))) {
238 parent.getChildren().add(new TestTreeNode(suite.getName(), suite.getDescription()));
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.
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<>());
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()));
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.
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());
271 return new ArrayList<>();
276 * Get the list of scenarios at a given endpoint.
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);
283 rv = new ArrayList<>();
289 * Get the list of test suites for an endpoint for the given scenario.
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);
296 rv = new ArrayList<>();
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.
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);
311 rv = new ArrayList<>();
317 * Get a test case definition.
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);
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.
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);
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.
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);
350 RemoteTestingEndpointDefinition endpoint = accessConfig.getEndpoints().stream()
351 .filter(e -> StringUtils.equals(endpointName, e.getId()))
353 .orElseThrow(() -> new ExternalTestingException("No such endpoint", 500, "No endpoint named " + endpointName + " is defined"));
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());
360 headers.setContentType(MediaType.MULTIPART_FORM_DATA);
363 MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
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 -> {
369 t.setProfile(t.getScenario()); // VTP wants a profile. Use the scenario name.
372 body.add("executions", new ObjectMapper().writeValueAsString(testsToRun));
374 catch (Exception ex) {
375 logger.error("exception converting tests to string", ex);
376 VtpTestExecutionResponse err = new VtpTestExecutionResponse();
377 err.setHttpStatus(500);
379 err.setMessage("Execution failed due to " + ex.getMessage());
380 return Collections.singletonList(err);
383 for(VtpTestExecutionRequest test: testsToRun) {
384 runVariableResolvers(test, body);
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);
395 ParameterizedTypeReference<List<VtpTestExecutionResponse>> t = new ParameterizedTypeReference<List<VtpTestExecutionResponse>>() {};
397 return proxyRequestToExternalTestingSite(builder.toUriString(), requestEntity, t);
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);
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.
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);
420 // partition the requests by endpoint.
421 Map<String, List<VtpTestExecutionRequest>> partitions =
422 testsToRun.stream().collect(Collectors.groupingBy(VtpTestExecutionRequest::getEndpoint));
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());
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.
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.");
443 return testingConfig;
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.
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);
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.
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))
472 .orElseThrow(() -> new ExternalTestingException("No such endpoint", 500, "No endpoint named " + endpointName + " is defined")
475 Object[] newArgs = ArrayUtils.add(args, 0, ep.getUrl());
476 return String.format(format, newArgs);
478 throw new ExternalTestingException(INVALIDATE_STATE_ERROR, 500, NO_ACCESS_CONFIGURATION_DEFINED);
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.
488 private <T> T proxyGetRequestToExternalTestingSite(String url, ParameterizedTypeReference<T> responseType) {
489 return proxyRequestToExternalTestingSite(url, null, responseType);
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
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());
505 logger.debug("GET request to {} for {}", url, responseType.getType().getTypeName());
507 SimpleClientHttpRequestFactory rf =
508 (SimpleClientHttpRequestFactory) restTemplate.getRequestFactory();
510 rf.setReadTimeout(10000);
511 rf.setConnectTimeout(10000);
513 ResponseEntity<T> re;
515 if (request != null) {
516 re = restTemplate.exchange(url, HttpMethod.POST, request, responseType);
518 re = restTemplate.exchange(url, HttpMethod.GET, null, responseType);
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);
530 JsonObject o = new GsonBuilder().create().fromJson(s, JsonObject.class);
531 throw buildTestingException(ex.getRawStatusCode(), o);
533 catch (JsonParseException e) {
534 logger.warn("unexpected JSON response", e);
535 throw new ExternalTestingException(ex.getStatusText(), ex.getStatusCode().value(), ex.getResponseBodyAsString(), ex);
539 throw new ExternalTestingException(ex.getStatusText(), ex.getStatusCode().value(), ex.getResponseBodyAsString(), ex);
542 catch (ResourceAccessException ex) {
543 throw new ExternalTestingException("IO Error at Endpoint", 500, ex.getMessage(), ex);
545 catch (Exception ex) {
546 throw new ExternalTestingException(ex.getMessage(), 500, "Generic Exception", ex);
549 logger.debug("http status of {} from external testing entity {}", re.getStatusCodeValue(), url);
553 logger.error("null response from endpoint");
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
565 private ExternalTestingException buildTestingException(int statusCode, JsonObject o) {
567 String message = null;
570 code = o.get(CODE).getAsString();
572 else if (o.has(ERROR)) {
573 code = o.get(ERROR).getAsString();
576 if (o.has(HTTP_STATUS)) {
577 code = o.get(HTTP_STATUS).getAsJsonPrimitive().getAsString();
580 if (o.has(MESSAGE)) {
581 if (!o.get(MESSAGE).isJsonNull()) {
582 message = o.get(MESSAGE).getAsString();
585 else if (o.has(DETAIL)) {
586 message = o.get(DETAIL).getAsString();
589 if (message == null) {
590 message = o.get(PATH).getAsString();
593 message = message + " " + o.get(PATH).getAsString();
596 return new ExternalTestingException(code, statusCode, message);
600 * Resolve variables in the request calling the built-in variable resolvers.
601 * @param item test execution request item to be resolved.
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);