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