Merge SW Upgrade Blueprint into PNF_AAI and create one UAT BP for PNF
[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.atMost
28 import com.nhaarman.mockitokotlin2.eq
29 import com.nhaarman.mockitokotlin2.mock
30 import com.nhaarman.mockitokotlin2.times
31 import com.nhaarman.mockitokotlin2.verify
32 import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions
33 import com.nhaarman.mockitokotlin2.whenever
34 import org.apache.http.HttpHeaders
35 import org.apache.http.HttpStatus
36 import org.apache.http.client.HttpClient
37 import org.apache.http.client.methods.HttpPost
38 import org.apache.http.entity.ContentType
39 import org.apache.http.entity.StringEntity
40 import org.apache.http.entity.mime.HttpMultipartMode
41 import org.apache.http.entity.mime.MultipartEntityBuilder
42 import org.apache.http.impl.client.HttpClientBuilder
43 import org.apache.http.message.BasicHeader
44 import org.hamcrest.CoreMatchers.equalTo
45 import org.hamcrest.CoreMatchers.notNullValue
46 import org.hamcrest.MatcherAssert.assertThat
47 import org.mockito.Answers
48 import org.mockito.verification.VerificationMode
49 import org.onap.ccsdk.cds.blueprintsprocessor.rest.service.BluePrintRestLibPropertyService
50 import org.onap.ccsdk.cds.blueprintsprocessor.rest.service.BlueprintWebClientService
51 import org.onap.ccsdk.cds.blueprintsprocessor.rest.service.BlueprintWebClientService.WebClientResponse
52 import org.onap.ccsdk.cds.blueprintsprocessor.uat.logging.LogColor.COLOR_MOCKITO
53 import org.onap.ccsdk.cds.blueprintsprocessor.uat.logging.LogColor.markerOf
54 import org.onap.ccsdk.cds.blueprintsprocessor.uat.logging.MockInvocationLogger
55 import org.skyscreamer.jsonassert.JSONAssert
56 import org.skyscreamer.jsonassert.JSONCompareMode
57 import org.slf4j.Logger
58 import org.slf4j.LoggerFactory
59 import org.springframework.core.env.ConfigurableEnvironment
60 import org.springframework.http.MediaType
61 import org.springframework.stereotype.Component
62 import org.springframework.util.Base64Utils
63 import java.util.concurrent.ConcurrentHashMap
64
65 /**
66  * Assumptions:
67  *
68  * - Application HTTP service is bound to loopback interface;
69  * - Password is either defined in plain (with "{noop}" prefix), or it's the same of username.
70  *
71  * @author Eliezio Oliveira
72  */
73 @Component
74 class UatExecutor(
75     private val environment: ConfigurableEnvironment,
76     private val restClientFactory: BluePrintRestLibPropertyService,
77     private val mapper: ObjectMapper
78 ) {
79
80     companion object {
81         private const val NOOP_PASSWORD_PREFIX = "{noop}"
82         private const val PROPERTY_IN_UAT = "IN_UAT"
83         private val TIMES_SPEC_REGEX = "([<>]=?)?\\s*(\\d+)".toRegex()
84         private val log: Logger = LoggerFactory.getLogger(UatExecutor::class.java)
85         private val mockLoggingListener = MockInvocationLogger(markerOf(COLOR_MOCKITO))
86     }
87
88     // use lazy evaluation to postpone until localServerPort is injected by Spring
89     private val baseUrl: String by lazy {
90         "http://127.0.0.1:${localServerPort()}"
91     }
92
93     @Throws(AssertionError::class)
94     fun execute(uatSpec: String, cbaBytes: ByteArray) {
95         val uat = UatDefinition.load(mapper, uatSpec)
96         execute(uat, cbaBytes)
97     }
98
99     /**
100      *
101      * The UAT can range from minimum to completely defined.
102      *
103      * @return an updated UAT with all NB and SB messages.
104      */
105     @Throws(AssertionError::class)
106     fun execute(uat: UatDefinition, cbaBytes: ByteArray): UatDefinition {
107         val defaultHeaders = listOf(BasicHeader(HttpHeaders.AUTHORIZATION, clientAuthToken()))
108         val httpClient = HttpClientBuilder.create()
109                 .setDefaultHeaders(defaultHeaders)
110                 .build()
111         // Only if externalServices are defined
112         val mockInterceptor = MockPreInterceptor()
113         // Always defined and used, whatever the case
114         val spyInterceptor = SpyPostInterceptor(mapper)
115         restClientFactory.setInterceptors(mockInterceptor, spyInterceptor)
116         try {
117             markUatBegin()
118             // Configure mocked external services and save their expectations for further validation
119             val expectationsPerClient = uat.externalServices.associateBy(
120                     { service ->
121                         createRestClientMock(service.expectations).also { restClient ->
122                             // side-effect: register restClient to override real instance
123                             mockInterceptor.registerMock(service.selector, restClient)
124                         }
125                     },
126                     { service -> service.expectations }
127             )
128
129             val newProcesses = httpClient.use { client ->
130                 uploadBlueprint(client, cbaBytes)
131
132                 // Run processes
133                 uat.processes.map { process ->
134                     log.info("Executing process '${process.name}'")
135                     val responseNormalizer = JsonNormalizer.getNormalizer(mapper, process.responseNormalizerSpec)
136                     val actualResponse = processBlueprint(
137                             client, process.request,
138                             process.expectedResponse, responseNormalizer
139                     )
140                     ProcessDefinition(
141                             process.name,
142                             process.request,
143                             actualResponse,
144                             process.responseNormalizerSpec
145                     )
146                 }
147             }
148
149             // Validate requests to external services
150             for ((mockClient, expectations) in expectationsPerClient) {
151                 expectations.forEach { expectation ->
152                     val request = expectation.request
153                     verify(mockClient, evalVerificationMode(expectation.times)).exchangeResource(
154                             eq(request.method),
155                             eq(request.path),
156                             argThat { assertJsonEquals(request.body, this) },
157                             argThat(RequiredMapEntriesMatcher(request.headers))
158                     )
159                 }
160                 // Don't mind the invocations to the overloaded exchangeResource(String, String, String)
161                 verify(mockClient, atLeast(0)).exchangeResource(any(), any(), any())
162                 verifyNoMoreInteractions(mockClient)
163             }
164
165             val newExternalServices = spyInterceptor.getSpies()
166                     .map(SpyService::asServiceDefinition)
167
168             return UatDefinition(newProcesses, newExternalServices)
169         } finally {
170             restClientFactory.clearInterceptors()
171             markUatEnd()
172         }
173     }
174
175     private fun markUatBegin() {
176         System.setProperty(PROPERTY_IN_UAT, "1")
177     }
178
179     private fun markUatEnd() {
180         System.clearProperty(PROPERTY_IN_UAT)
181     }
182
183     private fun createRestClientMock(restExpectations: List<ExpectationDefinition>):
184             BlueprintWebClientService {
185         val restClient = mock<BlueprintWebClientService>(
186                 defaultAnswer = Answers.RETURNS_SMART_NULLS,
187                 // our custom verboseLogging handler
188                 invocationListeners = arrayOf(mockLoggingListener)
189         )
190
191         // Delegates to overloaded exchangeResource(String, String, String, Map<String, String>)
192         whenever(restClient.exchangeResource(any(), any(), any()))
193                 .thenAnswer { invocation ->
194                     val method = invocation.arguments[0] as String
195                     val path = invocation.arguments[1] as String
196                     val request = invocation.arguments[2] as String
197                     restClient.exchangeResource(method, path, request, emptyMap())
198                 }
199         for (expectation in restExpectations) {
200             var stubbing = whenever(
201                     restClient.exchangeResource(
202                             eq(expectation.request.method),
203                             eq(expectation.request.path),
204                             any(),
205                             any()
206                     )
207             )
208             for (response in expectation.responses) {
209                 stubbing = stubbing.thenReturn(WebClientResponse(response.status, response.body.toString()))
210             }
211         }
212         return restClient
213     }
214
215     @Throws(AssertionError::class)
216     private fun uploadBlueprint(client: HttpClient, cbaBytes: ByteArray) {
217         val multipartEntity = MultipartEntityBuilder.create()
218                 .setMode(HttpMultipartMode.BROWSER_COMPATIBLE)
219                 .addBinaryBody("file", cbaBytes, ContentType.DEFAULT_BINARY, "cba.zip")
220                 .build()
221         val request = HttpPost("$baseUrl/api/v1/blueprint-model/publish").apply {
222             entity = multipartEntity
223         }
224         client.execute(request) { response ->
225             val statusLine = response.statusLine
226             assertThat(statusLine.statusCode, equalTo(HttpStatus.SC_OK))
227         }
228     }
229
230     @Throws(AssertionError::class)
231     private fun processBlueprint(
232         client: HttpClient,
233         requestBody: JsonNode,
234         expectedResponse: JsonNode?,
235         responseNormalizer: (String) -> String
236     ): JsonNode {
237         val stringEntity = StringEntity(mapper.writeValueAsString(requestBody), ContentType.APPLICATION_JSON)
238         val request = HttpPost("$baseUrl/api/v1/execution-service/process").apply {
239             entity = stringEntity
240         }
241         val response = client.execute(request) { response ->
242             val statusLine = response.statusLine
243             assertThat(statusLine.statusCode, equalTo(HttpStatus.SC_OK))
244             val entity = response.entity
245             assertThat("Response contains no content", entity, notNullValue())
246             entity.content.bufferedReader().use { it.readText() }
247         }
248         val actualResponse = responseNormalizer(response)
249         if (expectedResponse != null) {
250             assertJsonEquals(expectedResponse, actualResponse)
251         }
252         return mapper.readTree(actualResponse)!!
253     }
254
255     private fun evalVerificationMode(times: String): VerificationMode {
256         val matchResult = TIMES_SPEC_REGEX.matchEntire(times) ?: throw InvalidUatDefinition(
257                 "Time specification '$times' does not follow expected format $TIMES_SPEC_REGEX")
258         val counter = matchResult.groups[2]!!.value.toInt()
259         return when (matchResult.groups[1]?.value) {
260             ">=" -> atLeast(counter)
261             ">" -> atLeast(counter + 1)
262             "<=" -> atMost(counter)
263             "<" -> atMost(counter - 1)
264             else -> times(counter)
265         }
266     }
267
268     @Throws(AssertionError::class)
269     private fun assertJsonEquals(expected: JsonNode?, actual: String): Boolean {
270         // special case
271         if ((expected == null) && actual.isBlank()) {
272             return true
273         }
274         // general case
275         JSONAssert.assertEquals(expected?.toString(), actual, JSONCompareMode.LENIENT)
276         // assertEquals throws an exception whenever match fails
277         return true
278     }
279
280     private fun localServerPort(): Int =
281             (environment.getProperty("local.server.port")
282                     ?: environment.getRequiredProperty("blueprint.httpPort")).toInt()
283
284     private fun clientAuthToken(): String {
285         val username = environment.getRequiredProperty("security.user.name")
286         val password = environment.getRequiredProperty("security.user.password")
287         val plainPassword = when {
288             password.startsWith(NOOP_PASSWORD_PREFIX) -> password.substring(
289                     NOOP_PASSWORD_PREFIX.length)
290             else -> username
291         }
292         return "Basic " + Base64Utils.encodeToString("$username:$plainPassword".toByteArray())
293     }
294
295     private class MockPreInterceptor : BluePrintRestLibPropertyService.PreInterceptor {
296         private val mocks = ConcurrentHashMap<String, BlueprintWebClientService>()
297
298         override fun getInstance(jsonNode: JsonNode): BlueprintWebClientService? {
299             TODO("jsonNode-keyed services not yet supported")
300         }
301
302         override fun getInstance(selector: String): BlueprintWebClientService? =
303                 mocks[selector]
304
305         fun registerMock(selector: String, client: BlueprintWebClientService) {
306             mocks[selector] = client
307         }
308     }
309
310     private class SpyPostInterceptor(private val mapper: ObjectMapper) : BluePrintRestLibPropertyService.PostInterceptor {
311
312         private val spies = ConcurrentHashMap<String, SpyService>()
313
314         override fun getInstance(jsonNode: JsonNode, service: BlueprintWebClientService): BlueprintWebClientService {
315             TODO("jsonNode-keyed services not yet supported")
316         }
317
318         override fun getInstance(selector: String, service: BlueprintWebClientService): BlueprintWebClientService {
319             val spiedService = SpyService(mapper, selector, service)
320             spies[selector] = spiedService
321             return spiedService
322         }
323
324         fun getSpies(): List<SpyService> =
325                 spies.values.toList()
326     }
327
328     private class SpyService(
329         private val mapper: ObjectMapper,
330         val selector: String,
331         private val realService: BlueprintWebClientService
332     ) :
333             BlueprintWebClientService by realService {
334
335         private val expectations: MutableList<ExpectationDefinition> = mutableListOf()
336
337         override fun exchangeResource(methodType: String, path: String, request: String): WebClientResponse<String> =
338                 exchangeResource(methodType, path, request,
339                         DEFAULT_HEADERS
340                 )
341
342         override fun exchangeResource(
343             methodType: String,
344             path: String,
345             request: String,
346             headers: Map<String, String>
347         ): WebClientResponse<String> {
348             val requestDefinition =
349                     RequestDefinition(methodType, path, headers, toJson(request))
350             val realAnswer = realService.exchangeResource(methodType, path, request, headers)
351             val responseBody = when {
352                 // TODO: confirm if we need to normalize the response here
353                 realAnswer.status == HttpStatus.SC_OK -> toJson(realAnswer.body)
354                 else -> null
355             }
356             val responseDefinition =
357                     ResponseDefinition(realAnswer.status, responseBody)
358             expectations.add(
359                     ExpectationDefinition(
360                             requestDefinition,
361                             responseDefinition
362                     )
363             )
364             return realAnswer
365         }
366
367         override suspend fun <T> retry(times: Int, initialDelay: Long, delay: Long, block: suspend (Int) -> T): T {
368             return super.retry(times, initialDelay, delay, block)
369         }
370
371         fun asServiceDefinition() =
372                 ServiceDefinition(selector, expectations)
373
374         private fun toJson(str: String): JsonNode? {
375             return when {
376                 str.isNotBlank() -> mapper.readTree(str)
377                 else -> null
378             }
379         }
380
381         companion object {
382             private val DEFAULT_HEADERS = mapOf(
383                     HttpHeaders.CONTENT_TYPE to MediaType.APPLICATION_JSON_VALUE,
384                     HttpHeaders.ACCEPT to MediaType.APPLICATION_JSON_VALUE
385             )
386         }
387     }
388 }