2 * ============LICENSE_START=======================================================
3 * Copyright (C) 2019 Nordix Foundation.
4 * ================================================================================
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
9 * http://www.apache.org/licenses/LICENSE-2.0
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
17 * SPDX-License-Identifier: Apache-2.0
18 * ============LICENSE_END=========================================================
20 package org.onap.ccsdk.cds.blueprintsprocessor.uat.utils
22 import com.fasterxml.jackson.databind.JsonNode
23 import com.fasterxml.jackson.databind.ObjectMapper
24 import com.nhaarman.mockitokotlin2.any
25 import com.nhaarman.mockitokotlin2.argThat
26 import com.nhaarman.mockitokotlin2.atLeast
27 import com.nhaarman.mockitokotlin2.atMost
28 import com.nhaarman.mockitokotlin2.eq
29 import com.nhaarman.mockitokotlin2.mock
30 import com.nhaarman.mockitokotlin2.times
31 import com.nhaarman.mockitokotlin2.verify
32 import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions
33 import com.nhaarman.mockitokotlin2.whenever
34 import org.apache.http.HttpHeaders
35 import org.apache.http.HttpStatus
36 import org.apache.http.client.HttpClient
37 import org.apache.http.client.methods.HttpPost
38 import org.apache.http.entity.ContentType
39 import org.apache.http.entity.StringEntity
40 import org.apache.http.entity.mime.HttpMultipartMode
41 import org.apache.http.entity.mime.MultipartEntityBuilder
42 import org.apache.http.impl.client.HttpClientBuilder
43 import org.apache.http.message.BasicHeader
44 import org.hamcrest.CoreMatchers.equalTo
45 import org.hamcrest.CoreMatchers.notNullValue
46 import org.hamcrest.MatcherAssert.assertThat
47 import org.mockito.Answers
48 import org.mockito.verification.VerificationMode
49 import org.onap.ccsdk.cds.blueprintsprocessor.rest.service.BluePrintRestLibPropertyService
50 import org.onap.ccsdk.cds.blueprintsprocessor.rest.service.BlueprintWebClientService
51 import org.onap.ccsdk.cds.blueprintsprocessor.rest.service.BlueprintWebClientService.WebClientResponse
52 import org.onap.ccsdk.cds.blueprintsprocessor.uat.logging.LogColor.COLOR_MOCKITO
53 import org.onap.ccsdk.cds.blueprintsprocessor.uat.logging.LogColor.markerOf
54 import org.onap.ccsdk.cds.blueprintsprocessor.uat.logging.MockInvocationLogger
55 import org.skyscreamer.jsonassert.JSONAssert
56 import org.skyscreamer.jsonassert.JSONCompareMode
57 import org.slf4j.Logger
58 import org.slf4j.LoggerFactory
59 import org.springframework.core.env.ConfigurableEnvironment
60 import org.springframework.http.MediaType
61 import org.springframework.stereotype.Component
62 import org.springframework.util.Base64Utils
63 import java.util.concurrent.ConcurrentHashMap
68 * - Application HTTP service is bound to loopback interface;
69 * - Password is either defined in plain (with "{noop}" prefix), or it's the same of username.
71 * @author Eliezio Oliveira
75 private val environment: ConfigurableEnvironment,
76 private val restClientFactory: BluePrintRestLibPropertyService,
77 private val mapper: ObjectMapper
82 private const val NOOP_PASSWORD_PREFIX = "{noop}"
83 private const val PROPERTY_IN_UAT = "IN_UAT"
84 private val TIMES_SPEC_REGEX = "([<>]=?)?\\s*(\\d+)".toRegex()
85 private val log: Logger = LoggerFactory.getLogger(UatExecutor::class.java)
86 private val mockLoggingListener = MockInvocationLogger(markerOf(COLOR_MOCKITO))
89 // use lazy evaluation to postpone until localServerPort is injected by Spring
90 private val baseUrl: String by lazy {
91 "http://127.0.0.1:${localServerPort()}"
94 @Throws(AssertionError::class)
95 fun execute(uatSpec: String, cbaBytes: ByteArray) {
96 val uat = UatDefinition.load(mapper, uatSpec)
97 execute(uat, cbaBytes)
102 * The UAT can range from minimum to completely defined.
104 * @return an updated UAT with all NB and SB messages.
106 @Throws(AssertionError::class)
107 fun execute(uat: UatDefinition, cbaBytes: ByteArray): UatDefinition {
108 val defaultHeaders = listOf(BasicHeader(HttpHeaders.AUTHORIZATION, clientAuthToken()))
109 val httpClient = HttpClientBuilder.create()
110 .setDefaultHeaders(defaultHeaders)
112 // Only if externalServices are defined
113 val mockInterceptor = MockPreInterceptor()
114 // Always defined and used, whatever the case
115 val spyInterceptor = SpyPostInterceptor(mapper)
116 restClientFactory.setInterceptors(mockInterceptor, spyInterceptor)
119 // Configure mocked external services and save their expectations for further validation
120 val expectationsPerClient = uat.externalServices.associateBy(
122 createRestClientMock(service.expectations).also { restClient ->
123 // side-effect: register restClient to override real instance
124 mockInterceptor.registerMock(service.selector, restClient)
127 { service -> service.expectations }
130 val newProcesses = httpClient.use { client ->
131 uploadBlueprint(client, cbaBytes)
134 uat.processes.map { process ->
135 log.info("Executing process '${process.name}'")
136 val responseNormalizer = JsonNormalizer.getNormalizer(mapper, process.responseNormalizerSpec)
137 val actualResponse = processBlueprint(
139 process.expectedResponse, responseNormalizer
145 process.responseNormalizerSpec
150 // Validate requests to external services
151 for ((mockClient, expectations) in expectationsPerClient) {
152 expectations.forEach { expectation ->
153 val request = expectation.request
154 verify(mockClient, evalVerificationMode(expectation.times)).exchangeResource(
157 argThat { assertJsonEquals(request.body, this) },
158 argThat(RequiredMapEntriesMatcher(request.headers))
161 // Don't mind the invocations to the overloaded exchangeResource(String, String, String)
162 verify(mockClient, atLeast(0)).exchangeResource(any(), any(), any())
163 verifyNoMoreInteractions(mockClient)
166 val newExternalServices = spyInterceptor.getSpies()
167 .map(SpyService::asServiceDefinition)
169 return UatDefinition(newProcesses, newExternalServices)
171 restClientFactory.clearInterceptors()
176 private fun markUatBegin() {
177 System.setProperty(PROPERTY_IN_UAT, "1")
180 private fun markUatEnd() {
181 System.clearProperty(PROPERTY_IN_UAT)
184 private fun createRestClientMock(restExpectations: List<ExpectationDefinition>):
185 BlueprintWebClientService {
186 val restClient = mock<BlueprintWebClientService>(
187 defaultAnswer = Answers.RETURNS_SMART_NULLS,
188 // our custom verboseLogging handler
189 invocationListeners = arrayOf(mockLoggingListener)
192 // Delegates to overloaded exchangeResource(String, String, String, Map<String, String>)
193 whenever(restClient.exchangeResource(any(), any(), any()))
194 .thenAnswer { invocation ->
195 val method = invocation.arguments[0] as String
196 val path = invocation.arguments[1] as String
197 val request = invocation.arguments[2] as String
198 restClient.exchangeResource(method, path, request, emptyMap())
200 for (expectation in restExpectations) {
201 var stubbing = whenever(
202 restClient.exchangeResource(
203 eq(expectation.request.method),
204 eq(expectation.request.path),
209 for (response in expectation.responses) {
210 stubbing = stubbing.thenReturn(WebClientResponse(response.status, response.body.toString()))
216 @Throws(AssertionError::class)
217 private fun uploadBlueprint(client: HttpClient, cbaBytes: ByteArray) {
218 val multipartEntity = MultipartEntityBuilder.create()
219 .setMode(HttpMultipartMode.BROWSER_COMPATIBLE)
220 .addBinaryBody("file", cbaBytes, ContentType.DEFAULT_BINARY, "cba.zip")
222 val request = HttpPost("$baseUrl/api/v1/blueprint-model/publish").apply {
223 entity = multipartEntity
225 client.execute(request) { response ->
226 val statusLine = response.statusLine
227 assertThat(statusLine.statusCode, equalTo(HttpStatus.SC_OK))
231 @Throws(AssertionError::class)
232 private fun processBlueprint(
234 process: ProcessDefinition,
235 expectedResponse: JsonNode?,
236 responseNormalizer: (String) -> String
238 val stringEntity = StringEntity(mapper.writeValueAsString(process.request), ContentType.APPLICATION_JSON)
239 val request = HttpPost("$baseUrl/api/v1/execution-service/process").apply {
240 entity = stringEntity
242 val response = client.execute(request) { response ->
243 val statusLine = response.statusLine
244 val expectedCode = expectedResponse?.get("status")?.get("code")?.intValue()
245 assertThat("${process.name}", statusLine.statusCode, equalTo(expectedCode ?: HttpStatus.SC_OK))
246 val entity = response.entity
247 assertThat("${process.name} Response contains no content", entity, notNullValue())
248 entity.content.bufferedReader().use { it.readText() }
250 val actualResponse = responseNormalizer(response)
251 if (expectedResponse != null) {
252 assertJsonEquals(expectedResponse, actualResponse, process.name)
254 return mapper.readTree(actualResponse)!!
257 private fun evalVerificationMode(times: String): VerificationMode {
258 val matchResult = TIMES_SPEC_REGEX.matchEntire(times) ?: throw InvalidUatDefinition(
259 "Time specification '$times' does not follow expected format $TIMES_SPEC_REGEX"
261 val counter = matchResult.groups[2]!!.value.toInt()
262 return when (matchResult.groups[1]?.value) {
263 ">=" -> atLeast(counter)
264 ">" -> atLeast(counter + 1)
265 "<=" -> atMost(counter)
266 "<" -> atMost(counter - 1)
267 else -> times(counter)
271 @Throws(AssertionError::class)
272 private fun assertJsonEquals(expected: JsonNode?, actual: String, msg: String = ""): Boolean {
274 if ((expected == null) && actual.isBlank()) {
278 JSONAssert.assertEquals(msg, expected?.toString(), actual, JSONCompareMode.LENIENT)
279 // assertEquals throws an exception whenever match fails
283 private fun localServerPort(): Int =
285 environment.getProperty("local.server.port")
286 ?: environment.getRequiredProperty("blueprint.httpPort")
289 private fun clientAuthToken(): String {
290 val username = environment.getRequiredProperty("security.user.name")
291 val password = environment.getRequiredProperty("security.user.password")
292 val plainPassword = when {
293 password.startsWith(NOOP_PASSWORD_PREFIX) -> password.substring(
294 NOOP_PASSWORD_PREFIX.length
298 return "Basic " + Base64Utils.encodeToString("$username:$plainPassword".toByteArray())
301 private class MockPreInterceptor : BluePrintRestLibPropertyService.PreInterceptor {
303 private val mocks = ConcurrentHashMap<String, BlueprintWebClientService>()
305 override fun getInstance(jsonNode: JsonNode): BlueprintWebClientService? {
306 TODO("jsonNode-keyed services not yet supported")
309 override fun getInstance(selector: String): BlueprintWebClientService? =
312 fun registerMock(selector: String, client: BlueprintWebClientService) {
313 mocks[selector] = client
317 private class SpyPostInterceptor(private val mapper: ObjectMapper) : BluePrintRestLibPropertyService.PostInterceptor {
319 private val spies = ConcurrentHashMap<String, SpyService>()
321 override fun getInstance(jsonNode: JsonNode, service: BlueprintWebClientService): BlueprintWebClientService {
322 TODO("jsonNode-keyed services not yet supported")
325 override fun getInstance(selector: String, service: BlueprintWebClientService): BlueprintWebClientService {
326 val spiedService = SpyService(mapper, selector, service)
327 spies[selector] = spiedService
331 fun getSpies(): List<SpyService> =
332 spies.values.toList()
335 private class SpyService(
336 private val mapper: ObjectMapper,
337 val selector: String,
338 private val realService: BlueprintWebClientService
340 BlueprintWebClientService by realService {
342 private val expectations: MutableList<ExpectationDefinition> = mutableListOf()
344 override fun exchangeResource(methodType: String, path: String, request: String): WebClientResponse<String> =
346 methodType, path, request,
350 override fun exchangeResource(
354 headers: Map<String, String>
355 ): WebClientResponse<String> {
356 val requestDefinition =
357 RequestDefinition(methodType, path, headers, toJson(request))
358 val realAnswer = realService.exchangeResource(methodType, path, request, headers)
359 val responseBody = when {
360 // TODO: confirm if we need to normalize the response here
361 realAnswer.status == HttpStatus.SC_OK -> toJson(realAnswer.body)
364 val responseDefinition =
365 ResponseDefinition(realAnswer.status, responseBody)
367 ExpectationDefinition(
375 override suspend fun <T> retry(times: Int, initialDelay: Long, delay: Long, block: suspend (Int) -> T): T {
376 return super.retry(times, initialDelay, delay, block)
379 fun asServiceDefinition() =
380 ServiceDefinition(selector, expectations)
382 private fun toJson(str: String): JsonNode? {
384 str.isNotBlank() -> mapper.readTree(str)
391 private val DEFAULT_HEADERS = mapOf(
392 HttpHeaders.CONTENT_TYPE to MediaType.APPLICATION_JSON_VALUE,
393 HttpHeaders.ACCEPT to MediaType.APPLICATION_JSON_VALUE