Added null check in ExternalTestingManagerImpl
[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
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 java.io.File;
25 import java.io.FileInputStream;
26 import java.io.IOException;
27 import java.io.InputStream;
28 import java.util.ArrayList;
29 import java.util.Collections;
30 import java.util.List;
31 import java.util.Map;
32 import java.util.Optional;
33 import java.util.UUID;
34 import java.util.stream.Collectors;
35 import java.util.stream.Stream;
36 import javax.annotation.PostConstruct;
37 import lombok.EqualsAndHashCode;
38 import org.apache.commons.lang3.ArrayUtils;
39 import org.apache.commons.lang3.StringUtils;
40 import org.apache.commons.lang3.tuple.Pair;
41 import org.onap.sdc.tosca.services.YamlUtil;
42 import org.openecomp.core.externaltesting.api.ClientConfiguration;
43 import org.openecomp.core.externaltesting.api.ExternalTestingManager;
44 import org.openecomp.core.externaltesting.api.RemoteTestingEndpointDefinition;
45 import org.openecomp.core.externaltesting.api.TestTreeNode;
46 import org.openecomp.core.externaltesting.api.VtpNameDescriptionPair;
47 import org.openecomp.core.externaltesting.api.VtpTestCase;
48 import org.openecomp.core.externaltesting.api.VtpTestExecutionOutput;
49 import org.openecomp.core.externaltesting.api.VtpTestExecutionRequest;
50 import org.openecomp.core.externaltesting.api.VtpTestExecutionResponse;
51 import org.openecomp.core.externaltesting.errors.ExternalTestingException;
52 import org.openecomp.sdc.vendorsoftwareproduct.OrchestrationTemplateCandidateManager;
53 import org.openecomp.sdc.vendorsoftwareproduct.OrchestrationTemplateCandidateManagerFactory;
54 import org.openecomp.sdc.vendorsoftwareproduct.VendorSoftwareProductManager;
55 import org.openecomp.sdc.vendorsoftwareproduct.VspManagerFactory;
56 import org.openecomp.sdc.versioning.VersioningManager;
57 import org.openecomp.sdc.versioning.VersioningManagerFactory;
58 import org.openecomp.sdc.versioning.dao.types.Version;
59 import org.slf4j.Logger;
60 import org.slf4j.LoggerFactory;
61 import org.springframework.core.ParameterizedTypeReference;
62 import org.springframework.core.io.ByteArrayResource;
63 import org.springframework.http.HttpEntity;
64 import org.springframework.http.HttpHeaders;
65 import org.springframework.http.HttpMethod;
66 import org.springframework.http.MediaType;
67 import org.springframework.http.ResponseEntity;
68 import org.springframework.http.client.SimpleClientHttpRequestFactory;
69 import org.springframework.util.LinkedMultiValueMap;
70 import org.springframework.util.MultiValueMap;
71 import org.springframework.web.client.HttpStatusCodeException;
72 import org.springframework.web.client.ResourceAccessException;
73 import org.springframework.web.client.RestTemplate;
74 import org.springframework.web.util.UriComponentsBuilder;
75
76 public class ExternalTestingManagerImpl implements ExternalTestingManager {
77
78     static final String VSP_ID = "vspId";
79     static final String VSP_VERSION = "vspVersion";
80     private static final String FILE_URL_PREFIX = "file://";
81     private static final String HTTP_STATUS = "httpStatus";
82     private static final String CODE = "code";
83     private static final String ERROR = "error";
84     private static final String MESSAGE = "message";
85     private static final String DETAIL = "detail";
86     private static final String PATH = "path";
87     private static final String VTP_SCENARIOS_URI = "%s/v1/vtp/scenarios";
88     private static final String VTP_TESTSUITE_URI = "%s/v1/vtp/scenarios/%s/testsuites";
89     private static final String VTP_TESTCASES_URI = "%s/v1/vtp/scenarios/%s/testcases";
90     private static final String VTP_TESTCASE_URI = "%s/v1/vtp/scenarios/%s/testsuites/%s/testcases/%s";
91     private static final String VTP_EXECUTIONS_URI = "%s/v1/vtp/executions";
92     private static final String VTP_EXECUTION_URI = "%s/v1/vtp/executions/%s";
93     private static final String VTP_EXECUTION_ID_URL = "%s/v1/vtp/executions?requestId=%s";
94     private static final String INVALIDATE_STATE_ERROR_CODE = "SDC-TEST-001";
95     private static final String NO_ACCESS_CONFIGURATION_DEFINED = "No access configuration defined";
96     private static final String NO_SUCH_ENDPOINT_ERROR_CODE = "SDC-TEST-002";
97     private static final String ENDPOINT_ERROR_CODE = "SDC-TEST-003";
98     private static final String TESTING_HTTP_ERROR_CODE = "SDC-TEST-004";
99     private static final String SDC_RESOLVER_ERR = "SDC-TEST-005";
100     private static final String VSP_CSAR = "vsp";
101     private static final String VSP_HEAT = "vsp-zip";
102     private Logger logger = LoggerFactory.getLogger(ExternalTestingManagerImpl.class);
103     private VersioningManager versioningManager;
104     private VendorSoftwareProductManager vendorSoftwareProductManager;
105     private OrchestrationTemplateCandidateManager candidateManager;
106
107     private TestingAccessConfig accessConfig;
108     private List<RemoteTestingEndpointDefinition> endpoints;
109
110     private RestTemplate restTemplate;
111
112     public ExternalTestingManagerImpl() {
113         restTemplate = new RestTemplate();
114     }
115
116     ExternalTestingManagerImpl(VersioningManager versioningManager,
117                                VendorSoftwareProductManager vendorSoftwareProductManager,
118                                OrchestrationTemplateCandidateManager candidateManager) {
119         this();
120         this.versioningManager = versioningManager;
121         this.vendorSoftwareProductManager = vendorSoftwareProductManager;
122         this.candidateManager = candidateManager;
123     }
124
125     /**
126      * Read the configuration from the yaml file for this bean.  If we get an exception during load, don't force an error starting SDC but log a
127      * warning.  Do no warm...
128      */
129     @PostConstruct
130     public void init() {
131
132         if (versioningManager == null) {
133             versioningManager = VersioningManagerFactory.getInstance().createInterface();
134         }
135         if (vendorSoftwareProductManager == null) {
136             vendorSoftwareProductManager = VspManagerFactory.getInstance().createInterface();
137         }
138         if (candidateManager == null) {
139             candidateManager = OrchestrationTemplateCandidateManagerFactory.getInstance().createInterface();
140         }
141
142         loadConfig();
143     }
144
145     private Stream<RemoteTestingEndpointDefinition> mapEndpointString(String ep) {
146         RemoteTestingEndpointDefinition rv = new RemoteTestingEndpointDefinition();
147         String[] cfg = ep.split(",");
148         if (cfg.length < 4) {
149             logger.error("invalid endpoint definition {}", ep);
150             return Stream.empty();
151         } else {
152             rv.setId(cfg[0]);
153             rv.setTitle(cfg[1]);
154             rv.setEnabled("true".equals(cfg[2]));
155             rv.setUrl(cfg[3]);
156             if (cfg.length > 4) {
157                 rv.setScenarioFilter(cfg[4]);
158             }
159             if (cfg.length > 5) {
160                 rv.setApiKey(cfg[5]);
161             }
162             return Stream.of(rv);
163         }
164     }
165
166     /**
167      * Load the configuration for this component.  When the SDC onboarding backend runs, it gets a system property called config.location.  We can use
168      * that to locate the config-externaltesting.yaml file.
169      */
170     private void loadConfig() {
171         String loc = System.getProperty("config.location");
172         File file = new File(loc, "externaltesting-configuration.yaml");
173         try (InputStream fileInput = new FileInputStream(file)) {
174             YamlUtil yamlUtil = new YamlUtil();
175             accessConfig = yamlUtil.yamlToObject(fileInput, TestingAccessConfig.class);
176
177             if (logger.isInfoEnabled()) {
178                 String s = new ObjectMapper().writeValueAsString(accessConfig);
179                 logger.info("loaded external testing config {}", s);
180             }
181
182             endpoints =
183                 accessConfig.getEndpoints().stream().flatMap(this::mapEndpointString).collect(Collectors.toList());
184
185             if (logger.isInfoEnabled()) {
186                 String s = new ObjectMapper().writeValueAsString(endpoints);
187                 logger.info("processed external testing config {}", s);
188             }
189         } catch (IOException ex) {
190             logger.error("failed to read external testing config.  Disabling the feature", ex);
191             accessConfig = new TestingAccessConfig();
192             accessConfig.setEndpoints(new ArrayList<>());
193             accessConfig.setClient(new ClientConfiguration());
194             accessConfig.getClient().setEnabled(false);
195             endpoints = new ArrayList<>();
196         }
197     }
198
199     /**
200      * Return the configuration of this feature that we want to expose to the client.  Treated as a JSON blob for flexibility.
201      */
202     @Override
203     public ClientConfiguration getConfig() {
204         ClientConfiguration cc = null;
205         if (accessConfig != null) {
206             cc = accessConfig.getClient();
207         }
208         if (cc == null) {
209             cc = new ClientConfiguration();
210             cc.setEnabled(false);
211         }
212         return cc;
213     }
214
215     /**
216      * To allow for functional testing, we let a caller invoke a setConfig request to enable/disable the client.  This new value is not persisted.
217      *
218      * @return new client configuration
219      */
220     @Override
221     public ClientConfiguration setConfig(ClientConfiguration cc) {
222         if (accessConfig == null) {
223             accessConfig = new TestingAccessConfig();
224         }
225         accessConfig.setClient(cc);
226         return getConfig();
227     }
228
229     /**
230      * To allow for functional testing, we let a caller invoke a setEndpoints request to configure where the BE makes request to.
231      *
232      * @return new endpoint definitions.
233      */
234     @Override
235     public List<RemoteTestingEndpointDefinition> setEndpoints(List<RemoteTestingEndpointDefinition> endpoints) {
236         this.endpoints = endpoints;
237         return this.getEndpoints();
238     }
239
240
241     @Override
242     public TestTreeNode getTestCasesAsTree() {
243         TestTreeNode root = new TestTreeNode("root", "root");
244
245         // quick out in case of non-configured SDC
246         if (endpoints == null) {
247             return root;
248         }
249
250         for (RemoteTestingEndpointDefinition ep : endpoints) {
251             if (ep.isEnabled()) {
252                 buildTreeFromEndpoint(ep, root);
253             }
254         }
255         return root;
256     }
257
258     private void buildTreeFromEndpoint(RemoteTestingEndpointDefinition ep, TestTreeNode root) {
259         try {
260             logger.debug("process endpoint {}", ep.getId());
261             getScenarios(ep.getId()).stream()
262                 .filter(s -> ((ep.getScenarioFilter() == null) || ep.getScenarioFilterPattern().matcher(s.getName())
263                     .matches())).forEach(s -> {
264                 addScenarioToTree(root, s);
265                 getTestSuites(ep.getId(), s.getName()).forEach(suite -> addSuiteToTree(root, s, suite));
266                 getTestCases(ep.getId(), s.getName()).forEach(tc -> {
267                     try {
268                         VtpTestCase details =
269                             getTestCase(ep.getId(), s.getName(), tc.getTestSuiteName(), tc.getTestCaseName());
270                         addTestCaseToTree(root, ep.getId(), s.getName(), tc.getTestSuiteName(), details);
271                     } catch (@SuppressWarnings("squid:S1166") ExternalTestingException ex) {
272                         // Not logging stack trace on purpose.  VTP was throwing exceptions for certain test cases.
273                         logger.warn("failed to load test case {}", tc.getTestCaseName());
274                     }
275                 });
276             });
277         } catch (ExternalTestingException ex) {
278             logger.error("unable to contact testing endpoint {}", ep.getId(), ex);
279         }
280     }
281
282     private Optional<TestTreeNode> findNamedChild(TestTreeNode root, String name) {
283         if (root.getChildren() == null) {
284             return Optional.empty();
285         }
286         return root.getChildren().stream().filter(n -> n.getName().equals(name)).findFirst();
287     }
288
289     /**
290      * Find the place in the tree to add the test case.
291      *
292      * @param root          root of the tree.
293      * @param endpointName  name of the endpoint to assign to the test case.
294      * @param scenarioName  scenario to add this case to
295      * @param testSuiteName suite in the scenario to add this case to
296      * @param tc            test case to add.
297      */
298     private void addTestCaseToTree(TestTreeNode root, String endpointName, String scenarioName, String testSuiteName,
299                                    VtpTestCase tc) {
300         // return quickly.
301         if (tc == null) {
302             return;
303         }
304         findNamedChild(root, scenarioName)
305             .ifPresent(scenarioNode -> findNamedChild(scenarioNode, testSuiteName).ifPresent(suiteNode -> {
306                 massageTestCaseForUI(tc, endpointName, scenarioName);
307                 if (suiteNode.getTests() == null) {
308                     suiteNode.setTests(new ArrayList<>());
309                 }
310                 suiteNode.getTests().add(tc);
311             }));
312     }
313
314     private void massageTestCaseForUI(VtpTestCase testcase, String endpoint, String scenario) {
315         testcase.setEndpoint(endpoint);
316         // VTP workaround.
317         if (testcase.getScenario() == null) {
318             testcase.setScenario(scenario);
319         }
320     }
321
322     /**
323      * Add the test suite to the tree at the appropriate place if it does not already exist in the tree.
324      *
325      * @param root     root of the tree.
326      * @param scenario scenario under which this suite should be placed
327      * @param suite    test suite to add.
328      */
329     private void addSuiteToTree(final TestTreeNode root, final VtpNameDescriptionPair scenario,
330                                 final VtpNameDescriptionPair suite) {
331         findNamedChild(root, scenario.getName()).ifPresent(parent -> {
332             if (parent.getChildren() == null) {
333                 parent.setChildren(new ArrayList<>());
334             }
335             if (parent.getChildren().stream().noneMatch(n -> StringUtils.equals(n.getName(), suite.getName()))) {
336                 parent.getChildren().add(new TestTreeNode(suite.getName(), suite.getDescription()));
337             }
338         });
339     }
340
341     /**
342      * Add the scenario to the tree if it does not already exist.
343      *
344      * @param root root of the tree.
345      * @param s    scenario to add.
346      */
347     private void addScenarioToTree(TestTreeNode root, VtpNameDescriptionPair s) {
348         logger.debug("addScenario {} to {} with {}", s.getName(), root.getName(), root.getChildren());
349         if (root.getChildren() == null) {
350             root.setChildren(new ArrayList<>());
351         }
352         if (root.getChildren().stream().noneMatch(n -> StringUtils.equals(n.getName(), s.getName()))) {
353             logger.debug("createScenario {} in {}", s.getName(), root.getName());
354             root.getChildren().add(new TestTreeNode(s.getName(), s.getDescription()));
355         }
356     }
357
358     /**
359      * Get the list of endpoints defined to the testing manager.
360      *
361      * @return list of endpoints or empty list if the manager is not configured.
362      */
363     public List<RemoteTestingEndpointDefinition> getEndpoints() {
364         if (endpoints != null) {
365             return endpoints.stream().filter(RemoteTestingEndpointDefinition::isEnabled).collect(Collectors.toList());
366         } else {
367             return new ArrayList<>();
368         }
369     }
370
371     /**
372      * Code shared by getScenarios and getTestSuites.
373      */
374     private List<VtpNameDescriptionPair> returnNameDescriptionPairFromUrl(String url) {
375         ParameterizedTypeReference<List<VtpNameDescriptionPair>> t =
376             new ParameterizedTypeReference<List<VtpNameDescriptionPair>>() {
377             };
378         List<VtpNameDescriptionPair> rv = proxyGetRequestToExternalTestingSite(url, t);
379         if (rv == null) {
380             rv = new ArrayList<>();
381         }
382         return rv;
383     }
384
385     /**
386      * Get the list of scenarios at a given endpoint.
387      */
388     public List<VtpNameDescriptionPair> getScenarios(final String endpoint) {
389         String url = buildEndpointUrl(VTP_SCENARIOS_URI, endpoint, ArrayUtils.EMPTY_STRING_ARRAY);
390         return returnNameDescriptionPairFromUrl(url);
391     }
392
393     /**
394      * Get the list of test suites for an endpoint for the given scenario.
395      */
396     public List<VtpNameDescriptionPair> getTestSuites(final String endpoint, final String scenario) {
397         String url = buildEndpointUrl(VTP_TESTSUITE_URI, endpoint, new String[]{scenario});
398         return returnNameDescriptionPairFromUrl(url);
399     }
400
401     /**
402      * Get the list of test cases under a scenario.  This is the VTP API.  It would seem better to get the list of cases under a test suite but that
403      * is not supported.
404      */
405     @Override
406     public List<VtpTestCase> getTestCases(String endpoint, String scenario) {
407         String url = buildEndpointUrl(VTP_TESTCASES_URI, endpoint, new String[]{scenario});
408         ParameterizedTypeReference<List<VtpTestCase>> t = new ParameterizedTypeReference<List<VtpTestCase>>() {
409         };
410         List<VtpTestCase> rv = proxyGetRequestToExternalTestingSite(url, t);
411         if (rv == null) {
412             rv = new ArrayList<>();
413         }
414         return rv;
415     }
416
417     /**
418      * Get a test case definition.
419      */
420     @Override
421     public VtpTestCase getTestCase(String endpoint, String scenario, String testSuite, String testCaseName) {
422         String url = buildEndpointUrl(VTP_TESTCASE_URI, endpoint, new String[]{scenario, testSuite, testCaseName});
423         ParameterizedTypeReference<VtpTestCase> t = new ParameterizedTypeReference<VtpTestCase>() {
424         };
425         return proxyGetRequestToExternalTestingSite(url, t);
426     }
427
428     /**
429      * Return the results of a previous test execution.
430      *
431      * @param endpoint    endpoint to query
432      * @param executionId execution to query.
433      * @return execution response from testing endpoint.
434      */
435     @Override
436     public VtpTestExecutionResponse getExecution(String endpoint, String executionId) {
437         String url = buildEndpointUrl(VTP_EXECUTION_URI, endpoint, new String[]{executionId});
438         ParameterizedTypeReference<VtpTestExecutionResponse> t =
439             new ParameterizedTypeReference<VtpTestExecutionResponse>() {
440             };
441         return proxyGetRequestToExternalTestingSite(url, t);
442     }
443
444
445     /**
446      * Execute tests splitting them across endpoints and collecting the results.
447      *
448      * @param testsToRun list of tests to be executed.
449      * @return collection of result objects.
450      */
451
452     @Override
453     public List<VtpTestExecutionResponse> execute(final List<VtpTestExecutionRequest> testsToRun, String vspId,
454                                                   String vspVersionId, String requestId, Map<String, byte[]> fileMap) {
455         if (endpoints == null) {
456             throw new ExternalTestingException(INVALIDATE_STATE_ERROR_CODE, 500, NO_ACCESS_CONFIGURATION_DEFINED);
457         }
458
459         // partition the requests by endpoint.
460         Map<String, List<VtpTestExecutionRequest>> partitions =
461             testsToRun.stream().collect(Collectors.groupingBy(VtpTestExecutionRequest::getEndpoint));
462
463         // process each group and collect the results.
464         return partitions.entrySet().stream().flatMap(
465             e -> doExecute(e.getKey(), e.getValue(), vspId, vspVersionId, requestId, fileMap).stream())
466             .collect(Collectors.toList());
467     }
468
469     /**
470      * Get the list of Execution by requestId.
471      */
472     @Override
473     public List<VtpTestExecutionOutput> getExecutionIds(String endpoint, String requestId) {
474         String url = buildEndpointUrl(VTP_EXECUTION_ID_URL, endpoint, new String[]{requestId});
475         ParameterizedTypeReference<List<VtpTestExecutionOutput>> t =
476             new ParameterizedTypeReference<List<VtpTestExecutionOutput>>() {
477             };
478         List<VtpTestExecutionOutput> rv = proxyGetRequestToExternalTestingSite(url, t);
479         if (rv == null) {
480             rv = new ArrayList<>();
481         }
482         return rv;
483     }
484
485     /**
486      * Execute a set of tests at a given endpoint.
487      *
488      * @param endpointName name of the endpoint
489      * @param testsToRun   set of tests to run
490      * @return list of execution responses.
491      */
492     private List<VtpTestExecutionResponse> doExecute(final String endpointName,
493                                                      final List<VtpTestExecutionRequest> testsToRun, String vspId, String vspVersionId,
494                                                      String requestId,
495                                                      Map<String, byte[]> fileMap) {
496         if (endpoints == null) {
497             throw new ExternalTestingException(INVALIDATE_STATE_ERROR_CODE, 500, NO_ACCESS_CONFIGURATION_DEFINED);
498         }
499
500         RemoteTestingEndpointDefinition endpoint =
501             endpoints.stream().filter(e -> StringUtils.equals(endpointName, e.getId())).findFirst().orElseThrow(
502                 () -> new ExternalTestingException(NO_SUCH_ENDPOINT_ERROR_CODE, 400,
503                     "No endpoint named " + endpointName + " is defined"));
504
505         // if the endpoint requires an API key, specify it in the headers.
506         HttpHeaders headers = new HttpHeaders();
507         if (endpoint.getApiKey() != null) {
508             headers.add("X-API-Key", endpoint.getApiKey());
509         }
510         headers.setContentType(MediaType.MULTIPART_FORM_DATA);
511
512         // build the body.
513         MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
514
515         for (VtpTestExecutionRequest test : testsToRun) {
516             // it will true only noramal validation not for certification
517             if ((test.getParameters() != null) && (test.getParameters().containsKey(VSP_CSAR) || test.getParameters()
518                 .containsKey(
519                     VSP_HEAT))) {
520                 attachArchiveContent(test, body, vspId, vspVersionId);
521             }
522
523         }
524
525         attachFileContentInTest(body, fileMap);
526         try {
527             // remove the endpoint from the test request since that is a FE/BE attribute
528             testsToRun.forEach(t -> t.setEndpoint(null));
529             String strExecution = new ObjectMapper().writeValueAsString(testsToRun);
530             body.add("executions", strExecution);
531
532         } catch (IOException ex) {
533             logger.error("exception converting tests to string", ex);
534             VtpTestExecutionResponse err = new VtpTestExecutionResponse();
535             err.setHttpStatus(500);
536             err.setCode(TESTING_HTTP_ERROR_CODE);
537             err.setMessage("Execution failed due to " + ex.getMessage());
538             return Collections.singletonList(err);
539         }
540
541         // form and send request.
542         HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
543         String url = buildEndpointUrl(VTP_EXECUTIONS_URI, endpointName, ArrayUtils.EMPTY_STRING_ARRAY);
544         UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url);
545         if (requestId != null) {
546             builder = builder.queryParam("requestId", requestId);
547         }
548         ParameterizedTypeReference<List<VtpTestExecutionResponse>> t =
549             new ParameterizedTypeReference<List<VtpTestExecutionResponse>>() {
550             };
551         try {
552             return proxyRequestToExternalTestingSite(builder.toUriString(), requestEntity, t);
553         } catch (ExternalTestingException ex) {
554             logger.info("exception caught invoking endpoint {}", endpointName, ex);
555             if (ex.getHttpStatus() == 504) {
556                 return Collections.singletonList(new VtpTestExecutionResponse());
557             }
558             VtpTestExecutionResponse err = new VtpTestExecutionResponse();
559             err.setHttpStatus(ex.getHttpStatus());
560             err.setCode(TESTING_HTTP_ERROR_CODE);
561             err.setMessage(ex.getMessageCode() + ": " + ex.getDetail());
562             return Collections.singletonList(err);
563         }
564     }
565
566     /**
567      * Return URL with endpoint url as prefix.
568      *
569      * @param format       format string.
570      * @param endpointName endpoint to address
571      * @param args         args for format.
572      * @return qualified url.
573      */
574     private String buildEndpointUrl(String format, String endpointName, String[] args) {
575         if (endpoints != null) {
576             RemoteTestingEndpointDefinition ep =
577                 endpoints.stream().filter(e -> e.isEnabled() && e.getId().equals(endpointName)).findFirst()
578                     .orElseThrow(() -> new ExternalTestingException(NO_SUCH_ENDPOINT_ERROR_CODE, 500,
579                         "No endpoint named " + endpointName + " is defined"));
580
581             Object[] newArgs = ArrayUtils.add(args, 0, ep.getUrl());
582             return String.format(format, newArgs);
583         }
584         throw new ExternalTestingException(INVALIDATE_STATE_ERROR_CODE, 500, NO_ACCESS_CONFIGURATION_DEFINED);
585     }
586
587     /**
588      * Proxy a get request to a testing endpoint.
589      *
590      * @param url          URL to invoke.
591      * @param responseType type of response expected.
592      * @param <T>          type of response expected
593      * @return instance of <T> parsed from the JSON response from endpoint.
594      */
595     private <T> T proxyGetRequestToExternalTestingSite(String url, ParameterizedTypeReference<T> responseType) {
596         return proxyRequestToExternalTestingSite(url, null, responseType);
597     }
598
599     /**
600      * Make the actual HTTP post (using Spring RestTemplate) to an endpoint.
601      *
602      * @param url          URL to the endpoint
603      * @param request      optional request body to send
604      * @param responseType expected type
605      * @param <T>          extended type
606      * @return instance of expected type
607      */
608     private <R, T> T proxyRequestToExternalTestingSite(String url, HttpEntity<R> request,
609                                                        ParameterizedTypeReference<T> responseType) {
610         if (request != null) {
611             logger.debug("POST request to {} with {} for {}", url, request, responseType.getType().getTypeName());
612         } else {
613             logger.debug("GET request to {} for {}", url, responseType.getType().getTypeName());
614         }
615
616         SimpleClientHttpRequestFactory rf = (SimpleClientHttpRequestFactory) restTemplate.getRequestFactory();
617         ResponseEntity<T> re;
618
619         if (rf != null) {
620             rf.setReadTimeout(10000);
621             rf.setConnectTimeout(10000);
622         }
623
624         try {
625             if (request != null) {
626                 re = restTemplate.exchange(url, HttpMethod.POST, request, responseType);
627             } else {
628                 re = restTemplate.exchange(url, HttpMethod.GET, null, responseType);
629             }
630         } catch (HttpStatusCodeException ex) {
631             // make my own exception out of this.
632             HttpHeaders httpHeaders = ex.getResponseHeaders();
633             logger.warn("Unexpected HTTP Status from endpoint {}", ex.getRawStatusCode());
634
635             if (httpHeaders == null) {
636                 throw new ExternalTestingException(ENDPOINT_ERROR_CODE, 500, "Failed to get HTTP Response Headers", ex);
637             }
638
639             MediaType responseHeadersContentType = httpHeaders.getContentType();
640
641             if (responseHeadersContentType == null || (!responseHeadersContentType.isCompatibleWith(MediaType.APPLICATION_JSON)
642                 && !responseHeadersContentType.isCompatibleWith(MediaType.parseMediaType("application/problem+json")))) {
643                 throw new ExternalTestingException(ENDPOINT_ERROR_CODE, ex.getStatusCode().value(), ex.getResponseBodyAsString(), ex);
644             }
645
646             String s = ex.getResponseBodyAsString();
647             logger.warn("endpoint body content is {}", s);
648
649             try {
650                 JsonObject o = new GsonBuilder().create().fromJson(s, JsonObject.class);
651                 throw buildTestingException(ex.getRawStatusCode(), o);
652             } catch (JsonParseException e) {
653                 logger.warn("unexpected JSON response", e);
654                 throw new ExternalTestingException(ENDPOINT_ERROR_CODE, ex.getStatusCode().value(),
655                     ex.getResponseBodyAsString(), ex);
656             }
657         } catch (ResourceAccessException ex) {
658             throw new ExternalTestingException(ENDPOINT_ERROR_CODE, 500, ex.getMessage(), ex);
659         } catch (Exception ex) {
660             throw new ExternalTestingException(ENDPOINT_ERROR_CODE, 500, "Generic Exception " + ex.getMessage(), ex);
661         }
662
663         if (re != null) {
664             logger.debug("http status of {} from external testing entity {}", re.getStatusCodeValue(), url);
665             return re.getBody();
666         } else {
667             logger.error("null response from endpoint");
668             return null;
669         }
670     }
671
672     private void attachFileContentInTest(MultiValueMap<String, Object> body, Map<String, byte[]> fileMap) {
673         if (fileMap != null) {
674             fileMap.forEach((name, inputStream) -> body.add("file", new NamedByteArrayResource(inputStream, name)));
675         }
676
677     }
678
679     /**
680      * Errors from the endpoint could conform to the expected ETSI body or not. Here we try to handle various response body elements.
681      *
682      * @param statusCode http status code in response.
683      * @param o          JSON object parsed from the http response body
684      * @return Testing error body that should be returned to the caller
685      */
686     private ExternalTestingException buildTestingException(int statusCode, JsonObject o) {
687         String code = null;
688         String message = null;
689
690         if (o.has(CODE)) {
691             code = o.get(CODE).getAsString();
692         } else if (o.has(ERROR)) {
693             code = o.get(ERROR).getAsString();
694         } else {
695             if (o.has(HTTP_STATUS)) {
696                 code = o.get(HTTP_STATUS).getAsJsonPrimitive().getAsString();
697             }
698         }
699         if (o.has(MESSAGE)) {
700             if (!o.get(MESSAGE).isJsonNull()) {
701                 message = o.get(MESSAGE).getAsString();
702             }
703         } else if (o.has(DETAIL)) {
704             message = o.get(DETAIL).getAsString();
705         }
706         if (o.has(PATH)) {
707             if (message == null) {
708                 message = o.get(PATH).getAsString();
709             } else {
710                 message = message + " " + o.get(PATH).getAsString();
711             }
712         }
713         return new ExternalTestingException(code, statusCode, message);
714     }
715
716     void attachArchiveContent(VtpTestExecutionRequest test, MultiValueMap<String, Object> body, String vspId,
717                               String vspVersionId) {
718         try {
719             extractMetadata(test, body, vspId, vspVersionId);
720         } catch (IOException ex) {
721             logger.error("metadata extraction failed", ex);
722         }
723     }
724
725     /**
726      * Extract the metadata from the VSP CSAR file.
727      *
728      * @param requestItem item to add metadata to for processing
729      * @param vspId       VSP identifier
730      * @param version     VSP version
731      */
732     private void extractMetadata(VtpTestExecutionRequest requestItem, MultiValueMap<String, Object> body, String vspId,
733                                  String version) throws IOException {
734
735         Version ver = new Version(version);
736         logger.debug("attempt to retrieve archive for VSP {} version {}", vspId, ver.getId());
737
738         Optional<Pair<String, byte[]>> ozip = candidateManager.get(vspId, ver);
739         if (!ozip.isPresent()) {
740             ozip = vendorSoftwareProductManager.get(vspId, ver);
741         }
742
743         if (!ozip.isPresent()) {
744             List<Version> versions = versioningManager.list(vspId);
745             String knownVersions = versions.stream()
746                 .map(v -> String.format("%d.%d: %s (%s)", v.getMajor(), v.getMinor(),
747                     v.getStatus(), v.getId())).collect(Collectors.joining("\n"));
748
749             String detail = String.format(
750                 "Archive processing failed.  Unable to find archive for VSP ID %s and Version %s.  Known versions are:\n%s",
751                 vspId, version, knownVersions);
752
753             throw new ExternalTestingException(SDC_RESOLVER_ERR, 500, detail);
754         }
755
756         // safe here to do get.
757         Pair<String, byte[]> zip = ozip.get();
758         processArchive(requestItem, body, zip.getRight());
759     }
760
761     private void processArchive(final VtpTestExecutionRequest test, final MultiValueMap<String, Object> body,
762                                 final byte[] zip) {
763
764         // VTP does not support concurrent executions of the same test with the same associated file name.
765         // It writes files to /tmp and if we were to send two requests with the same file, the results
766         // are unpredictable.
767         String key = UUID.randomUUID().toString();
768         key = key.substring(0, key.indexOf('-'));
769
770         if (test.getParameters().containsKey(VSP_HEAT)) {
771             body.add("file", new NamedByteArrayResource(zip, key + ".heat.zip"));
772             test.getParameters().put(VSP_HEAT, FILE_URL_PREFIX + key + ".heat.zip");
773         } else {
774             body.add("file", new NamedByteArrayResource(zip, key + ".csar"));
775             test.getParameters().put(VSP_CSAR, FILE_URL_PREFIX + key + ".csar");
776         }
777     }
778
779     /**
780      * We need to name the byte array we add to the multipart request sent to the VTP.
781      */
782     @EqualsAndHashCode(callSuper = false)
783     protected class NamedByteArrayResource extends ByteArrayResource {
784
785         private String filename;
786
787         NamedByteArrayResource(byte[] bytes, String filename) {
788             super(bytes, filename);
789             this.filename = filename;
790         }
791
792         @Override
793         public String getFilename() {
794             return this.filename;
795         }
796     }
797
798
799 }