From 94ad509756f17e79c278e3cc2f87440009125cd1 Mon Sep 17 00:00:00 2001 From: gummar Date: Tue, 18 Feb 2020 18:54:44 +0000 Subject: Merge SW Upgrade Blueprint into PNF_AAI and create one UAT BP for PNF UAT: Add support to multiple responses for a single request Set property IN_UAT=1 during UAT execution so blueprints can tune their settings to values more suitable for testing (like timeouts) Add 'times' field to specify expected number of invocations Add UAT blueprint script for PNF SW Upgrade UC Add current thread check for Hazelcast distributed lock Resolve URI before returning Issue-ID: CCSDK-2091 Change-Id: Id256bad043488f88f1b60015ebf9ade4be607fa2 Signed-off-by: gummar --- .../src/main/resources/application-local.yml | 4 +- .../cds/blueprintsprocessor/uat/UatServicesTest.kt | 23 +-- .../uat/utils/InvalidUatDefinition.kt | 22 +++ .../blueprintsprocessor/uat/utils/UatDefinition.kt | 24 +++- .../blueprintsprocessor/uat/utils/UatExecutor.kt | 159 ++++++++++++--------- 5 files changed, 149 insertions(+), 83 deletions(-) create mode 100644 ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/utils/InvalidUatDefinition.kt (limited to 'ms/blueprintsprocessor/application/src') diff --git a/ms/blueprintsprocessor/application/src/main/resources/application-local.yml b/ms/blueprintsprocessor/application/src/main/resources/application-local.yml index de2cf4e52..f2843322c 100644 --- a/ms/blueprintsprocessor/application/src/main/resources/application-local.yml +++ b/ms/blueprintsprocessor/application/src/main/resources/application-local.yml @@ -38,10 +38,10 @@ blueprintsprocessor: remoteScriptCommand: enabled: true restclient: - sdncodl: + sdnc: password: Kp8bJ4SXszM0WXlhak3eHlcse2gAw84vaoGGmJvUy2U type: basic-auth - url: http://localhost:8282/ + url: http://localhost:8282 username: admin restconfEnabled: true controllerblueprints: 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 index aebda8c07..4e7d4ce40 100644 --- 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 @@ -30,6 +30,8 @@ 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 com.github.tomakehurst.wiremock.http.HttpHeader +import com.github.tomakehurst.wiremock.http.HttpHeaders import org.apache.http.HttpStatus import org.apache.http.client.methods.HttpPost import org.apache.http.entity.ContentType @@ -55,8 +57,6 @@ 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 @@ -211,7 +211,6 @@ class UatServicesTest : BaseUatTest() { 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) @@ -222,15 +221,19 @@ class UatServicesTest : BaseUatTest() { 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) + for (response in expectation.responses) { + val responseDefinitionBuilder: ResponseDefinitionBuilder = aResponse() + .withStatus(response.status) + if (response.body != null) { + responseDefinitionBuilder.withBody(mapper.writeValueAsBytes(response.body)) + .withHeaders(HttpHeaders( + response.headers.entries.map { e -> HttpHeader(e.key, e.value) })) + } + + // TODO: MockServer verification for multiple responses should be done using Wiremock scenarios + mappingBuilder.willReturn(responseDefinitionBuilder) } - mappingBuilder.willReturn(responseDefinitionBuilder) - mockServer.stubFor(mappingBuilder) } return mockServer diff --git a/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/utils/InvalidUatDefinition.kt b/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/utils/InvalidUatDefinition.kt new file mode 100644 index 000000000..4a756411f --- /dev/null +++ b/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/utils/InvalidUatDefinition.kt @@ -0,0 +1,22 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2020 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.utils + +class InvalidUatDefinition(message: String) : RuntimeException(message) diff --git a/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/utils/UatDefinition.kt b/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/utils/UatDefinition.kt index c45ac45c6..17b79f588 100644 --- a/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/utils/UatDefinition.kt +++ b/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/utils/UatDefinition.kt @@ -47,18 +47,30 @@ data class RequestDefinition( ) @JsonInclude(JsonInclude.Include.NON_EMPTY) -data class ResponseDefinition(val status: Int = 200, val body: JsonNode? = null) { +data class ResponseDefinition(val status: Int = 200, val body: JsonNode? = null, val headers: Map = mapOf("Content-Type" to "application/json")) { companion object { - val DEFAULT_RESPONSE = ResponseDefinition() + val DEFAULT_RESPONSES = listOf(ResponseDefinition()) } } @JsonInclude(JsonInclude.Include.NON_EMPTY) -data class ExpectationDefinition( +class ExpectationDefinition( val request: RequestDefinition, - val response: ResponseDefinition = ResponseDefinition.DEFAULT_RESPONSE -) + response: ResponseDefinition?, + responses: List? = null, + val times: String = ">= 1" +) { + val responses: List = resolveOneOrMany(response, responses, ResponseDefinition.DEFAULT_RESPONSES) + + companion object { + fun resolveOneOrMany(one: T?, many: List?, defaultMany: List): List = when { + many != null -> many + one != null -> listOf(one) + else -> defaultMany + } + } +} @JsonInclude(JsonInclude.Include.NON_EMPTY) data class ServiceDefinition(val selector: String, val expectations: List) @@ -97,6 +109,6 @@ data class UatDefinition( companion object { fun load(mapper: ObjectMapper, spec: String): UatDefinition = - mapper.convertValue(Yaml().load(spec), UatDefinition::class.java) + mapper.convertValue(Yaml().load(spec), UatDefinition::class.java) } } diff --git a/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/utils/UatExecutor.kt b/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/utils/UatExecutor.kt index a904fa9b6..d120e71d6 100644 --- a/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/utils/UatExecutor.kt +++ b/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/uat/utils/UatExecutor.kt @@ -24,9 +24,10 @@ 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.atMost import com.nhaarman.mockitokotlin2.eq import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.times import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions import com.nhaarman.mockitokotlin2.whenever @@ -44,6 +45,7 @@ import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.CoreMatchers.notNullValue import org.hamcrest.MatcherAssert.assertThat import org.mockito.Answers +import org.mockito.verification.VerificationMode 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 @@ -77,7 +79,8 @@ class UatExecutor( companion object { private const val NOOP_PASSWORD_PREFIX = "{noop}" - + private const val PROPERTY_IN_UAT = "IN_UAT" + private val TIMES_SPEC_REGEX = "([<>]=?)?\\s*(\\d+)".toRegex() private val log: Logger = LoggerFactory.getLogger(UatExecutor::class.java) private val mockLoggingListener = MockInvocationLogger(markerOf(COLOR_MOCKITO)) } @@ -103,23 +106,24 @@ class UatExecutor( fun execute(uat: UatDefinition, cbaBytes: ByteArray): UatDefinition { val defaultHeaders = listOf(BasicHeader(HttpHeaders.AUTHORIZATION, clientAuthToken())) val httpClient = HttpClientBuilder.create() - .setDefaultHeaders(defaultHeaders) - .build() + .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 } } + markUatBegin() + // Configure mocked external services and save their expectations for further validation + val expectationsPerClient = 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 } ) val newProcesses = httpClient.use { client -> @@ -130,26 +134,27 @@ class UatExecutor( log.info("Executing process '${process.name}'") val responseNormalizer = JsonNormalizer.getNormalizer(mapper, process.responseNormalizerSpec) val actualResponse = processBlueprint( - client, process.request, - process.expectedResponse, responseNormalizer + client, process.request, + process.expectedResponse, responseNormalizer ) ProcessDefinition( - process.name, - process.request, - actualResponse, - process.responseNormalizerSpec + 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)) + for ((mockClient, expectations) in expectationsPerClient) { + expectations.forEach { expectation -> + val request = expectation.request + verify(mockClient, evalVerificationMode(expectation.times)).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) @@ -158,40 +163,51 @@ class UatExecutor( } val newExternalServices = spyInterceptor.getSpies() - .map(SpyService::asServiceDefinition) + .map(SpyService::asServiceDefinition) return UatDefinition(newProcesses, newExternalServices) } finally { restClientFactory.clearInterceptors() + markUatEnd() } } + private fun markUatBegin() { + System.setProperty(PROPERTY_IN_UAT, "1") + } + + private fun markUatEnd() { + System.clearProperty(PROPERTY_IN_UAT) + } + private fun createRestClientMock(restExpectations: List): BlueprintWebClientService { val restClient = mock( - defaultAnswer = Answers.RETURNS_SMART_NULLS, - // our custom verboseLogging handler - invocationListeners = arrayOf(mockLoggingListener) + defaultAnswer = Answers.RETURNS_SMART_NULLS, + // our custom verboseLogging handler + invocationListeners = arrayOf(mockLoggingListener) ) // Delegates to overloaded exchangeResource(String, String, String, Map) 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()) - } + .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() - ) + var stubbing = whenever( + restClient.exchangeResource( + eq(expectation.request.method), + eq(expectation.request.path), + any(), + any() + ) ) - .thenReturn(WebClientResponse(expectation.response.status, expectation.response.body.toString())) + for (response in expectation.responses) { + stubbing = stubbing.thenReturn(WebClientResponse(response.status, response.body.toString())) + } } return restClient } @@ -199,9 +215,9 @@ class UatExecutor( @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() + .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 } @@ -236,6 +252,19 @@ class UatExecutor( return mapper.readTree(actualResponse)!! } + private fun evalVerificationMode(times: String): VerificationMode { + val matchResult = TIMES_SPEC_REGEX.matchEntire(times) ?: throw InvalidUatDefinition( + "Time specification '$times' does not follow expected format $TIMES_SPEC_REGEX") + val counter = matchResult.groups[2]!!.value.toInt() + return when (matchResult.groups[1]?.value) { + ">=" -> atLeast(counter) + ">" -> atLeast(counter + 1) + "<=" -> atMost(counter) + "<" -> atMost(counter - 1) + else -> times(counter) + } + } + @Throws(AssertionError::class) private fun assertJsonEquals(expected: JsonNode?, actual: String): Boolean { // special case @@ -249,15 +278,15 @@ class UatExecutor( } private fun localServerPort(): Int = - (environment.getProperty("local.server.port") - ?: environment.getRequiredProperty("blueprint.httpPort")).toInt() + (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) + NOOP_PASSWORD_PREFIX.length) else -> username } return "Basic " + Base64Utils.encodeToString("$username:$plainPassword".toByteArray()) @@ -271,7 +300,7 @@ class UatExecutor( } override fun getInstance(selector: String): BlueprintWebClientService? = - mocks[selector] + mocks[selector] fun registerMock(selector: String, client: BlueprintWebClientService) { mocks[selector] = client @@ -293,7 +322,7 @@ class UatExecutor( } fun getSpies(): List = - spies.values.toList() + spies.values.toList() } private class SpyService( @@ -301,14 +330,14 @@ class UatExecutor( val selector: String, private val realService: BlueprintWebClientService ) : - BlueprintWebClientService by realService { + BlueprintWebClientService by realService { private val expectations: MutableList = mutableListOf() override fun exchangeResource(methodType: String, path: String, request: String): WebClientResponse = - exchangeResource(methodType, path, request, - DEFAULT_HEADERS - ) + exchangeResource(methodType, path, request, + DEFAULT_HEADERS + ) override fun exchangeResource( methodType: String, @@ -317,7 +346,7 @@ class UatExecutor( headers: Map ): WebClientResponse { val requestDefinition = - RequestDefinition(methodType, path, headers, toJson(request)) + 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 @@ -325,12 +354,12 @@ class UatExecutor( else -> null } val responseDefinition = - ResponseDefinition(realAnswer.status, responseBody) + ResponseDefinition(realAnswer.status, responseBody) expectations.add( - ExpectationDefinition( - requestDefinition, - responseDefinition - ) + ExpectationDefinition( + requestDefinition, + responseDefinition + ) ) return realAnswer } @@ -340,7 +369,7 @@ class UatExecutor( } fun asServiceDefinition() = - ServiceDefinition(selector, expectations) + ServiceDefinition(selector, expectations) private fun toJson(str: String): JsonNode? { return when { @@ -351,8 +380,8 @@ class UatExecutor( companion object { private val DEFAULT_HEADERS = mapOf( - HttpHeaders.CONTENT_TYPE to MediaType.APPLICATION_JSON_VALUE, - HttpHeaders.ACCEPT to MediaType.APPLICATION_JSON_VALUE + HttpHeaders.CONTENT_TYPE to MediaType.APPLICATION_JSON_VALUE, + HttpHeaders.ACCEPT to MediaType.APPLICATION_JSON_VALUE ) } } -- cgit 1.2.3-korg