89ba1500bbd49aacf6f08d863f08a3b18da3740d
[ccsdk/cds.git] / ms / blueprintsprocessor / application / src / main / kotlin / org / onap / ccsdk / cds / blueprintsprocessor / uat / 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
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(process.name, process.request, actualResponse, process.responseNormalizerSpec)
137                 }
138             }
139
140             // Validate requests to external services
141             for ((mockClient, requests) in requestsPerClient) {
142                 requests.forEach { request ->
143                     verify(mockClient, atLeastOnce()).exchangeResource(
144                         eq(request.method),
145                         eq(request.path),
146                         argThat { assertJsonEquals(request.body, this) },
147                         argThat(RequiredMapEntriesMatcher(request.headers))
148                     )
149                 }
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)
153             }
154
155             val newExternalServices = spyInterceptor.getSpies()
156                 .map(SpyService::asServiceDefinition)
157
158             return UatDefinition(newProcesses, newExternalServices)
159         } finally {
160             restClientFactory.clearInterceptors()
161         }
162     }
163
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)
170         )
171
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())
179             }
180         for (expectation in restExpectations) {
181             whenever(
182                 restClient.exchangeResource(
183                     eq(expectation.request.method),
184                     eq(expectation.request.path),
185                     any(),
186                     any()
187                 )
188             )
189                 .thenReturn(WebClientResponse(expectation.response.status, expectation.response.body.toString()))
190         }
191         return restClient
192     }
193
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")
199             .build()
200         val request = HttpPost("$baseUrl/api/v1/blueprint-model/publish").apply {
201             entity = multipartEntity
202         }
203         client.execute(request) { response ->
204             val statusLine = response.statusLine
205             assertThat(statusLine.statusCode, equalTo(HttpStatus.SC_OK))
206         }
207     }
208
209     @Throws(AssertionError::class)
210     private fun processBlueprint(
211         client: HttpClient,
212         requestBody: JsonNode,
213         expectedResponse: JsonNode?,
214         responseNormalizer: (String) -> String
215     ): JsonNode {
216         val stringEntity = StringEntity(mapper.writeValueAsString(requestBody), ContentType.APPLICATION_JSON)
217         val request = HttpPost("$baseUrl/api/v1/execution-service/process").apply {
218             entity = stringEntity
219         }
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() }
226         }
227         val actualResponse = responseNormalizer(response)
228         if (expectedResponse != null) {
229             assertJsonEquals(expectedResponse, actualResponse)
230         }
231         return mapper.readTree(actualResponse)!!
232     }
233
234     @Throws(AssertionError::class)
235     private fun assertJsonEquals(expected: JsonNode?, actual: String): Boolean {
236         // special case
237         if ((expected == null) && actual.isBlank()) {
238             return true
239         }
240         // general case
241         JSONAssert.assertEquals(expected?.toString(), actual, JSONCompareMode.LENIENT)
242         // assertEquals throws an exception whenever match fails
243         return true
244     }
245
246     private fun localServerPort(): Int =
247         (environment.getProperty("local.server.port")
248             ?: environment.getRequiredProperty("blueprint.httpPort")).toInt()
249
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)
255             else -> username
256         }
257         return "Basic " + Base64Utils.encodeToString("$username:$plainPassword".toByteArray())
258     }
259
260     private class MockPreInterceptor : BluePrintRestLibPropertyService.PreInterceptor {
261         private val mocks = ConcurrentHashMap<String, BlueprintWebClientService>()
262
263         override fun getInstance(jsonNode: JsonNode): BlueprintWebClientService? {
264             TODO("jsonNode-keyed services not yet supported")
265         }
266
267         override fun getInstance(selector: String): BlueprintWebClientService? =
268             mocks[selector]
269
270         fun registerMock(selector: String, client: BlueprintWebClientService) {
271             mocks[selector] = client
272         }
273     }
274
275     private class SpyPostInterceptor(private val mapper: ObjectMapper) : BluePrintRestLibPropertyService.PostInterceptor {
276
277         private val spies = ConcurrentHashMap<String, SpyService>()
278
279         override fun getInstance(jsonNode: JsonNode, service: BlueprintWebClientService): BlueprintWebClientService {
280             TODO("jsonNode-keyed services not yet supported")
281         }
282
283         override fun getInstance(selector: String, service: BlueprintWebClientService): BlueprintWebClientService {
284             val spiedService = SpyService(mapper, selector, service)
285             spies[selector] = spiedService
286             return spiedService
287         }
288
289         fun getSpies(): List<SpyService> =
290             spies.values.toList()
291     }
292
293     private class SpyService(
294         private val mapper: ObjectMapper,
295         val selector: String,
296         private val realService: BlueprintWebClientService
297     ) :
298         BlueprintWebClientService by realService {
299
300         private val expectations: MutableList<ExpectationDefinition> = mutableListOf()
301
302         override fun exchangeResource(methodType: String, path: String, request: String): WebClientResponse<String> =
303             exchangeResource(methodType, path, request, DEFAULT_HEADERS)
304
305         override fun exchangeResource(
306             methodType: String,
307             path: String,
308             request: String,
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)
316                 else -> null
317             }
318             val responseDefinition = ResponseDefinition(realAnswer.status, responseBody)
319             expectations.add(ExpectationDefinition(requestDefinition, responseDefinition))
320             return realAnswer
321         }
322
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)
325         }
326
327         fun asServiceDefinition() =
328             ServiceDefinition(selector, expectations)
329
330         private fun toJson(str: String): JsonNode? {
331             return when {
332                 str.isNotBlank() -> mapper.readTree(str)
333                 else -> null
334             }
335         }
336
337         companion object {
338             private val DEFAULT_HEADERS = mapOf(
339                 HttpHeaders.CONTENT_TYPE to MediaType.APPLICATION_JSON_VALUE,
340                 HttpHeaders.ACCEPT to MediaType.APPLICATION_JSON_VALUE
341             )
342         }
343     }
344 }