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