Merge "Implemented UAT runtime services"
authorBrinda Santh Muthuramalingam <brindasanth@in.ibm.com>
Thu, 24 Oct 2019 15:34:33 +0000 (15:34 +0000)
committerGerrit Code Review <gerrit@onap.org>
Thu, 24 Oct 2019 15:34:33 +0000 (15:34 +0000)
24 files changed:
ms/blueprintsprocessor/application/pom.xml
ms/blueprintsprocessor/application/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/JsonNormalizer.kt [moved from ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/JsonNormalizer.kt with 94% similarity]
ms/blueprintsprocessor/application/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/MoreMatchers.kt [moved from ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/MoreMatchers.kt with 96% similarity]
ms/blueprintsprocessor/application/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/PathDeserializer.kt [moved from ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/PathDeserializer.kt with 93% similarity]
ms/blueprintsprocessor/application/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/UatDefinition.kt [moved from ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/UatDefinition.kt with 55% similarity]
ms/blueprintsprocessor/application/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/UatExecutor.kt [new file with mode: 0644]
ms/blueprintsprocessor/application/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/UatServices.kt [new file with mode: 0644]
ms/blueprintsprocessor/application/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/logging/ColorMarker.kt [moved from ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/CollectionUtils2.kt with 71% similarity]
ms/blueprintsprocessor/application/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/logging/LogColor.kt [new file with mode: 0644]
ms/blueprintsprocessor/application/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/logging/MockInvocationLogger.kt [new file with mode: 0644]
ms/blueprintsprocessor/application/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/logging/SmartColorDiscriminator.kt [new file with mode: 0644]
ms/blueprintsprocessor/application/src/main/resources/application-local.yml [new file with mode: 0644]
ms/blueprintsprocessor/application/src/main/resources/application-uat.yml [new file with mode: 0644]
ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/BlueprintsAcceptanceTest.kt [deleted file]
ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/BaseUatTest.kt [new file with mode: 0644]
ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/BlueprintsAcceptanceTest.kt [new file with mode: 0644]
ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/ExtendedTemporaryFolder.kt [moved from ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ExtendedTemporaryFolder.kt with 77% similarity]
ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/MarkedSlf4jNotifier.kt [new file with mode: 0644]
ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/TestSecuritySettings.kt [moved from ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/TestSecuritySettings.kt with 92% similarity]
ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/UatServicesTest.kt [new file with mode: 0644]
ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/WorkingFoldersInitializer.kt [moved from ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/WorkingFoldersInitializer.kt with 97% similarity]
ms/blueprintsprocessor/application/src/test/resources/logback-test.xml
ms/blueprintsprocessor/modules/commons/rest-lib/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/rest/service/BluePrintRestLibPropertyService.kt
ms/controllerblueprints/modules/blueprint-core/src/main/kotlin/org/onap/ccsdk/cds/controllerblueprints/core/BluePrintConstants.kt

index ed1b67d..3a95e8c 100755 (executable)
@@ -24,7 +24,7 @@
         <groupId>org.onap.ccsdk.cds.blueprintsprocessor</groupId>
         <artifactId>parent</artifactId>
         <version>0.7.0-SNAPSHOT</version>
-        <relativePath>../parent</relativePath>
+        <relativePath>..</relativePath>
     </parent>
 
     <artifactId>application</artifactId>
             <artifactId>reactor-test</artifactId>
             <scope>test</scope>
         </dependency>
+        <!-- BEGIN UAT -->
+        <dependency>
+            <groupId>org.skyscreamer</groupId>
+            <artifactId>jsonassert</artifactId>
+        </dependency>
         <dependency>
             <groupId>org.yaml</groupId>
             <artifactId>snakeyaml</artifactId>
-            <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>com.nhaarman.mockitokotlin2</groupId>
             <artifactId>mockito-kotlin</artifactId>
-            <version>2.1.0</version>
-            <scope>test</scope>
+            <version>2.2.0</version>
         </dependency>
         <dependency>
             <groupId>com.schibsted.spt.data</groupId>
             <artifactId>jslt</artifactId>
             <version>0.1.8</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpclient</artifactId>
+            <version>${apache.httpcomponents.client.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpmime</artifactId>
+            <version>${apache.httpcomponents.client.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.github.tomakehurst</groupId>
+            <artifactId>wiremock-jre8</artifactId>
+            <version>2.25.0</version>
             <scope>test</scope>
         </dependency>
+        <!-- END UAT -->
         <dependency>
             <groupId>ch.qos.logback</groupId>
             <artifactId>logback-classic</artifactId>
  * SPDX-License-Identifier: Apache-2.0
  * ============LICENSE_END=========================================================
  */
-package org.onap.ccsdk.cds.blueprintsprocessor
+package org.onap.ccsdk.cds.blueprintsprocessor.uat
 
 import com.fasterxml.jackson.databind.JsonNode
 import com.fasterxml.jackson.databind.ObjectMapper
 import com.fasterxml.jackson.databind.node.ContainerNode
-import com.fasterxml.jackson.databind.node.MissingNode
 import com.fasterxml.jackson.databind.node.ObjectNode
 import com.schibsted.spt.data.jslt.Parser
 
-class JsonNormalizer {
+internal class JsonNormalizer {
 
     companion object {
 
-        fun getNormalizer(mapper: ObjectMapper, jsltSpec: JsonNode): (String) -> String {
-            if (jsltSpec is MissingNode) {
+        fun getNormalizer(mapper: ObjectMapper, jsltSpec: JsonNode?): (String) -> String {
+            if (jsltSpec == null) {
                 return { it }
             }
             return { s: String ->
  * SPDX-License-Identifier: Apache-2.0
  * ============LICENSE_END=========================================================
  */
-package org.onap.ccsdk.cds.blueprintsprocessor
+package org.onap.ccsdk.cds.blueprintsprocessor.uat
 
 import com.fasterxml.jackson.core.JsonParser
 import com.fasterxml.jackson.databind.DeserializationContext
 import com.fasterxml.jackson.databind.deser.std.StdDeserializer
 
-class PathDeserializer : StdDeserializer<String>(String::class.java) {
+internal class PathDeserializer : StdDeserializer<String>(String::class.java) {
     override fun deserialize(jp: JsonParser, ctxt: DeserializationContext?): String {
         val path = jp.codec.readValue(jp, Any::class.java)
         return flatJoin(path)
  * SPDX-License-Identifier: Apache-2.0
  * ============LICENSE_END=========================================================
  */
-package org.onap.ccsdk.cds.blueprintsprocessor
+package org.onap.ccsdk.cds.blueprintsprocessor.uat
 
 import com.fasterxml.jackson.annotation.JsonAlias
+import com.fasterxml.jackson.annotation.JsonInclude
 import com.fasterxml.jackson.databind.JsonNode
 import com.fasterxml.jackson.databind.ObjectMapper
 import com.fasterxml.jackson.databind.annotation.JsonDeserialize
-import com.fasterxml.jackson.databind.node.MissingNode
+import com.fasterxml.jackson.module.kotlin.convertValue
+import org.yaml.snakeyaml.DumperOptions
 import org.yaml.snakeyaml.Yaml
-import java.nio.file.Path
+import org.yaml.snakeyaml.nodes.Tag
 
-data class ProcessDefinition(val name: String, val request: JsonNode, val expectedResponse: JsonNode,
-                             val responseNormalizerSpec: JsonNode = MissingNode.getInstance())
+@JsonInclude(JsonInclude.Include.NON_EMPTY)
+data class ProcessDefinition(val name: String, val request: JsonNode, val expectedResponse: JsonNode? = null,
+                             val responseNormalizerSpec: JsonNode? = null)
 
+@JsonInclude(JsonInclude.Include.NON_EMPTY)
 data class RequestDefinition(val method: String,
                              @JsonDeserialize(using = PathDeserializer::class)
                              val path: String,
                              val headers: Map<String, String> = emptyMap(),
-                             val body: JsonNode = MissingNode.getInstance())
+                             val body: JsonNode? = null)
 
-data class ResponseDefinition(val status: Int = 200, val body: JsonNode = MissingNode.getInstance()) {
+@JsonInclude(JsonInclude.Include.NON_EMPTY)
+data class ResponseDefinition(val status: Int = 200, val body: JsonNode? = null) {
     companion object {
         val DEFAULT_RESPONSE = ResponseDefinition()
     }
 }
 
+@JsonInclude(JsonInclude.Include.NON_EMPTY)
 data class ExpectationDefinition(val request: RequestDefinition,
                                  val response: ResponseDefinition = ResponseDefinition.DEFAULT_RESPONSE)
 
+@JsonInclude(JsonInclude.Include.NON_EMPTY)
 data class ServiceDefinition(val selector: String, val expectations: List<ExpectationDefinition>)
 
+@JsonInclude(JsonInclude.Include.NON_EMPTY)
 data class UatDefinition(val processes: List<ProcessDefinition>,
                          @JsonAlias("external-services")
                          val externalServices: List<ServiceDefinition> = emptyList()) {
 
-    companion object {
-        fun load(mapper: ObjectMapper, path: Path): UatDefinition {
-            return path.toFile().reader().use { reader ->
-                mapper.convertValue(Yaml().load(reader), UatDefinition::class.java)
+    fun dump(mapper: ObjectMapper, excludedProperties: List<String> = emptyList()): String {
+        val uatAsMap: Map<String, Any> = mapper.convertValue(this)
+        if (excludedProperties.isNotEmpty()) {
+            pruneTree(uatAsMap, excludedProperties)
+        }
+        return Yaml().dumpAs(uatAsMap, Tag.MAP, DumperOptions.FlowStyle.BLOCK)
+    }
+
+    fun toBare(): UatDefinition {
+        val newProcesses = processes.map { p ->
+            ProcessDefinition(p.name, p.request, null, p.responseNormalizerSpec)
+        }
+        return UatDefinition(newProcesses)
+    }
+
+    private fun pruneTree(node: Any?, excludedProperties: List<String>) {
+        when (node) {
+            is MutableMap<*, *> -> {
+                excludedProperties.forEach { key -> node.remove(key) }
+                node.forEach { (_, value) -> pruneTree(value, excludedProperties) }
             }
+            is List<*> -> node.forEach { value -> pruneTree(value, excludedProperties) }
         }
     }
+
+    companion object {
+        fun load(mapper: ObjectMapper, spec: String): UatDefinition =
+                mapper.convertValue(Yaml().load(spec), UatDefinition::class.java)
+
+    }
 }
diff --git a/ms/blueprintsprocessor/application/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/UatExecutor.kt b/ms/blueprintsprocessor/application/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/UatExecutor.kt
new file mode 100644 (file)
index 0000000..6678075
--- /dev/null
@@ -0,0 +1,324 @@
+/*-
+ * ============LICENSE_START=======================================================
+ *  Copyright (C) 2019 Nordix Foundation.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+package org.onap.ccsdk.cds.blueprintsprocessor.uat
+
+import com.fasterxml.jackson.databind.JsonNode
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.nhaarman.mockitokotlin2.any
+import com.nhaarman.mockitokotlin2.argThat
+import com.nhaarman.mockitokotlin2.atLeast
+import com.nhaarman.mockitokotlin2.atLeastOnce
+import com.nhaarman.mockitokotlin2.eq
+import com.nhaarman.mockitokotlin2.mock
+import com.nhaarman.mockitokotlin2.verify
+import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions
+import com.nhaarman.mockitokotlin2.whenever
+import org.apache.http.HttpHeaders
+import org.apache.http.HttpStatus
+import org.apache.http.client.HttpClient
+import org.apache.http.client.methods.HttpPost
+import org.apache.http.entity.ContentType
+import org.apache.http.entity.StringEntity
+import org.apache.http.entity.mime.HttpMultipartMode
+import org.apache.http.entity.mime.MultipartEntityBuilder
+import org.apache.http.impl.client.HttpClientBuilder
+import org.apache.http.message.BasicHeader
+import org.hamcrest.CoreMatchers.equalTo
+import org.hamcrest.CoreMatchers.notNullValue
+import org.hamcrest.MatcherAssert.assertThat
+import org.mockito.Answers
+import org.onap.ccsdk.cds.blueprintsprocessor.rest.service.BluePrintRestLibPropertyService
+import org.onap.ccsdk.cds.blueprintsprocessor.rest.service.BlueprintWebClientService
+import org.onap.ccsdk.cds.blueprintsprocessor.rest.service.BlueprintWebClientService.WebClientResponse
+import org.onap.ccsdk.cds.blueprintsprocessor.uat.logging.LogColor.COLOR_MOCKITO
+import org.onap.ccsdk.cds.blueprintsprocessor.uat.logging.LogColor.markerOf
+import org.onap.ccsdk.cds.blueprintsprocessor.uat.logging.MockInvocationLogger
+import org.skyscreamer.jsonassert.JSONAssert
+import org.skyscreamer.jsonassert.JSONCompareMode
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import org.springframework.core.env.ConfigurableEnvironment
+import org.springframework.http.MediaType
+import org.springframework.stereotype.Component
+import org.springframework.util.Base64Utils
+import java.util.concurrent.ConcurrentHashMap
+
+/**
+ * Assumptions:
+ *
+ * - Application HTTP service is bound to loopback interface;
+ * - Password is either defined in plain (with "{noop}" prefix), or it's the same of username.
+ *
+ * @author Eliezio Oliveira
+ */
+@Component
+class UatExecutor(
+        private val environment: ConfigurableEnvironment,
+        private val restClientFactory: BluePrintRestLibPropertyService,
+        private val mapper: ObjectMapper
+) {
+
+    companion object {
+        private const val NOOP_PASSWORD_PREFIX = "{noop}"
+
+        private val log: Logger = LoggerFactory.getLogger(UatExecutor::class.java)
+        private val mockLoggingListener = MockInvocationLogger(markerOf(COLOR_MOCKITO))
+    }
+
+    // use lazy evaluation to postpone until localServerPort is injected by Spring
+    private val baseUrl: String by lazy {
+        "http://127.0.0.1:${localServerPort()}"
+    }
+
+    @Throws(AssertionError::class)
+    fun execute(uatSpec: String, cbaBytes: ByteArray) {
+        val uat = UatDefinition.load(mapper, uatSpec)
+        execute(uat, cbaBytes)
+    }
+
+    /**
+     *
+     * The UAT can range from minimum to completely defined.
+     *
+     * @return an updated UAT with all NB and SB messages.
+     */
+    @Throws(AssertionError::class)
+    fun execute(uat: UatDefinition, cbaBytes: ByteArray): UatDefinition {
+        val defaultHeaders = listOf(BasicHeader(HttpHeaders.AUTHORIZATION, clientAuthToken()))
+        val httpClient = HttpClientBuilder.create()
+                .setDefaultHeaders(defaultHeaders)
+                .build()
+        // Only if externalServices are defined
+        val mockInterceptor = MockPreInterceptor()
+        // Always defined and used, whatever the case
+        val spyInterceptor = SpyPostInterceptor(mapper)
+        restClientFactory.setInterceptors(mockInterceptor, spyInterceptor)
+        try {
+            // Configure mocked external services and save their expected requests for further validation
+            val requestsPerClient = uat.externalServices.associateBy(
+                    { service ->
+                        createRestClientMock(service.expectations).also { restClient ->
+                            // side-effect: register restClient to override real instance
+                            mockInterceptor.registerMock(service.selector, restClient)
+                        }
+                    },
+                    { service -> service.expectations.map { it.request } }
+            )
+
+            val newProcesses = httpClient.use { client ->
+                uploadBlueprint(client, cbaBytes)
+
+                // Run processes
+                uat.processes.map { process ->
+                    log.info("Executing process '${process.name}'")
+                    val responseNormalizer = JsonNormalizer.getNormalizer(mapper, process.responseNormalizerSpec)
+                    val actualResponse = processBlueprint(client, process.request,
+                            process.expectedResponse, responseNormalizer)
+                    ProcessDefinition(process.name, process.request, actualResponse, process.responseNormalizerSpec)
+                }
+            }
+
+            // Validate requests to external services
+            for ((mockClient, requests) in requestsPerClient) {
+                requests.forEach { request ->
+                    verify(mockClient, atLeastOnce()).exchangeResource(
+                            eq(request.method),
+                            eq(request.path),
+                            argThat { assertJsonEquals(request.body, this) },
+                            argThat(RequiredMapEntriesMatcher(request.headers)))
+                }
+                // Don't mind the invocations to the overloaded exchangeResource(String, String, String)
+                verify(mockClient, atLeast(0)).exchangeResource(any(), any(), any())
+                verifyNoMoreInteractions(mockClient)
+            }
+
+            val newExternalServices = spyInterceptor.getSpies()
+                    .map(SpyService::asServiceDefinition)
+
+            return UatDefinition(newProcesses, newExternalServices)
+        } finally {
+            restClientFactory.clearInterceptors()
+        }
+    }
+
+    private fun createRestClientMock(restExpectations: List<ExpectationDefinition>)
+            : BlueprintWebClientService {
+        val restClient = mock<BlueprintWebClientService>(
+                defaultAnswer = Answers.RETURNS_SMART_NULLS,
+                // our custom verboseLogging handler
+                invocationListeners = arrayOf(mockLoggingListener)
+        )
+
+        // Delegates to overloaded exchangeResource(String, String, String, Map<String, String>)
+        whenever(restClient.exchangeResource(any(), any(), any()))
+                .thenAnswer { invocation ->
+                    val method = invocation.arguments[0] as String
+                    val path = invocation.arguments[1] as String
+                    val request = invocation.arguments[2] as String
+                    restClient.exchangeResource(method, path, request, emptyMap())
+                }
+        for (expectation in restExpectations) {
+            whenever(restClient.exchangeResource(
+                    eq(expectation.request.method),
+                    eq(expectation.request.path),
+                    any(),
+                    any()))
+                    .thenReturn(WebClientResponse(expectation.response.status, expectation.response.body.toString()))
+        }
+        return restClient
+    }
+
+    @Throws(AssertionError::class)
+    private fun uploadBlueprint(client: HttpClient, cbaBytes: ByteArray) {
+        val multipartEntity = MultipartEntityBuilder.create()
+                .setMode(HttpMultipartMode.BROWSER_COMPATIBLE)
+                .addBinaryBody("file", cbaBytes, ContentType.DEFAULT_BINARY, "cba.zip")
+                .build()
+        val request = HttpPost("$baseUrl/api/v1/blueprint-model/publish").apply {
+            entity = multipartEntity
+        }
+        client.execute(request) { response ->
+            val statusLine = response.statusLine
+            assertThat(statusLine.statusCode, equalTo(HttpStatus.SC_OK))
+        }
+    }
+
+    @Throws(AssertionError::class)
+    private fun processBlueprint(client: HttpClient, requestBody: JsonNode,
+                                 expectedResponse: JsonNode?, responseNormalizer: (String) -> String): JsonNode {
+        val stringEntity = StringEntity(mapper.writeValueAsString(requestBody), ContentType.APPLICATION_JSON)
+        val request = HttpPost("$baseUrl/api/v1/execution-service/process").apply {
+            entity = stringEntity
+        }
+        val response = client.execute(request) { response ->
+            val statusLine = response.statusLine
+            assertThat(statusLine.statusCode, equalTo(HttpStatus.SC_OK))
+            val entity = response.entity
+            assertThat("Response contains no content", entity, notNullValue())
+            entity.content.bufferedReader().use { it.readText() }
+        }
+        val actualResponse = responseNormalizer(response)
+        if (expectedResponse != null) {
+            assertJsonEquals(expectedResponse, actualResponse)
+        }
+        return mapper.readTree(actualResponse)!!
+    }
+
+    @Throws(AssertionError::class)
+    private fun assertJsonEquals(expected: JsonNode?, actual: String): Boolean {
+        // special case
+        if ((expected == null) && actual.isBlank()) {
+            return true
+        }
+        // general case
+        JSONAssert.assertEquals(expected?.toString(), actual, JSONCompareMode.LENIENT)
+        // assertEquals throws an exception whenever match fails
+        return true
+    }
+
+    private fun localServerPort(): Int =
+            (environment.getProperty("local.server.port")
+                    ?: environment.getRequiredProperty("blueprint.httpPort")).toInt()
+
+    private fun clientAuthToken(): String {
+        val username = environment.getRequiredProperty("security.user.name")
+        val password = environment.getRequiredProperty("security.user.password")
+        val plainPassword = when {
+            password.startsWith(NOOP_PASSWORD_PREFIX) -> password.substring(NOOP_PASSWORD_PREFIX.length)
+            else -> username
+        }
+        return "Basic " + Base64Utils.encodeToString("$username:$plainPassword".toByteArray())
+    }
+
+    private class MockPreInterceptor : BluePrintRestLibPropertyService.PreInterceptor {
+        private val mocks = ConcurrentHashMap<String, BlueprintWebClientService>()
+
+        override fun getInstance(jsonNode: JsonNode): BlueprintWebClientService? {
+            TODO("jsonNode-keyed services not yet supported")
+        }
+
+        override fun getInstance(selector: String): BlueprintWebClientService? =
+                mocks[selector]
+
+        fun registerMock(selector: String, client: BlueprintWebClientService) {
+            mocks[selector] = client
+        }
+    }
+
+    private class SpyPostInterceptor(private val mapper: ObjectMapper) : BluePrintRestLibPropertyService.PostInterceptor {
+
+        private val spies = ConcurrentHashMap<String, SpyService>()
+
+        override fun getInstance(jsonNode: JsonNode, service: BlueprintWebClientService): BlueprintWebClientService {
+            TODO("jsonNode-keyed services not yet supported")
+        }
+
+        override fun getInstance(selector: String, service: BlueprintWebClientService): BlueprintWebClientService {
+            val spiedService = SpyService(mapper, selector, service)
+            spies[selector] = spiedService
+            return spiedService
+        }
+
+        fun getSpies(): List<SpyService> =
+                spies.values.toList()
+    }
+
+    private class SpyService(private val mapper: ObjectMapper,
+                             val selector: String,
+                             private val realService: BlueprintWebClientService) :
+            BlueprintWebClientService by realService {
+
+        private val expectations: MutableList<ExpectationDefinition> = mutableListOf()
+
+        override fun exchangeResource(methodType: String, path: String, request: String): WebClientResponse<String> =
+                exchangeResource(methodType, path, request, DEFAULT_HEADERS)
+
+        override fun exchangeResource(methodType: String, path: String, request: String,
+                                      headers: Map<String, String>): WebClientResponse<String> {
+            val requestDefinition = RequestDefinition(methodType, path, headers, toJson(request))
+            val realAnswer = realService.exchangeResource(methodType, path, request, headers)
+            val responseBody = when {
+                // TODO: confirm if we need to normalize the response here
+                realAnswer.status == HttpStatus.SC_OK -> toJson(realAnswer.body)
+                else -> null
+            }
+            val responseDefinition = ResponseDefinition(realAnswer.status, responseBody)
+            expectations.add(ExpectationDefinition(requestDefinition, responseDefinition))
+            return realAnswer
+        }
+
+        fun asServiceDefinition() =
+                ServiceDefinition(selector, expectations)
+
+        private fun toJson(str: String): JsonNode? {
+            return when {
+                str.isNotBlank() -> mapper.readTree(str)
+                else -> null
+            }
+        }
+
+        companion object {
+            private val DEFAULT_HEADERS = mapOf(
+                    HttpHeaders.CONTENT_TYPE to MediaType.APPLICATION_JSON_VALUE,
+                    HttpHeaders.ACCEPT to MediaType.APPLICATION_JSON_VALUE
+            )
+        }
+    }
+}
\ No newline at end of file
diff --git a/ms/blueprintsprocessor/application/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/UatServices.kt b/ms/blueprintsprocessor/application/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/UatServices.kt
new file mode 100644 (file)
index 0000000..f133fd7
--- /dev/null
@@ -0,0 +1,121 @@
+/*-
+ * ============LICENSE_START=======================================================
+ *  Copyright (C) 2019 Nordix Foundation.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+package org.onap.ccsdk.cds.blueprintsprocessor.uat
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import kotlinx.coroutines.runBlocking
+import org.onap.ccsdk.cds.blueprintsprocessor.uat.logging.LogColor.COLOR_SERVICES
+import org.onap.ccsdk.cds.blueprintsprocessor.uat.logging.LogColor.resetContextColor
+import org.onap.ccsdk.cds.blueprintsprocessor.uat.logging.LogColor.setContextColor
+import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintConstants.UAT_SPECIFICATION_FILE
+import org.springframework.context.annotation.Profile
+import org.springframework.http.HttpStatus
+import org.springframework.http.MediaType
+import org.springframework.http.codec.multipart.FilePart
+import org.springframework.security.access.prepost.PreAuthorize
+import org.springframework.web.bind.annotation.PostMapping
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RequestPart
+import org.springframework.web.bind.annotation.RestController
+import org.springframework.web.server.ResponseStatusException
+import java.io.File
+import java.util.zip.ZipFile
+
+/**
+ * Supporting services to help creating UAT specifications.
+ *
+ * @author Eliezio Oliveira
+ */
+@RestController
+@RequestMapping("/api/v1/uat")
+@Profile("uat")
+open class UatServices(private val uatExecutor: UatExecutor, private val mapper: ObjectMapper) {
+
+    @PostMapping("/verify", consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
+    @PreAuthorize("hasRole('USER')")
+    @Suppress("BlockingMethodInNonBlockingContext")
+    open fun verify(@RequestPart("cba") cbaFile: FilePart) = runBlocking {
+        setContextColor(COLOR_SERVICES)
+        val tempFile = createTempFile()
+        try {
+            cbaFile.transferTo(tempFile)
+            val uatSpec = readZipEntryAsText(tempFile, UAT_SPECIFICATION_FILE)
+            val cbaBytes = tempFile.readBytes()
+            uatExecutor.execute(uatSpec, cbaBytes)
+        } catch (e: AssertionError) {
+            throw ResponseStatusException(HttpStatus.BAD_REQUEST, e.message)
+        } catch (t: Throwable) {
+            throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, t.message, t)
+        } finally {
+            tempFile.delete()
+            resetContextColor()
+        }
+    }
+
+    @PostMapping("/spy", consumes = [MediaType.MULTIPART_FORM_DATA_VALUE], produces = ["text/vnd.yaml"])
+    @PreAuthorize("hasRole('USER')")
+    @Suppress("BlockingMethodInNonBlockingContext")
+    open fun spy(@RequestPart("cba") cbaFile: FilePart,
+                 @RequestPart("uat", required = false) uatFile: FilePart?): String = runBlocking {
+        val tempFile = createTempFile()
+        setContextColor(COLOR_SERVICES)
+        try {
+            cbaFile.transferTo(tempFile)
+            val uatSpec = when {
+                uatFile != null -> uatFile.readText()
+                else -> readZipEntryAsText(tempFile, UAT_SPECIFICATION_FILE)
+            }
+            val uat = UatDefinition.load(mapper, uatSpec)
+            val cbaBytes = tempFile.readBytes()
+            val updatedUat = uatExecutor.execute(uat, cbaBytes)
+            return@runBlocking updatedUat.dump(mapper, FIELDS_TO_EXCLUDE)
+        } catch (t: Throwable) {
+            throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, t.message, t)
+        } finally {
+            tempFile.delete()
+            resetContextColor()
+        }
+    }
+
+    private fun FilePart.readText(): String {
+        val tempFile = createTempFile()
+        try {
+            transferTo(tempFile).block()
+            return tempFile.readText()
+        } finally {
+            tempFile.delete()
+        }
+    }
+
+    @Suppress("SameParameterValue")
+    private fun readZipEntryAsText(file: File, entryName: String): String {
+        return ZipFile(file).use { zipFile -> zipFile.readEntryAsText(entryName) }
+    }
+
+    private fun ZipFile.readEntryAsText(entryName: String): String {
+        val zipEntry = getEntry(entryName)
+        return getInputStream(zipEntry).readBytes().toString(Charsets.UTF_8)
+    }
+
+    companion object {
+        // Fields that can be safely ignored from BPP response, and can be omitted on the UAT specification.
+        private val FIELDS_TO_EXCLUDE = listOf("timestamp")
+    }
+}
\ No newline at end of file
  * SPDX-License-Identifier: Apache-2.0
  * ============LICENSE_END=========================================================
  */
-package org.onap.ccsdk.cds.blueprintsprocessor
+package org.onap.ccsdk.cds.blueprintsprocessor.uat.logging
 
-import org.springframework.util.CollectionUtils
-import org.springframework.util.MultiValueMap
+import org.slf4j.Marker
 
-
-/**
- * Convenient method to create a single-entry MultiValueMap.
- */
-fun <K, V> toMultiValueMap(key: K, vararg values: V): MultiValueMap<K, V> {
-    return CollectionUtils.toMultiValueMap(mapOf(key to values.asList()))
-}
+class ColorMarker internal constructor(private val dlg: Marker) : Marker by dlg
\ No newline at end of file
diff --git a/ms/blueprintsprocessor/application/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/logging/LogColor.kt b/ms/blueprintsprocessor/application/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/logging/LogColor.kt
new file mode 100644 (file)
index 0000000..dce5169
--- /dev/null
@@ -0,0 +1,45 @@
+/*-
+ * ============LICENSE_START=======================================================
+ *  Copyright (C) 2019 Nordix Foundation.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+package org.onap.ccsdk.cds.blueprintsprocessor.uat.logging
+
+import org.slf4j.MDC
+import org.slf4j.MarkerFactory
+
+object LogColor {
+
+    const val COLOR_SERVICES = "green"
+    const val COLOR_TEST_CLIENT = "yellow"
+    const val COLOR_MOCKITO = "cyan"
+    const val COLOR_WIREMOCK = "blue"
+
+    // The Slf4j MDC key that will hold the global color
+    const val MDC_COLOR_KEY = "color"
+
+    fun setContextColor(color: String) {
+        MDC.put(MDC_COLOR_KEY, color)
+    }
+
+    fun resetContextColor() {
+        MDC.remove(MDC_COLOR_KEY)
+    }
+
+    fun markerOf(color: String): ColorMarker =
+            ColorMarker(MarkerFactory.getMarker(color))
+}
diff --git a/ms/blueprintsprocessor/application/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/logging/MockInvocationLogger.kt b/ms/blueprintsprocessor/application/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/logging/MockInvocationLogger.kt
new file mode 100644 (file)
index 0000000..f8e6bd4
--- /dev/null
@@ -0,0 +1,65 @@
+/*-
+ * ============LICENSE_START=======================================================
+ *  Copyright (C) 2019 Nordix Foundation.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+package org.onap.ccsdk.cds.blueprintsprocessor.uat.logging
+
+import org.mockito.listeners.InvocationListener
+import org.mockito.listeners.MethodInvocationReport
+import org.slf4j.LoggerFactory
+import org.slf4j.Marker
+import java.util.concurrent.atomic.AtomicInteger
+
+/**
+ * Logs all Mockito's mock/spy invocations.
+ *
+ * Used for debugging interactions with a mock.
+ */
+class MockInvocationLogger(private val marker: Marker) : InvocationListener {
+
+    private val mockInvocationsCounter = AtomicInteger()
+
+    override fun reportInvocation(report: MethodInvocationReport) {
+        val sb = StringBuilder()
+        sb.appendln("Method invocation #${mockInvocationsCounter.incrementAndGet()} on mock/spy")
+        report.locationOfStubbing?.let { location ->
+            sb.append(INDENT).append("stubbed ").appendln(location)
+        }
+        sb.appendln(report.invocation)
+        sb.append(INDENT).append("invoked ").appendln(report.invocation.location)
+        if (report.threwException()) {
+            sb.append(INDENT).append("has thrown -> ").append(report.throwable.javaClass.name)
+            report.throwable.message?.let { message ->
+                sb.append(" with message ").append(message)
+            }
+            sb.appendln()
+        } else {
+            sb.append(INDENT).append("has returned -> \"").append(report.returnedValue).append('"')
+            report.returnedValue?.let { value ->
+                sb.append(" (").append(value.javaClass.name).append(')')
+            }
+            sb.appendln()
+        }
+        log.info(marker, sb.toString())
+    }
+
+    companion object {
+        private const val INDENT = "    "
+        private val log = LoggerFactory.getLogger(MockInvocationLogger::class.java)
+    }
+}
diff --git a/ms/blueprintsprocessor/application/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/logging/SmartColorDiscriminator.kt b/ms/blueprintsprocessor/application/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/logging/SmartColorDiscriminator.kt
new file mode 100644 (file)
index 0000000..d7b38d3
--- /dev/null
@@ -0,0 +1,41 @@
+/*-
+ * ============LICENSE_START=======================================================
+ *  Copyright (C) 2019 Nordix Foundation.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+package org.onap.ccsdk.cds.blueprintsprocessor.uat.logging
+
+import ch.qos.logback.classic.spi.ILoggingEvent
+import ch.qos.logback.core.sift.AbstractDiscriminator
+import org.onap.ccsdk.cds.blueprintsprocessor.uat.logging.LogColor.MDC_COLOR_KEY
+
+class SmartColorDiscriminator : AbstractDiscriminator<ILoggingEvent>() {
+    var defaultValue: String = "white"
+
+    override fun getKey(): String {
+        return MDC_COLOR_KEY
+    }
+
+    fun setKey() {
+        throw UnsupportedOperationException("Key not settable. Using $MDC_COLOR_KEY")
+    }
+
+    override fun getDiscriminatingValue(e: ILoggingEvent): String =
+            (e.marker as? ColorMarker)?.name
+                    ?: e.mdcPropertyMap?.get(MDC_COLOR_KEY)
+                    ?: defaultValue
+}
\ No newline at end of file
diff --git a/ms/blueprintsprocessor/application/src/main/resources/application-local.yml b/ms/blueprintsprocessor/application/src/main/resources/application-local.yml
new file mode 100644 (file)
index 0000000..de2cf4e
--- /dev/null
@@ -0,0 +1,65 @@
+appName: ControllerBluePrints
+appVersion: 1.0.0
+blueprints:
+    processor:
+        functions:
+            python:
+                executor:
+                    executionPath: ./components/scripts/python/ccsdk_blueprints
+                    modulePaths: ./components/scripts/python/ccsdk_blueprints,./components/scripts/python/ccsdk_netconf,./components/scripts/python/ccsdk_restconf
+blueprintsprocessor:
+    blueprintArchivePath: /tmp/cds/archive
+    blueprintDeployPath: /tmp/cds/deploy
+    blueprintWorkingPath: /tmp/cds/work
+    db:
+        primary:
+            driverClassName: org.mariadb.jdbc.Driver
+            hibernateDDLAuto: none
+            hibernateDialect: org.hibernate.dialect.MySQL5InnoDBDialect
+            hibernateHbm2ddlAuto: update
+            hibernateNamingStrategy: org.hibernate.cfg.ImprovedNamingStrategy
+            password: sdnctl
+            url: jdbc:mysql://localhost:3306/sdnctl
+            username: sdnctl
+    grpcEnable: false
+    grpcPort: 9111
+    httpPort: 8080
+    loadModelType: false
+    loadResourceDictionary: false
+    messageclient:
+        self-service-api:
+            bootstrapServers: 127.0.0.1:9092
+            clientId: default-client-id
+            consumerTopic: receiver.t
+            groupId: receiver-id
+            kafkaEnable: false
+            topic: producer.t
+            type: kafka-basic-auth
+    remoteScriptCommand:
+        enabled: true
+    restclient:
+        sdncodl:
+            password: Kp8bJ4SXszM0WXlhak3eHlcse2gAw84vaoGGmJvUy2U
+            type: basic-auth
+            url: http://localhost:8282/
+            username: admin
+    restconfEnabled: true
+controllerblueprints:
+    loadInitialData: true
+logging:
+    level:
+        org:
+            springframework:
+                boot:
+                    context:
+                        config: debug
+ms_name: org.onap.ccsdk.apps.controllerblueprints
+spring:
+    datasource:
+        password: sdnctl
+        url: jdbc:mysql://localhost:3306/sdnctl
+        username: sdnctl
+    security:
+        user:
+            name: ccsdkapps
+            password: '{bcrypt}$2a$10$duaUzVUVW0YPQCSIbGEkQOXwafZGwQ/b32/Ys4R1iwSSawFgz7QNu'
diff --git a/ms/blueprintsprocessor/application/src/main/resources/application-uat.yml b/ms/blueprintsprocessor/application/src/main/resources/application-uat.yml
new file mode 100644 (file)
index 0000000..f00d62b
--- /dev/null
@@ -0,0 +1,4 @@
+server:
+    error:
+        include-exception: true
+        include-stacktrace: always
diff --git a/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/BlueprintsAcceptanceTest.kt b/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/BlueprintsAcceptanceTest.kt
deleted file mode 100644 (file)
index ce7434f..0000000
+++ /dev/null
@@ -1,242 +0,0 @@
-/*-
- * ============LICENSE_START=======================================================
- *  Copyright (C) 2019 Nordix Foundation.
- * ================================================================================
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- * SPDX-License-Identifier: Apache-2.0
- * ============LICENSE_END=========================================================
- */
-package org.onap.ccsdk.cds.blueprintsprocessor
-
-import com.fasterxml.jackson.databind.JsonNode
-import com.fasterxml.jackson.databind.ObjectMapper
-import com.fasterxml.jackson.databind.node.MissingNode
-import com.nhaarman.mockitokotlin2.any
-import com.nhaarman.mockitokotlin2.argThat
-import com.nhaarman.mockitokotlin2.atLeast
-import com.nhaarman.mockitokotlin2.atLeastOnce
-import com.nhaarman.mockitokotlin2.eq
-import com.nhaarman.mockitokotlin2.mock
-import com.nhaarman.mockitokotlin2.verify
-import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions
-import com.nhaarman.mockitokotlin2.whenever
-import org.junit.ClassRule
-import org.junit.Rule
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-import org.mockito.Answers
-import org.onap.ccsdk.cds.blueprintsprocessor.rest.RestLibConstants
-import org.onap.ccsdk.cds.blueprintsprocessor.rest.service.BluePrintRestLibPropertyService
-import org.onap.ccsdk.cds.blueprintsprocessor.rest.service.BlueprintWebClientService
-import org.onap.ccsdk.cds.blueprintsprocessor.rest.service.BlueprintWebClientService.WebClientResponse
-import org.onap.ccsdk.cds.controllerblueprints.core.utils.BluePrintArchiveUtils.Companion.compressToBytes
-import org.skyscreamer.jsonassert.JSONAssert
-import org.skyscreamer.jsonassert.JSONCompareMode
-import org.slf4j.Logger
-import org.slf4j.LoggerFactory
-import org.springframework.beans.factory.annotation.Autowired
-import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient
-import org.springframework.boot.test.context.SpringBootTest
-import org.springframework.boot.test.mock.mockito.MockBean
-import org.springframework.core.io.ByteArrayResource
-import org.springframework.core.io.Resource
-import org.springframework.http.MediaType
-import org.springframework.test.context.ContextConfiguration
-import org.springframework.test.context.TestPropertySource
-import org.springframework.test.context.junit4.rules.SpringClassRule
-import org.springframework.test.context.junit4.rules.SpringMethodRule
-import org.springframework.test.web.reactive.server.EntityExchangeResult
-import org.springframework.test.web.reactive.server.WebTestClient
-import reactor.core.publisher.Mono
-import java.io.File
-import java.nio.charset.StandardCharsets
-import java.nio.file.Paths
-import kotlin.test.BeforeTest
-import kotlin.test.Test
-
-// Only one runner can be configured with jUnit 4. We had to replace the SpringRunner by equivalent jUnit rules.
-// See more on https://docs.spring.io/autorepo/docs/spring-framework/current/spring-framework-reference/testing.html#testcontext-junit4-rules
-@RunWith(Parameterized::class)
-// Set blueprintsprocessor.httpPort=0 to trigger a random port selection
-@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
-@AutoConfigureWebTestClient(timeout = "PT10S")
-@ContextConfiguration(initializers = [
-    WorkingFoldersInitializer::class,
-    TestSecuritySettings.ServerContextInitializer::class
-])
-@TestPropertySource(locations = ["classpath:application-test.properties"])
-class BlueprintsAcceptanceTest(private val blueprintName: String, private val filename: String) {
-
-    companion object {
-        const val UAT_BLUEPRINTS_BASE_DIR = "../../../components/model-catalog/blueprint-model/uat-blueprints"
-        const val EMBEDDED_UAT_FILE = "Tests/uat.yaml"
-
-        @ClassRule
-        @JvmField
-        val springClassRule = SpringClassRule()
-
-        val log: Logger = LoggerFactory.getLogger(BlueprintsAcceptanceTest::class.java)
-
-        /**
-         * Generates the parameters to create a test instance for every blueprint found under UAT_BLUEPRINTS_BASE_DIR
-         * that contains the proper UAT definition file.
-         */
-        @Parameterized.Parameters(name = "{index} {0}")
-        @JvmStatic
-        fun testParameters(): List<Array<String>> {
-            return File(UAT_BLUEPRINTS_BASE_DIR)
-                    .listFiles { file -> file.isDirectory && File(file, EMBEDDED_UAT_FILE).isFile }
-                    ?.map { file -> arrayOf(file.nameWithoutExtension, file.canonicalPath) }
-                    ?: emptyList()
-        }
-    }
-
-    @Rule
-    @JvmField
-    val springMethodRule = SpringMethodRule()
-
-    @MockBean(name = RestLibConstants.SERVICE_BLUEPRINT_REST_LIB_PROPERTY, answer = Answers.RETURNS_SMART_NULLS)
-    lateinit var restClientFactory: BluePrintRestLibPropertyService
-
-    @Autowired
-    // Bean is created programmatically by {@link WorkingFoldersInitializer#initialize(String)}
-    @Suppress("SpringJavaInjectionPointsAutowiringInspection")
-    lateinit var tempFolder: ExtendedTemporaryFolder
-
-    @Autowired
-    lateinit var webTestClient: WebTestClient
-
-    @Autowired
-    lateinit var mapper: ObjectMapper
-
-    @BeforeTest
-    fun cleanupTemporaryFolder() {
-        tempFolder.deleteAllFiles()
-    }
-
-    @Test
-    fun testBlueprint() {
-        val uat = UatDefinition.load(mapper, Paths.get(filename, EMBEDDED_UAT_FILE))
-
-        uploadBlueprint(blueprintName)
-
-        // Configure mocked external services and save their expected requests for further validation
-        val requestsPerClient = uat.externalServices.associateBy(
-                { service -> createRestClientMock(service.selector, service.expectations) },
-                { service -> service.expectations.map { it.request } }
-        )
-
-        // Run processes
-        for (process in uat.processes) {
-            log.info("Executing process '${process.name}'")
-            processBlueprint(process.request, process.expectedResponse,
-                    JsonNormalizer.getNormalizer(mapper, process.responseNormalizerSpec))
-        }
-
-        // Validate requests to external services
-        for ((mockClient, requests) in requestsPerClient) {
-            requests.forEach { request ->
-                verify(mockClient, atLeastOnce()).exchangeResource(
-                        eq(request.method),
-                        eq(request.path),
-                        argThat { assertJsonEqual(request.body, this) },
-                        argThat(RequiredMapEntriesMatcher(request.headers)))
-            }
-            // Don't mind the invocations to the overloaded exchangeResource(String, String, String)
-            verify(mockClient, atLeast(0)).exchangeResource(any(), any(), any())
-            verifyNoMoreInteractions(mockClient)
-        }
-    }
-
-    private fun createRestClientMock(selector: String, restExpectations: List<ExpectationDefinition>)
-            : BlueprintWebClientService {
-        val restClient = mock<BlueprintWebClientService>(verboseLogging = true,
-                defaultAnswer = Answers.RETURNS_SMART_NULLS)
-
-        // Delegates to overloaded exchangeResource(String, String, String, Map<String, String>)
-        whenever(restClient.exchangeResource(any(), any(), any()))
-                .thenAnswer { invocation ->
-                    val method = invocation.arguments[0] as String
-                    val path = invocation.arguments[1] as String
-                    val request = invocation.arguments[2] as String
-                    restClient.exchangeResource(method, path, request, emptyMap())
-                }
-        for (expectation in restExpectations) {
-            whenever(restClient.exchangeResource(
-                    eq(expectation.request.method),
-                    eq(expectation.request.path),
-                    any(),
-                    any()))
-                    .thenReturn(WebClientResponse(expectation.response.status, expectation.response.body.toString()))
-        }
-
-        whenever(restClientFactory.blueprintWebClientService(selector))
-                .thenReturn(restClient)
-        return restClient
-    }
-
-    private fun uploadBlueprint(blueprintName: String) {
-        val body = toMultiValueMap("file", getBlueprintAsResource(blueprintName))
-        webTestClient
-                .post()
-                .uri("/api/v1/blueprint-model/publish")
-                .header("Authorization", TestSecuritySettings.clientAuthToken())
-                .syncBody(body)
-                .exchange()
-                .expectStatus().isOk
-    }
-
-    private fun processBlueprint(request: JsonNode, expectedResponse: JsonNode,
-                                 responseNormalizer: (String) -> String) {
-        webTestClient
-                .post()
-                .uri("/api/v1/execution-service/process")
-                .header("Authorization", TestSecuritySettings.clientAuthToken())
-                .contentType(MediaType.APPLICATION_JSON_UTF8)
-                .body(Mono.just(request.toString()), String::class.java)
-                .exchange()
-                .expectStatus().isOk
-                .expectBody()
-                .consumeWith { response ->
-                    assertJsonEqual(expectedResponse, responseNormalizer(getBodyAsString(response)))
-                }
-    }
-
-    private fun getBlueprintAsResource(blueprintName: String): Resource {
-        val baseDir = Paths.get(UAT_BLUEPRINTS_BASE_DIR, blueprintName)
-        val zipBytes = compressToBytes(baseDir)
-        return object : ByteArrayResource(zipBytes) {
-            // Filename has to be returned in order to be able to post
-            override fun getFilename() = "$blueprintName.zip"
-        }
-    }
-
-    private fun assertJsonEqual(expected: JsonNode, actual: String): Boolean {
-        if ((actual == "") && (expected is MissingNode)) {
-            return true
-        }
-        JSONAssert.assertEquals(expected.toString(), actual, JSONCompareMode.LENIENT)
-        // assertEquals throws an exception whenever match fails
-        return true
-    }
-
-    private fun getBodyAsString(result: EntityExchangeResult<ByteArray>): String {
-        val body = result.responseBody
-        if ((body == null) || body.isEmpty()) {
-            return ""
-        }
-        val charset = result.responseHeaders.contentType?.charset ?: StandardCharsets.UTF_8
-        return String(body, charset)
-    }
-}
\ No newline at end of file
diff --git a/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/BaseUatTest.kt b/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/BaseUatTest.kt
new file mode 100644 (file)
index 0000000..ec338f2
--- /dev/null
@@ -0,0 +1,56 @@
+/*-
+ * ============LICENSE_START=======================================================
+ *  Copyright (C) 2019 Nordix Foundation.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+package org.onap.ccsdk.cds.blueprintsprocessor.uat
+
+import org.junit.runner.RunWith
+import org.onap.ccsdk.cds.blueprintsprocessor.uat.logging.LogColor.COLOR_TEST_CLIENT
+import org.onap.ccsdk.cds.blueprintsprocessor.uat.logging.LogColor.resetContextColor
+import org.onap.ccsdk.cds.blueprintsprocessor.uat.logging.LogColor.setContextColor
+import org.springframework.boot.test.context.SpringBootTest
+import org.springframework.test.context.ContextConfiguration
+import org.springframework.test.context.TestPropertySource
+import org.springframework.test.context.junit4.SpringRunner
+import kotlin.test.AfterTest
+import kotlin.test.BeforeTest
+
+@RunWith(SpringRunner::class)
+// Also set blueprintsprocessor.httpPort=0
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+@ContextConfiguration(initializers = [
+    WorkingFoldersInitializer::class,
+    TestSecuritySettings.ServerContextInitializer::class
+])
+@TestPropertySource(locations = ["classpath:application-test.properties"])
+abstract class BaseUatTest {
+
+    @BeforeTest
+    fun setScope() {
+        setContextColor(COLOR_TEST_CLIENT)
+    }
+
+    @AfterTest
+    fun clearScope() {
+        resetContextColor()
+    }
+
+    companion object {
+        const val UAT_BLUEPRINTS_BASE_DIR = "../../../components/model-catalog/blueprint-model/uat-blueprints"
+    }
+}
\ No newline at end of file
diff --git a/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/BlueprintsAcceptanceTest.kt b/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/BlueprintsAcceptanceTest.kt
new file mode 100644 (file)
index 0000000..4fed0ce
--- /dev/null
@@ -0,0 +1,91 @@
+/*-
+ * ============LICENSE_START=======================================================
+ *  Copyright (C) 2019 Nordix Foundation.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+package org.onap.ccsdk.cds.blueprintsprocessor.uat
+
+import org.junit.ClassRule
+import org.junit.Rule
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintConstants.UAT_SPECIFICATION_FILE
+import org.onap.ccsdk.cds.controllerblueprints.core.utils.BluePrintArchiveUtils.Companion.compressToBytes
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.test.context.junit4.rules.SpringClassRule
+import org.springframework.test.context.junit4.rules.SpringMethodRule
+import java.io.File
+import java.nio.file.FileSystem
+import java.nio.file.FileSystems
+import kotlin.test.BeforeTest
+import kotlin.test.Test
+
+// Only one runner can be configured with jUnit 4. We had to replace the SpringRunner by equivalent jUnit rules.
+// See more on https://docs.spring.io/autorepo/docs/spring-framework/current/spring-framework-reference/testing.html#testcontext-junit4-rules
+@RunWith(Parameterized::class)
+class BlueprintsAcceptanceTest(@Suppress("unused") private val blueprintName: String, // readable test description
+                               private val rootFs: FileSystem): BaseUatTest() {
+
+    companion object {
+
+        @ClassRule
+        @JvmField
+        val springClassRule = SpringClassRule()
+
+        /**
+         * Generates the parameters to create a test instance for every blueprint found under UAT_BLUEPRINTS_BASE_DIR
+         * that contains the proper UAT definition file.
+         */
+        @Parameterized.Parameters(name = "{index} {0}")
+        @JvmStatic
+        fun scanUatEmpoweredBlueprints(): List<Array<Any>> {
+            return (File(UAT_BLUEPRINTS_BASE_DIR)
+                    .listFiles { file -> file.isDirectory && File(file, UAT_SPECIFICATION_FILE).isFile }
+                    ?: throw RuntimeException("Failed to scan $UAT_BLUEPRINTS_BASE_DIR"))
+                    .map { file ->
+                        arrayOf(
+                                file.nameWithoutExtension,
+                                FileSystems.newFileSystem(file.canonicalFile.toPath(), null)
+                        )
+                    }
+        }
+    }
+
+    @Rule
+    @JvmField
+    val springMethodRule = SpringMethodRule()
+
+    @Autowired
+    // Bean is created programmatically by {@link WorkingFoldersInitializer#initialize(String)}
+    @Suppress("SpringJavaInjectionPointsAutowiringInspection")
+    lateinit var tempFolder: ExtendedTemporaryFolder
+
+    @Autowired
+    lateinit var uatExecutor: UatExecutor
+
+    @BeforeTest
+    fun cleanupTemporaryFolder() {
+        tempFolder.deleteAllFiles()
+    }
+
+    @Test
+    fun runUat() {
+        val uatSpec = rootFs.getPath(UAT_SPECIFICATION_FILE).toFile().readText()
+        val cbaBytes = compressToBytes(rootFs.getPath("/"))
+        uatExecutor.execute(uatSpec, cbaBytes)
+    }
+}
\ No newline at end of file
@@ -17,9 +17,8 @@
  * SPDX-License-Identifier: Apache-2.0
  * ============LICENSE_END=========================================================
  */
-package org.onap.ccsdk.cds.blueprintsprocessor
+package org.onap.ccsdk.cds.blueprintsprocessor.uat
 
-import org.junit.rules.TemporaryFolder
 import java.io.File
 import java.io.IOException
 import java.nio.file.*
@@ -27,25 +26,27 @@ import java.nio.file.attribute.*
 import javax.annotation.PreDestroy
 
 class ExtendedTemporaryFolder {
-    private val tempFolder = TemporaryFolder()
-
-    init {
-        tempFolder.create()
-    }
+    private val tempFolder = createTempDir("uat")
 
     @PreDestroy
-    fun delete() = tempFolder.delete()
+    fun delete() = tempFolder.deleteRecursively()
 
     /**
      * A delegate to org.junit.rules.TemporaryFolder.TemporaryFolder.newFolder(String).
      */
-    fun newFolder(folder: String): File = tempFolder.newFolder(folder)
+    fun newFolder(folderName: String): File {
+        val dir = File(tempFolder, folderName)
+        if (!dir.mkdir()) {
+            throw IOException("Unable to create temporary directory $dir.")
+        }
+        return dir
+    }
 
     /**
      * Delete all files under the root temporary folder recursively. The folders are preserved.
      */
     fun deleteAllFiles() {
-        Files.walkFileTree(tempFolder.root.toPath(), object : SimpleFileVisitor<Path>() {
+        Files.walkFileTree(tempFolder.toPath(), object : SimpleFileVisitor<Path>() {
             @Throws(IOException::class)
             override fun visitFile(file: Path?, attrs: BasicFileAttributes?): FileVisitResult {
                 file?.toFile()?.delete()
diff --git a/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/MarkedSlf4jNotifier.kt b/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/MarkedSlf4jNotifier.kt
new file mode 100644 (file)
index 0000000..13ebd9e
--- /dev/null
@@ -0,0 +1,43 @@
+/*-
+ * ============LICENSE_START=======================================================
+ *  Copyright (C) 2019 Nordix Foundation.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+package org.onap.ccsdk.cds.blueprintsprocessor.uat
+
+import com.github.tomakehurst.wiremock.common.Notifier
+import org.slf4j.LoggerFactory
+import org.slf4j.Marker
+
+class MarkedSlf4jNotifier(private val marker: Marker) : Notifier {
+
+    override fun info(message: String) {
+        log.info(marker, message)
+    }
+
+    override fun error(message: String) {
+        log.error(marker, message)
+    }
+
+    override fun error(message: String, t: Throwable) {
+        log.error(marker, message, t)
+    }
+
+    companion object {
+        private val log = LoggerFactory.getLogger("uat.WireMock")
+    }
+}
  * SPDX-License-Identifier: Apache-2.0
  * ============LICENSE_END=========================================================
  */
-package org.onap.ccsdk.cds.blueprintsprocessor
+package org.onap.ccsdk.cds.blueprintsprocessor.uat
 
 import org.springframework.context.ApplicationContextInitializer
 import org.springframework.context.ConfigurableApplicationContext
 import org.springframework.test.context.support.TestPropertySourceUtils
 import org.springframework.util.Base64Utils
-import java.nio.charset.StandardCharsets
 
 class TestSecuritySettings {
     companion object {
@@ -31,7 +30,7 @@ class TestSecuritySettings {
         private const val authPassword = "Heisenberg"
 
         fun clientAuthToken() =
-                "Basic " + Base64Utils.encodeToString("$authUsername:$authPassword".toByteArray(StandardCharsets.UTF_8))
+                "Basic " + Base64Utils.encodeToString("$authUsername:$authPassword".toByteArray())
     }
 
     class ServerContextInitializer : ApplicationContextInitializer<ConfigurableApplicationContext> {
diff --git a/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/UatServicesTest.kt b/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/UatServicesTest.kt
new file mode 100644 (file)
index 0000000..78dc709
--- /dev/null
@@ -0,0 +1,260 @@
+/*-
+ * ============LICENSE_START=======================================================
+ *  Copyright (C) 2019 Nordix Foundation.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+package org.onap.ccsdk.cds.blueprintsprocessor.uat
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.github.tomakehurst.wiremock.WireMockServer
+import com.github.tomakehurst.wiremock.client.MappingBuilder
+import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder
+import com.github.tomakehurst.wiremock.client.VerificationException
+import com.github.tomakehurst.wiremock.client.WireMock.aResponse
+import com.github.tomakehurst.wiremock.client.WireMock.equalTo
+import com.github.tomakehurst.wiremock.client.WireMock.equalToJson
+import com.github.tomakehurst.wiremock.client.WireMock.request
+import com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo
+import com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig
+import org.apache.http.HttpStatus
+import org.apache.http.client.methods.HttpPost
+import org.apache.http.entity.ContentType
+import org.apache.http.entity.mime.HttpMultipartMode
+import org.apache.http.entity.mime.MultipartEntityBuilder
+import org.apache.http.impl.client.CloseableHttpClient
+import org.apache.http.impl.client.HttpClientBuilder
+import org.apache.http.message.BasicHeader
+import org.hamcrest.CoreMatchers
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalToIgnoringCase
+import org.jetbrains.kotlin.konan.util.prefixIfNot
+import org.onap.ccsdk.cds.blueprintsprocessor.uat.logging.LogColor.COLOR_WIREMOCK
+import org.onap.ccsdk.cds.blueprintsprocessor.uat.logging.LogColor.markerOf
+import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintConstants.UAT_SPECIFICATION_FILE
+import org.onap.ccsdk.cds.controllerblueprints.core.utils.BluePrintArchiveUtils.Companion.compressToBytes
+import org.skyscreamer.jsonassert.JSONAssert
+import org.skyscreamer.jsonassert.JSONCompareMode
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.web.server.LocalServerPort
+import org.springframework.core.env.ConfigurableEnvironment
+import org.springframework.core.env.MapPropertySource
+import org.springframework.http.HttpHeaders
+import org.springframework.http.MediaType
+import org.springframework.test.context.ActiveProfiles
+import org.springframework.test.context.support.TestPropertySourceUtils.INLINED_PROPERTIES_PROPERTY_SOURCE_NAME
+import org.yaml.snakeyaml.Yaml
+import java.nio.file.Paths
+import kotlin.test.AfterTest
+import kotlin.test.BeforeTest
+import kotlin.test.Test
+import kotlin.test.assertNotNull
+
+@ActiveProfiles("uat")
+@Suppress("MemberVisibilityCanBePrivate")
+class UatServicesTest : BaseUatTest() {
+
+    companion object {
+        private const val BLUEPRINT_NAME = "pnf_config"
+        private val BLUEPRINT_BASE_DIR = Paths.get(UAT_BLUEPRINTS_BASE_DIR, BLUEPRINT_NAME)
+        private val UAT_PATH = BLUEPRINT_BASE_DIR.resolve(UAT_SPECIFICATION_FILE)
+        private val wireMockMarker = markerOf(COLOR_WIREMOCK)
+    }
+
+    @Autowired
+    lateinit var mapper: ObjectMapper
+
+    @Autowired
+    lateinit var environment: ConfigurableEnvironment
+
+    private val ephemeralProperties = mutableSetOf<String>()
+    private val startedMockServers = mutableListOf<WireMockServer>()
+
+    private fun setProperties(properties: Map<String, String>) {
+        inlinedPropertySource().putAll(properties)
+        ephemeralProperties += properties.keys
+    }
+
+    @AfterTest
+    fun resetProperties() {
+        val source = inlinedPropertySource()
+        ephemeralProperties.forEach { key -> source.remove(key) }
+        ephemeralProperties.clear()
+    }
+
+    @AfterTest
+    fun stopMockServers() {
+        startedMockServers.forEach { mockServer ->
+            try {
+                mockServer.checkForUnmatchedRequests()
+            } finally {
+                mockServer.stop()
+            }
+        }
+        startedMockServers.clear()
+    }
+
+    private fun inlinedPropertySource(): MutableMap<String, Any> =
+            (environment.propertySources[INLINED_PROPERTIES_PROPERTY_SOURCE_NAME] as MapPropertySource).source
+
+    @LocalServerPort
+    var localServerPort: Int = 0
+
+    // use lazy evaluation to postpone until localServerPort is injected by Spring
+    val baseUrl: String by lazy {
+        "http://127.0.0.1:$localServerPort"
+    }
+
+    lateinit var httpClient: CloseableHttpClient
+
+    @BeforeTest
+    fun setupHttpClient() {
+        val defaultHeaders = listOf(BasicHeader(org.apache.http.HttpHeaders.AUTHORIZATION,
+                TestSecuritySettings.clientAuthToken()))
+        httpClient = HttpClientBuilder.create()
+                .setDefaultHeaders(defaultHeaders)
+                .build()
+    }
+
+    @Test
+    fun `verify service validates candidate UAT`() {
+        // GIVEN
+        val cbaBytes = compressToBytes(BLUEPRINT_BASE_DIR)
+        val multipartEntity = MultipartEntityBuilder.create()
+                .setMode(HttpMultipartMode.BROWSER_COMPATIBLE)
+                .addBinaryBody("cba", cbaBytes, ContentType.DEFAULT_BINARY, "cba.zip")
+                .build()
+        val request = HttpPost("$baseUrl/api/v1/uat/verify").apply {
+            entity = multipartEntity
+        }
+
+        // WHEN
+        httpClient.execute(request) { response ->
+
+            // THEN
+            val statusLine = response.statusLine
+            assertThat(statusLine.statusCode, CoreMatchers.equalTo(HttpStatus.SC_OK))
+        }
+    }
+
+    @Test
+    fun `spy service generates complete UAT from bare UAT`() {
+        // GIVEN
+        val uatSpec = UAT_PATH.toFile().readText()
+        val fullUat = UatDefinition.load(mapper, uatSpec)
+        val expectedJson = mapper.writeValueAsString(fullUat)
+
+        val bareUatBytes = fullUat.toBare().dump(mapper).toByteArray()
+
+        fullUat.externalServices.forEach { service ->
+            val mockServer = createMockServer(service)
+            mockServer.start()
+            startedMockServers += mockServer
+            setPropertiesForMockServer(service, mockServer)
+        }
+
+        val cbaBytes = compressToBytes(BLUEPRINT_BASE_DIR)
+        val multipartEntity = MultipartEntityBuilder.create()
+                .setMode(HttpMultipartMode.BROWSER_COMPATIBLE)
+                .addBinaryBody("cba", cbaBytes, ContentType.DEFAULT_BINARY, "cba.zip")
+                .addBinaryBody("uat", bareUatBytes, ContentType.DEFAULT_BINARY, "uat.yaml")
+                .build()
+        val request = HttpPost("$baseUrl/api/v1/uat/spy").apply {
+            entity = multipartEntity
+        }
+
+        // WHEN
+        httpClient.execute(request) { response ->
+
+            // THEN
+            val statusLine = response.statusLine
+            assertThat(statusLine.statusCode, CoreMatchers.equalTo(HttpStatus.SC_OK))
+            val entity = response.entity
+            assertNotNull(entity)
+            val contentType = ContentType.get(entity)
+            assertThat(contentType.mimeType, equalToIgnoringCase("text/vnd.yaml"))
+            val yamlResponse = entity.content.bufferedReader().readText()
+            val jsonResponse = yamlToJson(yamlResponse)
+            JSONAssert.assertEquals(expectedJson, jsonResponse, JSONCompareMode.LENIENT)
+        }
+    }
+
+    private fun createMockServer(service: ServiceDefinition): WireMockServer {
+        val mockServer = WireMockServer(wireMockConfig()
+                .dynamicPort()
+                .notifier(MarkedSlf4jNotifier(wireMockMarker))
+        )
+        service.expectations.forEach { expectation ->
+
+            val request = expectation.request
+            val response = expectation.response
+            // WebTestClient always use absolute path, prefixing with "/" if necessary
+            val urlPattern = urlEqualTo(request.path.prefixIfNot("/"))
+            val mappingBuilder: MappingBuilder = request(request.method, urlPattern)
+            request.headers.forEach { (key, value) ->
+                mappingBuilder.withHeader(key, equalTo(value))
+            }
+            if (request.body != null) {
+                mappingBuilder.withRequestBody(equalToJson(mapper.writeValueAsString(request.body), true, true))
+            }
+
+            val responseDefinitionBuilder: ResponseDefinitionBuilder = aResponse()
+                    .withStatus(response.status)
+            if (response.body != null) {
+                responseDefinitionBuilder.withBody(mapper.writeValueAsBytes(response.body))
+                        .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
+            }
+
+            mappingBuilder.willReturn(responseDefinitionBuilder)
+
+            mockServer.stubFor(mappingBuilder)
+        }
+        return mockServer
+    }
+
+    private fun setPropertiesForMockServer(service: ServiceDefinition, mockServer: WireMockServer) {
+        val selector = service.selector
+        val httpPort = mockServer.port()
+        val properties = mapOf(
+                "blueprintsprocessor.restclient.$selector.type" to "basic-auth",
+                "blueprintsprocessor.restclient.$selector.url" to "http://localhost:$httpPort/",
+                // TODO credentials should be validated
+                "blueprintsprocessor.restclient.$selector.username" to "admin",
+                "blueprintsprocessor.restclient.$selector.password" to "Kp8bJ4SXszM0WXlhak3eHlcse2gAw84vaoGGmJvUy2U"
+        )
+        setProperties(properties)
+    }
+
+    /**
+     * Borrowed from com.github.tomakehurst.wiremock.junit.WireMockRule.checkForUnmatchedRequests
+     */
+    private fun WireMockServer.checkForUnmatchedRequests() {
+        val unmatchedRequests = findAllUnmatchedRequests()
+        if (unmatchedRequests.isNotEmpty()) {
+            val nearMisses = findNearMissesForAllUnmatchedRequests()
+            if (nearMisses.isEmpty()) {
+                throw VerificationException.forUnmatchedRequests(unmatchedRequests)
+            } else {
+                throw VerificationException.forUnmatchedNearMisses(nearMisses)
+            }
+        }
+    }
+
+    private fun yamlToJson(yaml: String): String {
+        val map: Map<String, Any> = Yaml().load(yaml)
+        return mapper.writeValueAsString(map)
+    }
+}
\ No newline at end of file
index 70d94f5..f635e79 100644 (file)
   -->
 
 <configuration>
-    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
-        <encoder>
-            <pattern>%d{HH:mm:ss.SSS} %-5level %-40.40logger{39} : %msg%n</pattern>
-        </encoder>
+    <appender name="SIFT" class="ch.qos.logback.classic.sift.SiftingAppender">
+        <discriminator class="org.onap.ccsdk.cds.blueprintsprocessor.uat.logging.SmartColorDiscriminator">
+            <defaultValue>white</defaultValue>
+        </discriminator>
+        <sift>
+            <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+                <encoder>
+                    <pattern>%${color}(%d{HH:mm:ss.SSS} %-5level %-40.40logger{39} : %msg%n)</pattern>
+                </encoder>
+            </appender>
+        </sift>
     </appender>
 
     <logger name="org.springframework.web.HttpLogging" level="trace"/>
     <logger name="org.hibernate.SQL" level="debug"/>
     <logger name="org.hibernate.type.descriptor.sql" level="trace"/>
 
+    <logger name="org.apache.http" level="debug"/>
+    <logger name="org.apache.http.wire" level="error"/>
+
     <root level="info">
-        <appender-ref ref="STDOUT"/>
+        <appender-ref ref="SIFT"/>
     </root>
 
 </configuration>
index 9fa13bd..384946a 100644 (file)
@@ -29,16 +29,32 @@ import org.springframework.stereotype.Service
 open class BluePrintRestLibPropertyService(private var bluePrintProperties:
                                            BluePrintProperties) {
 
-    open fun blueprintWebClientService(jsonNode: JsonNode):
-        BlueprintWebClientService {
-        val restClientProperties = restClientProperties(jsonNode)
-        return blueprintWebClientService(restClientProperties)
+    private var preInterceptor: PreInterceptor? = null
+    private var postInterceptor: PostInterceptor? = null
+
+    fun setInterceptors(preInterceptor: PreInterceptor?, postInterceptor: PostInterceptor?) {
+        this.preInterceptor = preInterceptor
+        this.postInterceptor = postInterceptor
+    }
+
+    fun clearInterceptors() {
+        this.preInterceptor = null
+        this.postInterceptor = null
+    }
+
+    open fun blueprintWebClientService(jsonNode: JsonNode): BlueprintWebClientService {
+        val service = preInterceptor?.getInstance(jsonNode)
+                ?: blueprintWebClientService(restClientProperties(jsonNode))
+        return postInterceptor?.getInstance(jsonNode, service) ?: service
     }
 
     open fun blueprintWebClientService(selector: String): BlueprintWebClientService {
-        val prefix = "blueprintsprocessor.restclient.$selector"
-        val restClientProperties = restClientProperties(prefix)
-        return blueprintWebClientService(restClientProperties)
+        val service = preInterceptor?.getInstance(selector) ?: run {
+            val prefix = "blueprintsprocessor.restclient.$selector"
+            val restClientProperties = restClientProperties(prefix)
+            blueprintWebClientService(restClientProperties)
+        }
+        return postInterceptor?.getInstance(selector, service) ?: service
     }
 
     fun restClientProperties(prefix: String): RestClientProperties {
@@ -182,6 +198,18 @@ open class BluePrintRestLibPropertyService(private var bluePrintProperties:
         return bluePrintProperties.propertyBeanType(
             prefix, PolicyManagerRestClientProperties::class.java)
     }
+
+    interface PreInterceptor {
+        fun getInstance(jsonNode: JsonNode): BlueprintWebClientService?
+
+        fun getInstance(selector: String): BlueprintWebClientService?
+    }
+
+    interface PostInterceptor {
+        fun getInstance(jsonNode: JsonNode, service: BlueprintWebClientService): BlueprintWebClientService
+
+        fun getInstance(selector: String, service: BlueprintWebClientService): BlueprintWebClientService
+    }
 }
 
 
index ba5815b..09350ac 100644 (file)
@@ -161,6 +161,8 @@ object BluePrintConstants {
     const val TOSCA_SCRIPTS_KOTLIN_DIR: String = "$TOSCA_SCRIPTS_DIR/kotlin"
     const val TOSCA_SCRIPTS_JYTHON_DIR: String = "$TOSCA_SCRIPTS_DIR/python"
 
+    const val UAT_SPECIFICATION_FILE = "Tests/uat.yaml"
+
     const val GRAPH_START_NODE_NAME = "START"
     const val GRAPH_END_NODE_NAME = "END"