a904fa9b624209bf224c2957b0ebe7758dfd410f
[ccsdk/cds.git] / ms / blueprintsprocessor / application / src / test / kotlin / org / onap / ccsdk / cds / blueprintsprocessor / uat / utils / UatExecutor.kt
1 /*-
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
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
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.
16  *
17  * SPDX-License-Identifier: Apache-2.0
18  * ============LICENSE_END=========================================================
19  */
20 package org.onap.ccsdk.cds.blueprintsprocessor.uat.utils
21
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
62
63 /**
64  * Assumptions:
65  *
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.
68  *
69  * @author Eliezio Oliveira
70  */
71 @Component
72 class UatExecutor(
73     private val environment: ConfigurableEnvironment,
74     private val restClientFactory: BluePrintRestLibPropertyService,
75     private val mapper: ObjectMapper
76 ) {
77
78     companion object {
79         private const val NOOP_PASSWORD_PREFIX = "{noop}"
80
81         private val log: Logger = LoggerFactory.getLogger(UatExecutor::class.java)
82         private val mockLoggingListener = MockInvocationLogger(markerOf(COLOR_MOCKITO))
83     }
84
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()}"
88     }
89
90     @Throws(AssertionError::class)
91     fun execute(uatSpec: String, cbaBytes: ByteArray) {
92         val uat = UatDefinition.load(mapper, uatSpec)
93         execute(uat, cbaBytes)
94     }
95
96     /**
97      *
98      * The UAT can range from minimum to completely defined.
99      *
100      * @return an updated UAT with all NB and SB messages.
101      */
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)
107             .build()
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)
113         try {
114             // Configure mocked external services and save their expected requests for further validation
115             val requestsPerClient = uat.externalServices.associateBy(
116                 { service ->
117                     createRestClientMock(service.expectations).also { restClient ->
118                         // side-effect: register restClient to override real instance
119                         mockInterceptor.registerMock(service.selector, restClient)
120                     }
121                 },
122                 { service -> service.expectations.map { it.request } }
123             )
124
125             val newProcesses = httpClient.use { client ->
126                 uploadBlueprint(client, cbaBytes)
127
128                 // Run processes
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
135                     )
136                     ProcessDefinition(
137                         process.name,
138                         process.request,
139                         actualResponse,
140                         process.responseNormalizerSpec
141                     )
142                 }
143             }
144
145             // Validate requests to external services
146             for ((mockClient, requests) in requestsPerClient) {
147                 requests.forEach { request ->
148                     verify(mockClient, atLeastOnce()).exchangeResource(
149                         eq(request.method),
150                         eq(request.path),
151                         argThat { assertJsonEquals(request.body, this) },
152                         argThat(RequiredMapEntriesMatcher(request.headers))
153                     )
154                 }
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)
158             }
159
160             val newExternalServices = spyInterceptor.getSpies()
161                 .map(SpyService::asServiceDefinition)
162
163             return UatDefinition(newProcesses, newExternalServices)
164         } finally {
165             restClientFactory.clearInterceptors()
166         }
167     }
168
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)
175         )
176
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())
184             }
185         for (expectation in restExpectations) {
186             whenever(
187                 restClient.exchangeResource(
188                     eq(expectation.request.method),
189                     eq(expectation.request.path),
190                     any(),
191                     any()
192                 )
193             )
194                 .thenReturn(WebClientResponse(expectation.response.status, expectation.response.body.toString()))
195         }
196         return restClient
197     }
198
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")
204             .build()
205         val request = HttpPost("$baseUrl/api/v1/blueprint-model/publish").apply {
206             entity = multipartEntity
207         }
208         client.execute(request) { response ->
209             val statusLine = response.statusLine
210             assertThat(statusLine.statusCode, equalTo(HttpStatus.SC_OK))
211         }
212     }
213
214     @Throws(AssertionError::class)
215     private fun processBlueprint(
216         client: HttpClient,
217         requestBody: JsonNode,
218         expectedResponse: JsonNode?,
219         responseNormalizer: (String) -> String
220     ): JsonNode {
221         val stringEntity = StringEntity(mapper.writeValueAsString(requestBody), ContentType.APPLICATION_JSON)
222         val request = HttpPost("$baseUrl/api/v1/execution-service/process").apply {
223             entity = stringEntity
224         }
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() }
231         }
232         val actualResponse = responseNormalizer(response)
233         if (expectedResponse != null) {
234             assertJsonEquals(expectedResponse, actualResponse)
235         }
236         return mapper.readTree(actualResponse)!!
237     }
238
239     @Throws(AssertionError::class)
240     private fun assertJsonEquals(expected: JsonNode?, actual: String): Boolean {
241         // special case
242         if ((expected == null) && actual.isBlank()) {
243             return true
244         }
245         // general case
246         JSONAssert.assertEquals(expected?.toString(), actual, JSONCompareMode.LENIENT)
247         // assertEquals throws an exception whenever match fails
248         return true
249     }
250
251     private fun localServerPort(): Int =
252         (environment.getProperty("local.server.port")
253             ?: environment.getRequiredProperty("blueprint.httpPort")).toInt()
254
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)
261             else -> username
262         }
263         return "Basic " + Base64Utils.encodeToString("$username:$plainPassword".toByteArray())
264     }
265
266     private class MockPreInterceptor : BluePrintRestLibPropertyService.PreInterceptor {
267         private val mocks = ConcurrentHashMap<String, BlueprintWebClientService>()
268
269         override fun getInstance(jsonNode: JsonNode): BlueprintWebClientService? {
270             TODO("jsonNode-keyed services not yet supported")
271         }
272
273         override fun getInstance(selector: String): BlueprintWebClientService? =
274             mocks[selector]
275
276         fun registerMock(selector: String, client: BlueprintWebClientService) {
277             mocks[selector] = client
278         }
279     }
280
281     private class SpyPostInterceptor(private val mapper: ObjectMapper) : BluePrintRestLibPropertyService.PostInterceptor {
282
283         private val spies = ConcurrentHashMap<String, SpyService>()
284
285         override fun getInstance(jsonNode: JsonNode, service: BlueprintWebClientService): BlueprintWebClientService {
286             TODO("jsonNode-keyed services not yet supported")
287         }
288
289         override fun getInstance(selector: String, service: BlueprintWebClientService): BlueprintWebClientService {
290             val spiedService = SpyService(mapper, selector, service)
291             spies[selector] = spiedService
292             return spiedService
293         }
294
295         fun getSpies(): List<SpyService> =
296             spies.values.toList()
297     }
298
299     private class SpyService(
300         private val mapper: ObjectMapper,
301         val selector: String,
302         private val realService: BlueprintWebClientService
303     ) :
304         BlueprintWebClientService by realService {
305
306         private val expectations: MutableList<ExpectationDefinition> = mutableListOf()
307
308         override fun exchangeResource(methodType: String, path: String, request: String): WebClientResponse<String> =
309             exchangeResource(methodType, path, request,
310                 DEFAULT_HEADERS
311             )
312
313         override fun exchangeResource(
314             methodType: String,
315             path: String,
316             request: String,
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)
325                 else -> null
326             }
327             val responseDefinition =
328                 ResponseDefinition(realAnswer.status, responseBody)
329             expectations.add(
330                 ExpectationDefinition(
331                     requestDefinition,
332                     responseDefinition
333                 )
334             )
335             return realAnswer
336         }
337
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)
340         }
341
342         fun asServiceDefinition() =
343             ServiceDefinition(selector, expectations)
344
345         private fun toJson(str: String): JsonNode? {
346             return when {
347                 str.isNotBlank() -> mapper.readTree(str)
348                 else -> null
349             }
350         }
351
352         companion object {
353             private val DEFAULT_HEADERS = mapOf(
354                 HttpHeaders.CONTENT_TYPE to MediaType.APPLICATION_JSON_VALUE,
355                 HttpHeaders.ACCEPT to MediaType.APPLICATION_JSON_VALUE
356             )
357         }
358     }
359 }