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.atLeastOnce
28 import com.nhaarman.mockitokotlin2.eq
29 import com.nhaarman.mockitokotlin2.mock
30 import com.nhaarman.mockitokotlin2.verify
31 import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions
32 import com.nhaarman.mockitokotlin2.whenever
33 import org.apache.http.HttpHeaders
34 import org.apache.http.HttpStatus
35 import org.apache.http.client.HttpClient
36 import org.apache.http.client.methods.HttpPost
37 import org.apache.http.entity.ContentType
38 import org.apache.http.entity.StringEntity
39 import org.apache.http.entity.mime.HttpMultipartMode
40 import org.apache.http.entity.mime.MultipartEntityBuilder
41 import org.apache.http.impl.client.HttpClientBuilder
42 import org.apache.http.message.BasicHeader
43 import org.hamcrest.CoreMatchers.equalTo
44 import org.hamcrest.CoreMatchers.notNullValue
45 import org.hamcrest.MatcherAssert.assertThat
46 import org.mockito.Answers
47 import org.onap.ccsdk.cds.blueprintsprocessor.rest.service.BluePrintRestLibPropertyService
48 import org.onap.ccsdk.cds.blueprintsprocessor.rest.service.BlueprintWebClientService
49 import org.onap.ccsdk.cds.blueprintsprocessor.rest.service.BlueprintWebClientService.WebClientResponse
50 import org.onap.ccsdk.cds.blueprintsprocessor.uat.logging.LogColor.COLOR_MOCKITO
51 import org.onap.ccsdk.cds.blueprintsprocessor.uat.logging.LogColor.markerOf
52 import org.onap.ccsdk.cds.blueprintsprocessor.uat.logging.MockInvocationLogger
53 import org.skyscreamer.jsonassert.JSONAssert
54 import org.skyscreamer.jsonassert.JSONCompareMode
55 import org.slf4j.Logger
56 import org.slf4j.LoggerFactory
57 import org.springframework.core.env.ConfigurableEnvironment
58 import org.springframework.http.MediaType
59 import org.springframework.stereotype.Component
60 import org.springframework.util.Base64Utils
61 import java.util.concurrent.ConcurrentHashMap
66 * - Application HTTP service is bound to loopback interface;
67 * - Password is either defined in plain (with "{noop}" prefix), or it's the same of username.
69 * @author Eliezio Oliveira
73 private val environment: ConfigurableEnvironment,
74 private val restClientFactory: BluePrintRestLibPropertyService,
75 private val mapper: ObjectMapper
79 private const val NOOP_PASSWORD_PREFIX = "{noop}"
81 private val log: Logger = LoggerFactory.getLogger(UatExecutor::class.java)
82 private val mockLoggingListener = MockInvocationLogger(markerOf(COLOR_MOCKITO))
85 // use lazy evaluation to postpone until localServerPort is injected by Spring
86 private val baseUrl: String by lazy {
87 "http://127.0.0.1:${localServerPort()}"
90 @Throws(AssertionError::class)
91 fun execute(uatSpec: String, cbaBytes: ByteArray) {
92 val uat = UatDefinition.load(mapper, uatSpec)
93 execute(uat, cbaBytes)
98 * The UAT can range from minimum to completely defined.
100 * @return an updated UAT with all NB and SB messages.
102 @Throws(AssertionError::class)
103 fun execute(uat: UatDefinition, cbaBytes: ByteArray): UatDefinition {
104 val defaultHeaders = listOf(BasicHeader(HttpHeaders.AUTHORIZATION, clientAuthToken()))
105 val httpClient = HttpClientBuilder.create()
106 .setDefaultHeaders(defaultHeaders)
108 // Only if externalServices are defined
109 val mockInterceptor = MockPreInterceptor()
110 // Always defined and used, whatever the case
111 val spyInterceptor = SpyPostInterceptor(mapper)
112 restClientFactory.setInterceptors(mockInterceptor, spyInterceptor)
114 // Configure mocked external services and save their expected requests for further validation
115 val requestsPerClient = uat.externalServices.associateBy(
117 createRestClientMock(service.expectations).also { restClient ->
118 // side-effect: register restClient to override real instance
119 mockInterceptor.registerMock(service.selector, restClient)
122 { service -> service.expectations.map { it.request } }
125 val newProcesses = httpClient.use { client ->
126 uploadBlueprint(client, cbaBytes)
129 uat.processes.map { process ->
130 log.info("Executing process '${process.name}'")
131 val responseNormalizer = JsonNormalizer.getNormalizer(mapper, process.responseNormalizerSpec)
132 val actualResponse = processBlueprint(
133 client, process.request,
134 process.expectedResponse, responseNormalizer
140 process.responseNormalizerSpec
145 // Validate requests to external services
146 for ((mockClient, requests) in requestsPerClient) {
147 requests.forEach { request ->
148 verify(mockClient, atLeastOnce()).exchangeResource(
151 argThat { assertJsonEquals(request.body, this) },
152 argThat(RequiredMapEntriesMatcher(request.headers))
155 // Don't mind the invocations to the overloaded exchangeResource(String, String, String)
156 verify(mockClient, atLeast(0)).exchangeResource(any(), any(), any())
157 verifyNoMoreInteractions(mockClient)
160 val newExternalServices = spyInterceptor.getSpies()
161 .map(SpyService::asServiceDefinition)
163 return UatDefinition(newProcesses, newExternalServices)
165 restClientFactory.clearInterceptors()
169 private fun createRestClientMock(restExpectations: List<ExpectationDefinition>):
170 BlueprintWebClientService {
171 val restClient = mock<BlueprintWebClientService>(
172 defaultAnswer = Answers.RETURNS_SMART_NULLS,
173 // our custom verboseLogging handler
174 invocationListeners = arrayOf(mockLoggingListener)
177 // Delegates to overloaded exchangeResource(String, String, String, Map<String, String>)
178 whenever(restClient.exchangeResource(any(), any(), any()))
179 .thenAnswer { invocation ->
180 val method = invocation.arguments[0] as String
181 val path = invocation.arguments[1] as String
182 val request = invocation.arguments[2] as String
183 restClient.exchangeResource(method, path, request, emptyMap())
185 for (expectation in restExpectations) {
187 restClient.exchangeResource(
188 eq(expectation.request.method),
189 eq(expectation.request.path),
194 .thenReturn(WebClientResponse(expectation.response.status, expectation.response.body.toString()))
199 @Throws(AssertionError::class)
200 private fun uploadBlueprint(client: HttpClient, cbaBytes: ByteArray) {
201 val multipartEntity = MultipartEntityBuilder.create()
202 .setMode(HttpMultipartMode.BROWSER_COMPATIBLE)
203 .addBinaryBody("file", cbaBytes, ContentType.DEFAULT_BINARY, "cba.zip")
205 val request = HttpPost("$baseUrl/api/v1/blueprint-model/publish").apply {
206 entity = multipartEntity
208 client.execute(request) { response ->
209 val statusLine = response.statusLine
210 assertThat(statusLine.statusCode, equalTo(HttpStatus.SC_OK))
214 @Throws(AssertionError::class)
215 private fun processBlueprint(
217 requestBody: JsonNode,
218 expectedResponse: JsonNode?,
219 responseNormalizer: (String) -> String
221 val stringEntity = StringEntity(mapper.writeValueAsString(requestBody), ContentType.APPLICATION_JSON)
222 val request = HttpPost("$baseUrl/api/v1/execution-service/process").apply {
223 entity = stringEntity
225 val response = client.execute(request) { response ->
226 val statusLine = response.statusLine
227 assertThat(statusLine.statusCode, equalTo(HttpStatus.SC_OK))
228 val entity = response.entity
229 assertThat("Response contains no content", entity, notNullValue())
230 entity.content.bufferedReader().use { it.readText() }
232 val actualResponse = responseNormalizer(response)
233 if (expectedResponse != null) {
234 assertJsonEquals(expectedResponse, actualResponse)
236 return mapper.readTree(actualResponse)!!
239 @Throws(AssertionError::class)
240 private fun assertJsonEquals(expected: JsonNode?, actual: String): Boolean {
242 if ((expected == null) && actual.isBlank()) {
246 JSONAssert.assertEquals(expected?.toString(), actual, JSONCompareMode.LENIENT)
247 // assertEquals throws an exception whenever match fails
251 private fun localServerPort(): Int =
252 (environment.getProperty("local.server.port")
253 ?: environment.getRequiredProperty("blueprint.httpPort")).toInt()
255 private fun clientAuthToken(): String {
256 val username = environment.getRequiredProperty("security.user.name")
257 val password = environment.getRequiredProperty("security.user.password")
258 val plainPassword = when {
259 password.startsWith(NOOP_PASSWORD_PREFIX) -> password.substring(
260 NOOP_PASSWORD_PREFIX.length)
263 return "Basic " + Base64Utils.encodeToString("$username:$plainPassword".toByteArray())
266 private class MockPreInterceptor : BluePrintRestLibPropertyService.PreInterceptor {
267 private val mocks = ConcurrentHashMap<String, BlueprintWebClientService>()
269 override fun getInstance(jsonNode: JsonNode): BlueprintWebClientService? {
270 TODO("jsonNode-keyed services not yet supported")
273 override fun getInstance(selector: String): BlueprintWebClientService? =
276 fun registerMock(selector: String, client: BlueprintWebClientService) {
277 mocks[selector] = client
281 private class SpyPostInterceptor(private val mapper: ObjectMapper) : BluePrintRestLibPropertyService.PostInterceptor {
283 private val spies = ConcurrentHashMap<String, SpyService>()
285 override fun getInstance(jsonNode: JsonNode, service: BlueprintWebClientService): BlueprintWebClientService {
286 TODO("jsonNode-keyed services not yet supported")
289 override fun getInstance(selector: String, service: BlueprintWebClientService): BlueprintWebClientService {
290 val spiedService = SpyService(mapper, selector, service)
291 spies[selector] = spiedService
295 fun getSpies(): List<SpyService> =
296 spies.values.toList()
299 private class SpyService(
300 private val mapper: ObjectMapper,
301 val selector: String,
302 private val realService: BlueprintWebClientService
304 BlueprintWebClientService by realService {
306 private val expectations: MutableList<ExpectationDefinition> = mutableListOf()
308 override fun exchangeResource(methodType: String, path: String, request: String): WebClientResponse<String> =
309 exchangeResource(methodType, path, request,
313 override fun exchangeResource(
317 headers: Map<String, String>
318 ): WebClientResponse<String> {
319 val requestDefinition =
320 RequestDefinition(methodType, path, headers, toJson(request))
321 val realAnswer = realService.exchangeResource(methodType, path, request, headers)
322 val responseBody = when {
323 // TODO: confirm if we need to normalize the response here
324 realAnswer.status == HttpStatus.SC_OK -> toJson(realAnswer.body)
327 val responseDefinition =
328 ResponseDefinition(realAnswer.status, responseBody)
330 ExpectationDefinition(
338 override suspend fun <T> retry(times: Int, initialDelay: Long, delay: Long, block: suspend (Int) -> T): T {
339 return super.retry(times, initialDelay, delay, block)
342 fun asServiceDefinition() =
343 ServiceDefinition(selector, expectations)
345 private fun toJson(str: String): JsonNode? {
347 str.isNotBlank() -> mapper.readTree(str)
353 private val DEFAULT_HEADERS = mapOf(
354 HttpHeaders.CONTENT_TYPE to MediaType.APPLICATION_JSON_VALUE,
355 HttpHeaders.ACCEPT to MediaType.APPLICATION_JSON_VALUE