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(
138 client, process.request,
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 requestBody: JsonNode,
235 expectedResponse: JsonNode?,
236 responseNormalizer: (String) -> String
238 val stringEntity = StringEntity(mapper.writeValueAsString(requestBody), 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 assertThat(statusLine.statusCode, equalTo(HttpStatus.SC_OK))
245 val entity = response.entity
246 assertThat("Response contains no content", entity, notNullValue())
247 entity.content.bufferedReader().use { it.readText() }
249 val actualResponse = responseNormalizer(response)
250 if (expectedResponse != null) {
251 assertJsonEquals(expectedResponse, actualResponse)
253 return mapper.readTree(actualResponse)!!
256 private fun evalVerificationMode(times: String): VerificationMode {
257 val matchResult = TIMES_SPEC_REGEX.matchEntire(times) ?: throw InvalidUatDefinition(
258 "Time specification '$times' does not follow expected format $TIMES_SPEC_REGEX"
260 val counter = matchResult.groups[2]!!.value.toInt()
261 return when (matchResult.groups[1]?.value) {
262 ">=" -> atLeast(counter)
263 ">" -> atLeast(counter + 1)
264 "<=" -> atMost(counter)
265 "<" -> atMost(counter - 1)
266 else -> times(counter)
270 @Throws(AssertionError::class)
271 private fun assertJsonEquals(expected: JsonNode?, actual: String): Boolean {
273 if ((expected == null) && actual.isBlank()) {
277 JSONAssert.assertEquals(expected?.toString(), actual, JSONCompareMode.LENIENT)
278 // assertEquals throws an exception whenever match fails
282 private fun localServerPort(): Int =
284 environment.getProperty("local.server.port")
285 ?: environment.getRequiredProperty("blueprint.httpPort")
288 private fun clientAuthToken(): String {
289 val username = environment.getRequiredProperty("security.user.name")
290 val password = environment.getRequiredProperty("security.user.password")
291 val plainPassword = when {
292 password.startsWith(NOOP_PASSWORD_PREFIX) -> password.substring(
293 NOOP_PASSWORD_PREFIX.length
297 return "Basic " + Base64Utils.encodeToString("$username:$plainPassword".toByteArray())
300 private class MockPreInterceptor : BlueprintRestLibPropertyService.PreInterceptor {
302 private val mocks = ConcurrentHashMap<String, BlueprintWebClientService>()
304 override fun getInstance(jsonNode: JsonNode): BlueprintWebClientService? {
305 TODO("jsonNode-keyed services not yet supported")
308 override fun getInstance(selector: String): BlueprintWebClientService? =
311 fun registerMock(selector: String, client: BlueprintWebClientService) {
312 mocks[selector] = client
316 private class SpyPostInterceptor(private val mapper: ObjectMapper) : BlueprintRestLibPropertyService.PostInterceptor {
318 private val spies = ConcurrentHashMap<String, SpyService>()
320 override fun getInstance(jsonNode: JsonNode, service: BlueprintWebClientService): BlueprintWebClientService {
321 TODO("jsonNode-keyed services not yet supported")
324 override fun getInstance(selector: String, service: BlueprintWebClientService): BlueprintWebClientService {
325 val spiedService = SpyService(mapper, selector, service)
326 spies[selector] = spiedService
330 fun getSpies(): List<SpyService> =
331 spies.values.toList()
334 private class SpyService(
335 private val mapper: ObjectMapper,
336 val selector: String,
337 private val realService: BlueprintWebClientService
339 BlueprintWebClientService by realService {
341 private val expectations: MutableList<ExpectationDefinition> = mutableListOf()
343 override fun exchangeResource(methodType: String, path: String, request: String): WebClientResponse<String> =
345 methodType, path, request,
349 override fun exchangeResource(
353 headers: Map<String, String>
354 ): WebClientResponse<String> {
355 val requestDefinition =
356 RequestDefinition(methodType, path, headers, toJson(request))
357 val realAnswer = realService.exchangeResource(methodType, path, request, headers)
358 val responseBody = when {
359 // TODO: confirm if we need to normalize the response here
360 realAnswer.status == HttpStatus.SC_OK -> toJson(realAnswer.body)
363 val responseDefinition =
364 ResponseDefinition(realAnswer.status, responseBody)
366 ExpectationDefinition(
374 override suspend fun <T> retry(times: Int, initialDelay: Long, delay: Long, block: suspend (Int) -> T): T {
375 return super.retry(times, initialDelay, delay, block)
378 fun asServiceDefinition() =
379 ServiceDefinition(selector, expectations)
381 private fun toJson(str: String): JsonNode? {
383 str.isNotBlank() -> mapper.readTree(str)
390 private val DEFAULT_HEADERS = mapOf(
391 HttpHeaders.CONTENT_TYPE to MediaType.APPLICATION_JSON_VALUE,
392 HttpHeaders.ACCEPT to MediaType.APPLICATION_JSON_VALUE