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(
133 client, process.request,
134 process.expectedResponse, responseNormalizer
136 ProcessDefinition(process.name, process.request, actualResponse, process.responseNormalizerSpec)
140 // Validate requests to external services
141 for ((mockClient, requests) in requestsPerClient) {
142 requests.forEach { request ->
143 verify(mockClient, atLeastOnce()).exchangeResource(
146 argThat { assertJsonEquals(request.body, this) },
147 argThat(RequiredMapEntriesMatcher(request.headers))
150 // Don't mind the invocations to the overloaded exchangeResource(String, String, String)
151 verify(mockClient, atLeast(0)).exchangeResource(any(), any(), any())
152 verifyNoMoreInteractions(mockClient)
155 val newExternalServices = spyInterceptor.getSpies()
156 .map(SpyService::asServiceDefinition)
158 return UatDefinition(newProcesses, newExternalServices)
160 restClientFactory.clearInterceptors()
164 private fun createRestClientMock(restExpectations: List<ExpectationDefinition>):
165 BlueprintWebClientService {
166 val restClient = mock<BlueprintWebClientService>(
167 defaultAnswer = Answers.RETURNS_SMART_NULLS,
168 // our custom verboseLogging handler
169 invocationListeners = arrayOf(mockLoggingListener)
172 // Delegates to overloaded exchangeResource(String, String, String, Map<String, String>)
173 whenever(restClient.exchangeResource(any(), any(), any()))
174 .thenAnswer { invocation ->
175 val method = invocation.arguments[0] as String
176 val path = invocation.arguments[1] as String
177 val request = invocation.arguments[2] as String
178 restClient.exchangeResource(method, path, request, emptyMap())
180 for (expectation in restExpectations) {
182 restClient.exchangeResource(
183 eq(expectation.request.method),
184 eq(expectation.request.path),
189 .thenReturn(WebClientResponse(expectation.response.status, expectation.response.body.toString()))
194 @Throws(AssertionError::class)
195 private fun uploadBlueprint(client: HttpClient, cbaBytes: ByteArray) {
196 val multipartEntity = MultipartEntityBuilder.create()
197 .setMode(HttpMultipartMode.BROWSER_COMPATIBLE)
198 .addBinaryBody("file", cbaBytes, ContentType.DEFAULT_BINARY, "cba.zip")
200 val request = HttpPost("$baseUrl/api/v1/blueprint-model/publish").apply {
201 entity = multipartEntity
203 client.execute(request) { response ->
204 val statusLine = response.statusLine
205 assertThat(statusLine.statusCode, equalTo(HttpStatus.SC_OK))
209 @Throws(AssertionError::class)
210 private fun processBlueprint(
212 requestBody: JsonNode,
213 expectedResponse: JsonNode?,
214 responseNormalizer: (String) -> String
216 val stringEntity = StringEntity(mapper.writeValueAsString(requestBody), ContentType.APPLICATION_JSON)
217 val request = HttpPost("$baseUrl/api/v1/execution-service/process").apply {
218 entity = stringEntity
220 val response = client.execute(request) { response ->
221 val statusLine = response.statusLine
222 assertThat(statusLine.statusCode, equalTo(HttpStatus.SC_OK))
223 val entity = response.entity
224 assertThat("Response contains no content", entity, notNullValue())
225 entity.content.bufferedReader().use { it.readText() }
227 val actualResponse = responseNormalizer(response)
228 if (expectedResponse != null) {
229 assertJsonEquals(expectedResponse, actualResponse)
231 return mapper.readTree(actualResponse)!!
234 @Throws(AssertionError::class)
235 private fun assertJsonEquals(expected: JsonNode?, actual: String): Boolean {
237 if ((expected == null) && actual.isBlank()) {
241 JSONAssert.assertEquals(expected?.toString(), actual, JSONCompareMode.LENIENT)
242 // assertEquals throws an exception whenever match fails
246 private fun localServerPort(): Int =
247 (environment.getProperty("local.server.port")
248 ?: environment.getRequiredProperty("blueprint.httpPort")).toInt()
250 private fun clientAuthToken(): String {
251 val username = environment.getRequiredProperty("security.user.name")
252 val password = environment.getRequiredProperty("security.user.password")
253 val plainPassword = when {
254 password.startsWith(NOOP_PASSWORD_PREFIX) -> password.substring(NOOP_PASSWORD_PREFIX.length)
257 return "Basic " + Base64Utils.encodeToString("$username:$plainPassword".toByteArray())
260 private class MockPreInterceptor : BluePrintRestLibPropertyService.PreInterceptor {
261 private val mocks = ConcurrentHashMap<String, BlueprintWebClientService>()
263 override fun getInstance(jsonNode: JsonNode): BlueprintWebClientService? {
264 TODO("jsonNode-keyed services not yet supported")
267 override fun getInstance(selector: String): BlueprintWebClientService? =
270 fun registerMock(selector: String, client: BlueprintWebClientService) {
271 mocks[selector] = client
275 private class SpyPostInterceptor(private val mapper: ObjectMapper) : BluePrintRestLibPropertyService.PostInterceptor {
277 private val spies = ConcurrentHashMap<String, SpyService>()
279 override fun getInstance(jsonNode: JsonNode, service: BlueprintWebClientService): BlueprintWebClientService {
280 TODO("jsonNode-keyed services not yet supported")
283 override fun getInstance(selector: String, service: BlueprintWebClientService): BlueprintWebClientService {
284 val spiedService = SpyService(mapper, selector, service)
285 spies[selector] = spiedService
289 fun getSpies(): List<SpyService> =
290 spies.values.toList()
293 private class SpyService(
294 private val mapper: ObjectMapper,
295 val selector: String,
296 private val realService: BlueprintWebClientService
298 BlueprintWebClientService by realService {
300 private val expectations: MutableList<ExpectationDefinition> = mutableListOf()
302 override fun exchangeResource(methodType: String, path: String, request: String): WebClientResponse<String> =
303 exchangeResource(methodType, path, request, DEFAULT_HEADERS)
305 override fun exchangeResource(
309 headers: Map<String, String>
310 ): WebClientResponse<String> {
311 val requestDefinition = RequestDefinition(methodType, path, headers, toJson(request))
312 val realAnswer = realService.exchangeResource(methodType, path, request, headers)
313 val responseBody = when {
314 // TODO: confirm if we need to normalize the response here
315 realAnswer.status == HttpStatus.SC_OK -> toJson(realAnswer.body)
318 val responseDefinition = ResponseDefinition(realAnswer.status, responseBody)
319 expectations.add(ExpectationDefinition(requestDefinition, responseDefinition))
323 override suspend fun <T> retry(times: Int, initialDelay: Long, delay: Long, block: suspend (Int) -> T): T {
324 return super.retry(times, initialDelay, delay, block)
327 fun asServiceDefinition() =
328 ServiceDefinition(selector, expectations)
330 private fun toJson(str: String): JsonNode? {
332 str.isNotBlank() -> mapper.readTree(str)
338 private val DEFAULT_HEADERS = mapOf(
339 HttpHeaders.CONTENT_TYPE to MediaType.APPLICATION_JSON_VALUE,
340 HttpHeaders.ACCEPT to MediaType.APPLICATION_JSON_VALUE