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
81 private const val NOOP_PASSWORD_PREFIX = "{noop}"
82 private const val PROPERTY_IN_UAT = "IN_UAT"
83 private val TIMES_SPEC_REGEX = "([<>]=?)?\\s*(\\d+)".toRegex()
84 private val log: Logger = LoggerFactory.getLogger(UatExecutor::class.java)
85 private val mockLoggingListener = MockInvocationLogger(markerOf(COLOR_MOCKITO))
88 // use lazy evaluation to postpone until localServerPort is injected by Spring
89 private val baseUrl: String by lazy {
90 "http://127.0.0.1:${localServerPort()}"
93 @Throws(AssertionError::class)
94 fun execute(uatSpec: String, cbaBytes: ByteArray) {
95 val uat = UatDefinition.load(mapper, uatSpec)
96 execute(uat, cbaBytes)
101 * The UAT can range from minimum to completely defined.
103 * @return an updated UAT with all NB and SB messages.
105 @Throws(AssertionError::class)
106 fun execute(uat: UatDefinition, cbaBytes: ByteArray): UatDefinition {
107 val defaultHeaders = listOf(BasicHeader(HttpHeaders.AUTHORIZATION, clientAuthToken()))
108 val httpClient = HttpClientBuilder.create()
109 .setDefaultHeaders(defaultHeaders)
111 // Only if externalServices are defined
112 val mockInterceptor = MockPreInterceptor()
113 // Always defined and used, whatever the case
114 val spyInterceptor = SpyPostInterceptor(mapper)
115 restClientFactory.setInterceptors(mockInterceptor, spyInterceptor)
118 // Configure mocked external services and save their expectations for further validation
119 val expectationsPerClient = uat.externalServices.associateBy(
121 createRestClientMock(service.expectations).also { restClient ->
122 // side-effect: register restClient to override real instance
123 mockInterceptor.registerMock(service.selector, restClient)
126 { service -> service.expectations }
129 val newProcesses = httpClient.use { client ->
130 uploadBlueprint(client, cbaBytes)
133 uat.processes.map { process ->
134 log.info("Executing process '${process.name}'")
135 val responseNormalizer = JsonNormalizer.getNormalizer(mapper, process.responseNormalizerSpec)
136 val actualResponse = processBlueprint(
137 client, process.request,
138 process.expectedResponse, responseNormalizer
144 process.responseNormalizerSpec
149 // Validate requests to external services
150 for ((mockClient, expectations) in expectationsPerClient) {
151 expectations.forEach { expectation ->
152 val request = expectation.request
153 verify(mockClient, evalVerificationMode(expectation.times)).exchangeResource(
156 argThat { assertJsonEquals(request.body, this) },
157 argThat(RequiredMapEntriesMatcher(request.headers))
160 // Don't mind the invocations to the overloaded exchangeResource(String, String, String)
161 verify(mockClient, atLeast(0)).exchangeResource(any(), any(), any())
162 verifyNoMoreInteractions(mockClient)
165 val newExternalServices = spyInterceptor.getSpies()
166 .map(SpyService::asServiceDefinition)
168 return UatDefinition(newProcesses, newExternalServices)
170 restClientFactory.clearInterceptors()
175 private fun markUatBegin() {
176 System.setProperty(PROPERTY_IN_UAT, "1")
179 private fun markUatEnd() {
180 System.clearProperty(PROPERTY_IN_UAT)
183 private fun createRestClientMock(restExpectations: List<ExpectationDefinition>):
184 BlueprintWebClientService {
185 val restClient = mock<BlueprintWebClientService>(
186 defaultAnswer = Answers.RETURNS_SMART_NULLS,
187 // our custom verboseLogging handler
188 invocationListeners = arrayOf(mockLoggingListener)
191 // Delegates to overloaded exchangeResource(String, String, String, Map<String, String>)
192 whenever(restClient.exchangeResource(any(), any(), any()))
193 .thenAnswer { invocation ->
194 val method = invocation.arguments[0] as String
195 val path = invocation.arguments[1] as String
196 val request = invocation.arguments[2] as String
197 restClient.exchangeResource(method, path, request, emptyMap())
199 for (expectation in restExpectations) {
200 var stubbing = whenever(
201 restClient.exchangeResource(
202 eq(expectation.request.method),
203 eq(expectation.request.path),
208 for (response in expectation.responses) {
209 stubbing = stubbing.thenReturn(WebClientResponse(response.status, response.body.toString()))
215 @Throws(AssertionError::class)
216 private fun uploadBlueprint(client: HttpClient, cbaBytes: ByteArray) {
217 val multipartEntity = MultipartEntityBuilder.create()
218 .setMode(HttpMultipartMode.BROWSER_COMPATIBLE)
219 .addBinaryBody("file", cbaBytes, ContentType.DEFAULT_BINARY, "cba.zip")
221 val request = HttpPost("$baseUrl/api/v1/blueprint-model/publish").apply {
222 entity = multipartEntity
224 client.execute(request) { response ->
225 val statusLine = response.statusLine
226 assertThat(statusLine.statusCode, equalTo(HttpStatus.SC_OK))
230 @Throws(AssertionError::class)
231 private fun processBlueprint(
233 requestBody: JsonNode,
234 expectedResponse: JsonNode?,
235 responseNormalizer: (String) -> String
237 val stringEntity = StringEntity(mapper.writeValueAsString(requestBody), ContentType.APPLICATION_JSON)
238 val request = HttpPost("$baseUrl/api/v1/execution-service/process").apply {
239 entity = stringEntity
241 val response = client.execute(request) { response ->
242 val statusLine = response.statusLine
243 assertThat(statusLine.statusCode, equalTo(HttpStatus.SC_OK))
244 val entity = response.entity
245 assertThat("Response contains no content", entity, notNullValue())
246 entity.content.bufferedReader().use { it.readText() }
248 val actualResponse = responseNormalizer(response)
249 if (expectedResponse != null) {
250 assertJsonEquals(expectedResponse, actualResponse)
252 return mapper.readTree(actualResponse)!!
255 private fun evalVerificationMode(times: String): VerificationMode {
256 val matchResult = TIMES_SPEC_REGEX.matchEntire(times) ?: throw InvalidUatDefinition(
257 "Time specification '$times' does not follow expected format $TIMES_SPEC_REGEX")
258 val counter = matchResult.groups[2]!!.value.toInt()
259 return when (matchResult.groups[1]?.value) {
260 ">=" -> atLeast(counter)
261 ">" -> atLeast(counter + 1)
262 "<=" -> atMost(counter)
263 "<" -> atMost(counter - 1)
264 else -> times(counter)
268 @Throws(AssertionError::class)
269 private fun assertJsonEquals(expected: JsonNode?, actual: String): Boolean {
271 if ((expected == null) && actual.isBlank()) {
275 JSONAssert.assertEquals(expected?.toString(), actual, JSONCompareMode.LENIENT)
276 // assertEquals throws an exception whenever match fails
280 private fun localServerPort(): Int =
281 (environment.getProperty("local.server.port")
282 ?: environment.getRequiredProperty("blueprint.httpPort")).toInt()
284 private fun clientAuthToken(): String {
285 val username = environment.getRequiredProperty("security.user.name")
286 val password = environment.getRequiredProperty("security.user.password")
287 val plainPassword = when {
288 password.startsWith(NOOP_PASSWORD_PREFIX) -> password.substring(
289 NOOP_PASSWORD_PREFIX.length)
292 return "Basic " + Base64Utils.encodeToString("$username:$plainPassword".toByteArray())
295 private class MockPreInterceptor : BluePrintRestLibPropertyService.PreInterceptor {
296 private val mocks = ConcurrentHashMap<String, BlueprintWebClientService>()
298 override fun getInstance(jsonNode: JsonNode): BlueprintWebClientService? {
299 TODO("jsonNode-keyed services not yet supported")
302 override fun getInstance(selector: String): BlueprintWebClientService? =
305 fun registerMock(selector: String, client: BlueprintWebClientService) {
306 mocks[selector] = client
310 private class SpyPostInterceptor(private val mapper: ObjectMapper) : BluePrintRestLibPropertyService.PostInterceptor {
312 private val spies = ConcurrentHashMap<String, SpyService>()
314 override fun getInstance(jsonNode: JsonNode, service: BlueprintWebClientService): BlueprintWebClientService {
315 TODO("jsonNode-keyed services not yet supported")
318 override fun getInstance(selector: String, service: BlueprintWebClientService): BlueprintWebClientService {
319 val spiedService = SpyService(mapper, selector, service)
320 spies[selector] = spiedService
324 fun getSpies(): List<SpyService> =
325 spies.values.toList()
328 private class SpyService(
329 private val mapper: ObjectMapper,
330 val selector: String,
331 private val realService: BlueprintWebClientService
333 BlueprintWebClientService by realService {
335 private val expectations: MutableList<ExpectationDefinition> = mutableListOf()
337 override fun exchangeResource(methodType: String, path: String, request: String): WebClientResponse<String> =
338 exchangeResource(methodType, path, request,
342 override fun exchangeResource(
346 headers: Map<String, String>
347 ): WebClientResponse<String> {
348 val requestDefinition =
349 RequestDefinition(methodType, path, headers, toJson(request))
350 val realAnswer = realService.exchangeResource(methodType, path, request, headers)
351 val responseBody = when {
352 // TODO: confirm if we need to normalize the response here
353 realAnswer.status == HttpStatus.SC_OK -> toJson(realAnswer.body)
356 val responseDefinition =
357 ResponseDefinition(realAnswer.status, responseBody)
359 ExpectationDefinition(
367 override suspend fun <T> retry(times: Int, initialDelay: Long, delay: Long, block: suspend (Int) -> T): T {
368 return super.retry(times, initialDelay, delay, block)
371 fun asServiceDefinition() =
372 ServiceDefinition(selector, expectations)
374 private fun toJson(str: String): JsonNode? {
376 str.isNotBlank() -> mapper.readTree(str)
382 private val DEFAULT_HEADERS = mapOf(
383 HttpHeaders.CONTENT_TYPE to MediaType.APPLICATION_JSON_VALUE,
384 HttpHeaders.ACCEPT to MediaType.APPLICATION_JSON_VALUE