diff options
author | ebo <eliezio.oliveira@est.tech> | 2019-09-30 13:23:43 +0100 |
---|---|---|
committer | ebo <eliezio.oliveira@est.tech> | 2019-10-07 21:13:19 +0100 |
commit | 3d962090dad9de06cf854fab8daceaf18e0ec469 (patch) | |
tree | f271d54e091024bfe0ee100bc45b8d0cea5203fb /ms/blueprintsprocessor/application/src/main | |
parent | 809993fe658026f4cb953e49ab0331e2d4e5c8f8 (diff) |
Implemented UAT runtime services
Issue-ID: CCSDK-1759
Change-Id: I9e3df315074bfcfa5e341feefdabd00671194dc3
Signed-off-by: ebo <eliezio.oliveira@est.tech>
Diffstat (limited to 'ms/blueprintsprocessor/application/src/main')
12 files changed, 945 insertions, 0 deletions
diff --git a/ms/blueprintsprocessor/application/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/JsonNormalizer.kt b/ms/blueprintsprocessor/application/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/JsonNormalizer.kt new file mode 100644 index 000000000..1a625c279 --- /dev/null +++ b/ms/blueprintsprocessor/application/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/JsonNormalizer.kt @@ -0,0 +1,78 @@ +/*- + * ============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.fasterxml.jackson.databind.node.ContainerNode +import com.fasterxml.jackson.databind.node.ObjectNode +import com.schibsted.spt.data.jslt.Parser + +internal class JsonNormalizer { + + companion object { + + fun getNormalizer(mapper: ObjectMapper, jsltSpec: JsonNode?): (String) -> String { + if (jsltSpec == null) { + 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/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/MoreMatchers.kt b/ms/blueprintsprocessor/application/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/MoreMatchers.kt new file mode 100644 index 000000000..163544fc9 --- /dev/null +++ b/ms/blueprintsprocessor/application/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/MoreMatchers.kt @@ -0,0 +1,34 @@ +/*- + * ============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.google.common.collect.Maps +import org.mockito.ArgumentMatcher + +class RequiredMapEntriesMatcher<K, V>(private val requiredEntries: Map<K, V>) : ArgumentMatcher<Map<K, V>> { + override fun matches(argument: Map<K, V>?): Boolean { + val missingEntries = Maps.difference(requiredEntries, argument).entriesOnlyOnLeft() + return missingEntries.isEmpty() + } + + override fun toString(): String { + return requiredEntries.toString() + } +} diff --git a/ms/blueprintsprocessor/application/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/PathDeserializer.kt b/ms/blueprintsprocessor/application/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/PathDeserializer.kt new file mode 100644 index 000000000..6b1b0c676 --- /dev/null +++ b/ms/blueprintsprocessor/application/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/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.uat + +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.deser.std.StdDeserializer + +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) + } + + /** + * 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/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/UatDefinition.kt b/ms/blueprintsprocessor/application/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/UatDefinition.kt new file mode 100644 index 000000000..3046f1041 --- /dev/null +++ b/ms/blueprintsprocessor/application/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/UatDefinition.kt @@ -0,0 +1,92 @@ +/*- + * ============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.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.module.kotlin.convertValue +import org.yaml.snakeyaml.DumperOptions +import org.yaml.snakeyaml.Yaml +import org.yaml.snakeyaml.nodes.Tag + +@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? = null) + +@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()) { + + 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 index 000000000..6678075bd --- /dev/null +++ b/ms/blueprintsprocessor/application/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/UatExecutor.kt @@ -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 index 000000000..f133fd7c7 --- /dev/null +++ b/ms/blueprintsprocessor/application/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/UatServices.kt @@ -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 diff --git a/ms/blueprintsprocessor/application/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/logging/ColorMarker.kt b/ms/blueprintsprocessor/application/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/logging/ColorMarker.kt new file mode 100644 index 000000000..10139c839 --- /dev/null +++ b/ms/blueprintsprocessor/application/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/logging/ColorMarker.kt @@ -0,0 +1,24 @@ +/*- + * ============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.Marker + +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 index 000000000..dce516933 --- /dev/null +++ b/ms/blueprintsprocessor/application/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/logging/LogColor.kt @@ -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 index 000000000..f8e6bd486 --- /dev/null +++ b/ms/blueprintsprocessor/application/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/logging/MockInvocationLogger.kt @@ -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 index 000000000..d7b38d3fa --- /dev/null +++ b/ms/blueprintsprocessor/application/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/logging/SmartColorDiscriminator.kt @@ -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 index 000000000..de2cf4e52 --- /dev/null +++ b/ms/blueprintsprocessor/application/src/main/resources/application-local.yml @@ -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 index 000000000..f00d62b0f --- /dev/null +++ b/ms/blueprintsprocessor/application/src/main/resources/application-uat.yml @@ -0,0 +1,4 @@ +server: + error: + include-exception: true + include-stacktrace: always |