b97dbf7bb86c20493c602c1fde8d9fb57894f164
[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 open 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,
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         process: ProcessDefinition,
235         expectedResponse: JsonNode?,
236         responseNormalizer: (String) -> String
237     ): JsonNode {
238         val stringEntity = StringEntity(mapper.writeValueAsString(process.request), 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             val expectedCode = expectedResponse?.get("status")?.get("code")?.intValue()
245             assertThat("${process.name}", statusLine.statusCode, equalTo(expectedCode ?: HttpStatus.SC_OK))
246             val entity = response.entity
247             assertThat("${process.name} Response contains no content", entity, notNullValue())
248             entity.content.bufferedReader().use { it.readText() }
249         }
250         val actualResponse = responseNormalizer(response)
251         if (expectedResponse != null) {
252             assertJsonEquals(expectedResponse, actualResponse, process.name)
253         }
254         return mapper.readTree(actualResponse)!!
255     }
256
257     private fun evalVerificationMode(times: String): VerificationMode {
258         val matchResult = TIMES_SPEC_REGEX.matchEntire(times) ?: throw InvalidUatDefinition(
259             "Time specification '$times' does not follow expected format $TIMES_SPEC_REGEX"
260         )
261         val counter = matchResult.groups[2]!!.value.toInt()
262         return when (matchResult.groups[1]?.value) {
263             ">=" -> atLeast(counter)
264             ">" -> atLeast(counter + 1)
265             "<=" -> atMost(counter)
266             "<" -> atMost(counter - 1)
267             else -> times(counter)
268         }
269     }
270
271     @Throws(AssertionError::class)
272     private fun assertJsonEquals(expected: JsonNode?, actual: String, msg: String = ""): Boolean {
273         // special case
274         if ((expected == null) && actual.isBlank()) {
275             return true
276         }
277         // general case
278         JSONAssert.assertEquals(msg, expected?.toString(), actual, JSONCompareMode.LENIENT)
279         // assertEquals throws an exception whenever match fails
280         return true
281     }
282
283     private fun localServerPort(): Int =
284         (
285             environment.getProperty("local.server.port")
286                 ?: environment.getRequiredProperty("blueprint.httpPort")
287             ).toInt()
288
289     private fun clientAuthToken(): String {
290         val username = environment.getRequiredProperty("security.user.name")
291         val password = environment.getRequiredProperty("security.user.password")
292         val plainPassword = when {
293             password.startsWith(NOOP_PASSWORD_PREFIX) -> password.substring(
294                 NOOP_PASSWORD_PREFIX.length
295             )
296             else -> username
297         }
298         return "Basic " + Base64Utils.encodeToString("$username:$plainPassword".toByteArray())
299     }
300
301     open class MockPreInterceptor : BluePrintRestLibPropertyService.PreInterceptor {
302
303         private val mocks = ConcurrentHashMap<String, BlueprintWebClientService>()
304
305         override fun getInstance(jsonNode: JsonNode): BlueprintWebClientService? {
306             TODO("jsonNode-keyed services not yet supported")
307         }
308
309         override fun getInstance(selector: String): BlueprintWebClientService? =
310             mocks[selector]
311
312         fun registerMock(selector: String, client: BlueprintWebClientService) {
313             mocks[selector] = client
314         }
315     }
316
317     open class SpyPostInterceptor(private val mapper: ObjectMapper) : BluePrintRestLibPropertyService.PostInterceptor {
318
319         private val spies = ConcurrentHashMap<String, SpyService>()
320
321         override fun getInstance(jsonNode: JsonNode, service: BlueprintWebClientService): BlueprintWebClientService {
322             TODO("jsonNode-keyed services not yet supported")
323         }
324
325         override fun getInstance(selector: String, service: BlueprintWebClientService): BlueprintWebClientService {
326             var spiedService = spies[selector]
327             if (spiedService != null)
328                 return spiedService
329
330             spiedService = SpyService(mapper, selector, service)
331             spies[selector] = spiedService
332
333             return spiedService
334         }
335         fun getSpies(): List<SpyService> =
336             spies.values.toList()
337     }
338
339     open class SpyService(
340         private val mapper: ObjectMapper,
341         val selector: String,
342         private val realService: BlueprintWebClientService
343     ) :
344         BlueprintWebClientService by realService {
345
346         private val expectations: MutableList<ExpectationDefinition> = mutableListOf()
347
348         override fun exchangeResource(methodType: String, path: String, request: String): WebClientResponse<String> =
349             exchangeResource(
350                 methodType, path, request,
351                 DEFAULT_HEADERS
352             )
353
354         override fun exchangeResource(
355             methodType: String,
356             path: String,
357             request: String,
358             headers: Map<String, String>
359         ): WebClientResponse<String> {
360             val requestDefinition =
361                 RequestDefinition(methodType, path, headers, toJson(request))
362             val realAnswer = realService.exchangeResource(methodType, path, request, headers)
363             val responseBody = when {
364                 // TODO: confirm if we need to normalize the response here
365                 realAnswer.status == HttpStatus.SC_OK -> toJson(realAnswer.body)
366                 else -> null
367             }
368             val responseDefinition =
369                 ResponseDefinition(realAnswer.status, responseBody)
370             expectations.add(
371                 ExpectationDefinition(
372                     requestDefinition,
373                     responseDefinition
374                 )
375             )
376             return realAnswer
377         }
378
379         override suspend fun <T> retry(times: Int, initialDelay: Long, delay: Long, block: suspend (Int) -> T): T {
380             return super.retry(times, initialDelay, delay, block)
381         }
382
383         fun asServiceDefinition() =
384             ServiceDefinition(selector, expectations)
385
386         private fun toJson(str: String): JsonNode? {
387             return when {
388                 str.isNotBlank() -> mapper.readTree(str)
389                 else -> null
390             }
391         }
392
393         companion object {
394
395             private val DEFAULT_HEADERS = mapOf(
396                 HttpHeaders.CONTENT_TYPE to MediaType.APPLICATION_JSON_VALUE,
397                 HttpHeaders.ACCEPT to MediaType.APPLICATION_JSON_VALUE
398             )
399         }
400     }
401 }