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
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(client, process.request,
133 process.expectedResponse, responseNormalizer)
134 ProcessDefinition(process.name, process.request, actualResponse, process.responseNormalizerSpec)
138 // Validate requests to external services
139 for ((mockClient, requests) in requestsPerClient) {
140 requests.forEach { request ->
141 verify(mockClient, atLeastOnce()).exchangeResource(
144 argThat { assertJsonEquals(request.body, this) },
145 argThat(RequiredMapEntriesMatcher(request.headers)))
147 // Don't mind the invocations to the overloaded exchangeResource(String, String, String)
148 verify(mockClient, atLeast(0)).exchangeResource(any(), any(), any())
149 verifyNoMoreInteractions(mockClient)
152 val newExternalServices = spyInterceptor.getSpies()
153 .map(SpyService::asServiceDefinition)
155 return UatDefinition(newProcesses, newExternalServices)
157 restClientFactory.clearInterceptors()
161 private fun createRestClientMock(restExpectations: List<ExpectationDefinition>)
162 : BlueprintWebClientService {
163 val restClient = mock<BlueprintWebClientService>(
164 defaultAnswer = Answers.RETURNS_SMART_NULLS,
165 // our custom verboseLogging handler
166 invocationListeners = arrayOf(mockLoggingListener)
169 // Delegates to overloaded exchangeResource(String, String, String, Map<String, String>)
170 whenever(restClient.exchangeResource(any(), any(), any()))
171 .thenAnswer { invocation ->
172 val method = invocation.arguments[0] as String
173 val path = invocation.arguments[1] as String
174 val request = invocation.arguments[2] as String
175 restClient.exchangeResource(method, path, request, emptyMap())
177 for (expectation in restExpectations) {
178 whenever(restClient.exchangeResource(
179 eq(expectation.request.method),
180 eq(expectation.request.path),
183 .thenReturn(WebClientResponse(expectation.response.status, expectation.response.body.toString()))
188 @Throws(AssertionError::class)
189 private fun uploadBlueprint(client: HttpClient, cbaBytes: ByteArray) {
190 val multipartEntity = MultipartEntityBuilder.create()
191 .setMode(HttpMultipartMode.BROWSER_COMPATIBLE)
192 .addBinaryBody("file", cbaBytes, ContentType.DEFAULT_BINARY, "cba.zip")
194 val request = HttpPost("$baseUrl/api/v1/blueprint-model/publish").apply {
195 entity = multipartEntity
197 client.execute(request) { response ->
198 val statusLine = response.statusLine
199 assertThat(statusLine.statusCode, equalTo(HttpStatus.SC_OK))
203 @Throws(AssertionError::class)
204 private fun processBlueprint(client: HttpClient, requestBody: JsonNode,
205 expectedResponse: JsonNode?, responseNormalizer: (String) -> String): JsonNode {
206 val stringEntity = StringEntity(mapper.writeValueAsString(requestBody), ContentType.APPLICATION_JSON)
207 val request = HttpPost("$baseUrl/api/v1/execution-service/process").apply {
208 entity = stringEntity
210 val response = client.execute(request) { response ->
211 val statusLine = response.statusLine
212 assertThat(statusLine.statusCode, equalTo(HttpStatus.SC_OK))
213 val entity = response.entity
214 assertThat("Response contains no content", entity, notNullValue())
215 entity.content.bufferedReader().use { it.readText() }
217 val actualResponse = responseNormalizer(response)
218 if (expectedResponse != null) {
219 assertJsonEquals(expectedResponse, actualResponse)
221 return mapper.readTree(actualResponse)!!
224 @Throws(AssertionError::class)
225 private fun assertJsonEquals(expected: JsonNode?, actual: String): Boolean {
227 if ((expected == null) && actual.isBlank()) {
231 JSONAssert.assertEquals(expected?.toString(), actual, JSONCompareMode.LENIENT)
232 // assertEquals throws an exception whenever match fails
236 private fun localServerPort(): Int =
237 (environment.getProperty("local.server.port")
238 ?: environment.getRequiredProperty("blueprint.httpPort")).toInt()
240 private fun clientAuthToken(): String {
241 val username = environment.getRequiredProperty("security.user.name")
242 val password = environment.getRequiredProperty("security.user.password")
243 val plainPassword = when {
244 password.startsWith(NOOP_PASSWORD_PREFIX) -> password.substring(NOOP_PASSWORD_PREFIX.length)
247 return "Basic " + Base64Utils.encodeToString("$username:$plainPassword".toByteArray())
250 private class MockPreInterceptor : BluePrintRestLibPropertyService.PreInterceptor {
251 private val mocks = ConcurrentHashMap<String, BlueprintWebClientService>()
253 override fun getInstance(jsonNode: JsonNode): BlueprintWebClientService? {
254 TODO("jsonNode-keyed services not yet supported")
257 override fun getInstance(selector: String): BlueprintWebClientService? =
260 fun registerMock(selector: String, client: BlueprintWebClientService) {
261 mocks[selector] = client
265 private class SpyPostInterceptor(private val mapper: ObjectMapper) : BluePrintRestLibPropertyService.PostInterceptor {
267 private val spies = ConcurrentHashMap<String, SpyService>()
269 override fun getInstance(jsonNode: JsonNode, service: BlueprintWebClientService): BlueprintWebClientService {
270 TODO("jsonNode-keyed services not yet supported")
273 override fun getInstance(selector: String, service: BlueprintWebClientService): BlueprintWebClientService {
274 val spiedService = SpyService(mapper, selector, service)
275 spies[selector] = spiedService
279 fun getSpies(): List<SpyService> =
280 spies.values.toList()
283 private class SpyService(private val mapper: ObjectMapper,
284 val selector: String,
285 private val realService: BlueprintWebClientService) :
286 BlueprintWebClientService by realService {
288 private val expectations: MutableList<ExpectationDefinition> = mutableListOf()
290 override fun exchangeResource(methodType: String, path: String, request: String): WebClientResponse<String> =
291 exchangeResource(methodType, path, request, DEFAULT_HEADERS)
293 override fun exchangeResource(methodType: String, path: String, request: String,
294 headers: Map<String, String>): WebClientResponse<String> {
295 val requestDefinition = RequestDefinition(methodType, path, headers, toJson(request))
296 val realAnswer = realService.exchangeResource(methodType, path, request, headers)
297 val responseBody = when {
298 // TODO: confirm if we need to normalize the response here
299 realAnswer.status == HttpStatus.SC_OK -> toJson(realAnswer.body)
302 val responseDefinition = ResponseDefinition(realAnswer.status, responseBody)
303 expectations.add(ExpectationDefinition(requestDefinition, responseDefinition))
307 fun asServiceDefinition() =
308 ServiceDefinition(selector, expectations)
310 private fun toJson(str: String): JsonNode? {
312 str.isNotBlank() -> mapper.readTree(str)
318 private val DEFAULT_HEADERS = mapOf(
319 HttpHeaders.CONTENT_TYPE to MediaType.APPLICATION_JSON_VALUE,
320 HttpHeaders.ACCEPT to MediaType.APPLICATION_JSON_VALUE