From ff445e6d950048c092c0a7ccd8a0a04cb18869b5 Mon Sep 17 00:00:00 2001
From: ebo <eliezio.oliveira@est.tech>
Date: Fri, 16 Aug 2019 13:18:47 +0000
Subject: Fixed validation of resource-assignment-params.config-assign

- In order to preserve the validation of the whole BPP response, I added a
  normalization step that will convert any non-JSON part of the response
  into a JSON-equivalent representation.
  This normalization is carried out by the JSLT library that provides many
  JSON manipulations using a JSON-like specification.
  See https://github.com/schibsted/jslt for more details.

- Fix UAT not being run by maven
  The Surefire plugin does accept '*Tests.java' but DOESN'T '*Tests.kt'!
  The UAT test class was renamed to use the 'Test' suffix only.

- Improved maintainability of UAT-Engine by switching from explicitly field
  handling to POJO-based parsing using Jackson along with SnakeYaml.
  UAT-related POJOs created on new module UatDefinition.kt

- Added a Protobuf-like description of an UAT YAML document.

Change-Id: Id1178489caa4e97808747a99bc9324fc84e9b96e
Issue-ID: CCSDK-1620
Signed-off-by: ebo <eliezio.oliveira@est.tech>
---
 .../BlueprintsAcceptanceTest.kt                    | 240 ++++++++++++++++++
 .../BlueprintsAcceptanceTests.kt                   | 280 ---------------------
 .../blueprintsprocessor/ExtendedTemporaryFolder.kt |  19 ++
 .../cds/blueprintsprocessor/JsonNormalizer.kt      |  79 ++++++
 .../cds/blueprintsprocessor/PathDeserializer.kt    |  52 ++++
 .../ccsdk/cds/blueprintsprocessor/UatDefinition.kt |  68 +++++
 6 files changed, 458 insertions(+), 280 deletions(-)
 create mode 100644 ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/BlueprintsAcceptanceTest.kt
 delete mode 100644 ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/BlueprintsAcceptanceTests.kt
 create mode 100644 ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/JsonNormalizer.kt
 create mode 100644 ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/PathDeserializer.kt
 create mode 100644 ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/UatDefinition.kt

(limited to 'ms/blueprintsprocessor/application/src/test')

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
new file mode 100644
index 000000000..ad4173c9e
--- /dev/null
+++ b/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/BlueprintsAcceptanceTest.kt
@@ -0,0 +1,240 @@
+/*-
+ * ============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.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)
+    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
+        val expectationPerClient = uat.externalServices.associateBy(
+                { service -> createRestClientMock(service.selector, service.expectations) },
+                { service -> service.expectations }
+        )
+
+        // Run processes
+        for (process in uat.processes) {
+            log.info("Executing process '${process.name}'")
+            processBlueprint(process.request, process.expectedResponse,
+                    JsonNormalizer.getNormalizer(mapper, process.responseNormalizerSpec))
+        }
+
+        // Validate request payloads to external services
+        for ((mockClient, expectations) in expectationPerClient) {
+            expectations.forEach { expectation ->
+                verify(mockClient, atLeastOnce()).exchangeResource(
+                        eq(expectation.request.method),
+                        eq(expectation.request.path),
+                        argThat { assertJsonEqual(expectation.request.body, this) },
+                        expectation.request.requestHeadersMatcher())
+            }
+            // 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)
+
+        // 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/execution-service/upload")
+                .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/BlueprintsAcceptanceTests.kt b/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/BlueprintsAcceptanceTests.kt
deleted file mode 100644
index 0a57277ea..000000000
--- a/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/BlueprintsAcceptanceTests.kt
+++ /dev/null
@@ -1,280 +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.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.junit.ClassRule
-import org.junit.Rule
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-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.WebTestClient
-import org.yaml.snakeyaml.Yaml
-import reactor.core.publisher.Mono
-import java.io.File
-import java.nio.file.Path
-import java.nio.file.Paths
-import kotlin.test.BeforeTest
-import kotlin.test.Test
-
-@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"])
-@Suppress("UNCHECKED_CAST")
-class BlueprintsAcceptanceTests(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(BlueprintsAcceptanceTests::class.java)
-
-        @Parameterized.Parameters(name = "{index} {0}")
-        @JvmStatic
-        fun filenames(): 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)
-    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 yaml: Map<String, *> = loadYaml(Paths.get(filename, EMBEDDED_UAT_FILE))
-
-        uploadBlueprint(blueprintName)
-
-        // Configure mocked external services
-        val services = yaml["external-services"] as List<Map<String, *>>? ?: emptyList()
-        val expectationPerClient = services.map { service ->
-            val selector = service["selector"] as String
-            val expectations = (service["expectations"] as List<Map<String, *>>).map {
-                parseExpectation(it)
-            }
-            val mockClient = createRestClientMock(selector, expectations)
-            mockClient to expectations
-        }.toMap()
-
-        // Run processes
-        for (process in (yaml["processes"] as List<Map<String, *>>)) {
-            val processName = process["name"]
-            log.info("Executing process '$processName'")
-            val request = mapper.writeValueAsString(process["request"])
-            val expectedResponse = mapper.writeValueAsString(process["expectedResponse"])
-            processBlueprint(request, expectedResponse)
-        }
-
-        // Validate request payloads
-        for ((mockClient, expectations) in expectationPerClient) {
-            expectations.forEach { expectation ->
-                verify(mockClient, atLeastOnce()).exchangeResource(
-                        eq(expectation.method),
-                        eq(expectation.path),
-                        argThat { assertJsonEqual(expectation.expectedRequestBody, this) },
-                        expectation.requestHeadersMatcher())
-            }
-            // 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<RestExpectation>): BlueprintWebClientService {
-        val restClient = mock<BlueprintWebClientService>(verboseLogging = true)
-
-        // 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.method),
-                    eq(expectation.path),
-                    any(),
-                    any()))
-                    .thenReturn(WebClientResponse(expectation.statusCode, expectation.responseBody))
-        }
-
-        whenever(restClientFactory.blueprintWebClientService(selector))
-                .thenReturn(restClient)
-        return restClient
-    }
-
-    private fun uploadBlueprint(blueprintName: String) {
-        val body = toMultiValueMap("file", getBlueprintAsResource(blueprintName))
-        webTestClient
-                .post()
-                .uri("/api/v1/execution-service/upload")
-                .header("Authorization", TestSecuritySettings.clientAuthToken())
-                .syncBody(body)
-                .exchange()
-                .expectStatus().isOk
-    }
-
-    private fun processBlueprint(request: String, expectedResponse: String) {
-        webTestClient
-                .post()
-                .uri("/api/v1/execution-service/process")
-                .header("Authorization", TestSecuritySettings.clientAuthToken())
-                .contentType(MediaType.APPLICATION_JSON_UTF8)
-                .body(Mono.just(request), String::class.java)
-                .exchange()
-                .expectStatus().isOk
-                .expectBody()
-                .json(expectedResponse)
-    }
-
-    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 loadYaml(path: Path): Map<String, Any> {
-        return path.toFile().reader().use { reader ->
-            Yaml().load(reader)
-        }
-    }
-
-    private fun assertJsonEqual(expected: Any, actual: String): Boolean {
-        if (actual != expected) {
-            // assertEquals throws an exception whenever match fails
-            JSONAssert.assertEquals(mapper.writeValueAsString(expected), actual, JSONCompareMode.LENIENT)
-        }
-        return true
-    }
-
-    private fun parseExpectation(expectation: Map<String, *>): RestExpectation {
-        val request = expectation["request"] as Map<String, Any>
-        val method = request["method"] as String
-        val path = joinPath(request.getValue("path"))
-        val contentType = request["content-type"] as String?
-        val requestBody = request.getOrDefault("body", "")
-
-        val response = expectation["response"] as Map<String, Any>? ?: emptyMap()
-        val status = response["status"] as Int? ?: 200
-        val responseBody = when (val body = response["body"] ?: "") {
-            is String -> body
-            else -> mapper.writeValueAsString(body)
-        }
-
-        return RestExpectation(method, path, contentType, requestBody, status, responseBody)
-    }
-
-    /**
-     * Join a multilevel lists of strings.
-     * Example: joinPath(listOf("a", listOf("b", "c"), "d")) will result in "a/b/c/d".
-     */
-    private fun joinPath(any: Any): String {
-        fun recursiveJoin(any: Any, sb: StringBuilder): StringBuilder {
-            when (any) {
-                is List<*> -> any.filterNotNull().forEach { recursiveJoin(it, sb) }
-                is String -> {
-                    if (sb.isNotEmpty()) {
-                        sb.append('/')
-                    }
-                    sb.append(any)
-                }
-                else -> throw IllegalArgumentException("Unsupported type: ${any.javaClass}")
-            }
-            return sb
-        }
-
-        return recursiveJoin(any, StringBuilder()).toString()
-    }
-
-    data class RestExpectation(val method: String, val path: String, val contentType: String?,
-                               val expectedRequestBody: Any,
-                               val statusCode: Int, val responseBody: String) {
-
-        fun requestHeadersMatcher(): Map<String, String> {
-            return if (contentType != null) eq(mapOf("Content-Type" to contentType)) else any()
-        }
-    }
-}
\ No newline at end of file
diff --git a/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ExtendedTemporaryFolder.kt b/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ExtendedTemporaryFolder.kt
index 4576f2761..3c517e6ac 100644
--- a/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ExtendedTemporaryFolder.kt
+++ b/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ExtendedTemporaryFolder.kt
@@ -1,3 +1,22 @@
+/*-
+ * ============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 org.junit.rules.TemporaryFolder
diff --git a/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/JsonNormalizer.kt b/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/JsonNormalizer.kt
new file mode 100644
index 000000000..69673f931
--- /dev/null
+++ b/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/JsonNormalizer.kt
@@ -0,0 +1,79 @@
+/*-
+ * ============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.ContainerNode
+import com.fasterxml.jackson.databind.node.MissingNode
+import com.fasterxml.jackson.databind.node.ObjectNode
+import com.schibsted.spt.data.jslt.Parser
+
+class JsonNormalizer {
+
+    companion object {
+
+        fun getNormalizer(mapper: ObjectMapper, jsltSpec: JsonNode): (String) -> String {
+            if (jsltSpec is MissingNode) {
+                return { it }
+            }
+            return { s: String ->
+                val input = mapper.readTree(s)
+                val expandedJstlSpec = expandJstlSpec(jsltSpec)
+                val jslt = Parser.compileString(expandedJstlSpec)
+                val output = jslt.apply(input)
+                output.toString()
+            }
+        }
+
+        /**
+         * Creates an extended JSTL spec by appending the "*: ." wildcard pattern to every inner JSON object, and
+         * removing the extra quotes added by the standard YAML/JSON converters on fields prefixed by "?".
+         *
+         * @param jstlSpec the JSTL spec as a structured JSON object.
+         * @return the string representation of the extended JSTL spec.
+         */
+        private fun expandJstlSpec(jstlSpec: JsonNode): String {
+            val extendedJstlSpec = updateObjectNodes(jstlSpec, "*", ".")
+            return extendedJstlSpec.toString()
+                    // Handle the "?" as a prefix to literal/non-quoted values
+                    .replace("\"\\?([^\"]+)\"".toRegex(), "$1")
+                    // Also, remove the quotes added by Jackson for key and value of the wildcard matcher
+                    .replace("\"([.*])\"".toRegex(), "$1")
+        }
+
+        /**
+         * Expands a structured JSON object, by adding the given key and value to every nested ObjectNode.
+         *
+         * @param jsonNode the root node.
+         * @param fieldName the fixed field name.
+         * @param fieldValue the fixed field value.
+         */
+        private fun updateObjectNodes(jsonNode: JsonNode, fieldName: String, fieldValue: String): JsonNode {
+            if (jsonNode is ContainerNode<*>) {
+                (jsonNode as? ObjectNode)?.put(fieldName, fieldValue)
+                jsonNode.forEach { child ->
+                    updateObjectNodes(child, fieldName, fieldValue)
+                }
+            }
+            return jsonNode
+        }
+    }
+}
diff --git a/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/PathDeserializer.kt b/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/PathDeserializer.kt
new file mode 100644
index 000000000..1a232f2d3
--- /dev/null
+++ b/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/PathDeserializer.kt
@@ -0,0 +1,52 @@
+/*-
+ * ============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.core.JsonParser
+import com.fasterxml.jackson.databind.DeserializationContext
+import com.fasterxml.jackson.databind.deser.std.StdDeserializer
+
+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)
+    }
+
+    /**
+     * Join a multilevel lists of strings.
+     * Example: flatJoin(listOf("a", listOf("b", "c"), "d")) will result in "a/b/c/d".
+     */
+    private fun flatJoin(path: Any): String {
+        fun flatJoinTo(sb: StringBuilder, path: Any): StringBuilder {
+            when (path) {
+                is List<*> -> path.filterNotNull().forEach { flatJoinTo(sb, it) }
+                is String -> {
+                    if (sb.isNotEmpty()) {
+                        sb.append('/')
+                    }
+                    sb.append(path)
+                }
+                else -> throw IllegalArgumentException("Unsupported type: ${path.javaClass}")
+            }
+            return sb
+        }
+        return flatJoinTo(StringBuilder(), path).toString()
+    }
+}
\ No newline at end of file
diff --git a/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/UatDefinition.kt b/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/UatDefinition.kt
new file mode 100644
index 000000000..ce2061168
--- /dev/null
+++ b/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/UatDefinition.kt
@@ -0,0 +1,68 @@
+/*-
+ * ============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.annotation.JsonAlias
+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.nhaarman.mockitokotlin2.any
+import com.nhaarman.mockitokotlin2.eq
+import org.yaml.snakeyaml.Yaml
+import java.nio.file.Path
+
+data class ProcessDefinition(val name: String, val request: JsonNode, val expectedResponse: JsonNode,
+                             val responseNormalizerSpec: JsonNode = MissingNode.getInstance())
+
+data class RequestDefinition(val method: String,
+                             @JsonDeserialize(using = PathDeserializer::class)
+                             val path: String,
+                             @JsonAlias("content-type")
+                             val contentType: String? = null,
+                             val body: JsonNode = MissingNode.getInstance()) {
+    fun requestHeadersMatcher(): Map<String, String> {
+        return if (contentType != null) eq(mapOf("Content-Type" to contentType)) else any()
+    }
+}
+
+data class ResponseDefinition(val status: Int = 200, val body: JsonNode = MissingNode.getInstance()) {
+    companion object {
+        val DEFAULT_RESPONSE = ResponseDefinition()
+    }
+}
+
+data class ExpectationDefinition(val request: RequestDefinition,
+                                 val response: ResponseDefinition = ResponseDefinition.DEFAULT_RESPONSE)
+
+data class ServiceDefinition(val selector: String, val expectations: List<ExpectationDefinition>)
+
+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)
+            }
+        }
+    }
+}
-- 
cgit