Merge "Loopback code for GRPC functionality"
[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(client, process.request,
133                             process.expectedResponse, responseNormalizer)
134                     ProcessDefinition(process.name, process.request, actualResponse, process.responseNormalizerSpec)
135                 }
136             }
137
138             // Validate requests to external services
139             for ((mockClient, requests) in requestsPerClient) {
140                 requests.forEach { request ->
141                     verify(mockClient, atLeastOnce()).exchangeResource(
142                             eq(request.method),
143                             eq(request.path),
144                             argThat { assertJsonEquals(request.body, this) },
145                             argThat(RequiredMapEntriesMatcher(request.headers)))
146                 }
147                 // Don't mind the invocations to the overloaded exchangeResource(String, String, String)
148                 verify(mockClient, atLeast(0)).exchangeResource(any(), any(), any())
149                 verifyNoMoreInteractions(mockClient)
150             }
151
152             val newExternalServices = spyInterceptor.getSpies()
153                     .map(SpyService::asServiceDefinition)
154
155             return UatDefinition(newProcesses, newExternalServices)
156         } finally {
157             restClientFactory.clearInterceptors()
158         }
159     }
160
161     private fun createRestClientMock(restExpectations: List<ExpectationDefinition>)
162             : BlueprintWebClientService {
163         val restClient = mock<BlueprintWebClientService>(
164                 defaultAnswer = Answers.RETURNS_SMART_NULLS,
165                 // our custom verboseLogging handler
166                 invocationListeners = arrayOf(mockLoggingListener)
167         )
168
169         // Delegates to overloaded exchangeResource(String, String, String, Map<String, String>)
170         whenever(restClient.exchangeResource(any(), any(), any()))
171                 .thenAnswer { invocation ->
172                     val method = invocation.arguments[0] as String
173                     val path = invocation.arguments[1] as String
174                     val request = invocation.arguments[2] as String
175                     restClient.exchangeResource(method, path, request, emptyMap())
176                 }
177         for (expectation in restExpectations) {
178             whenever(restClient.exchangeResource(
179                     eq(expectation.request.method),
180                     eq(expectation.request.path),
181                     any(),
182                     any()))
183                     .thenReturn(WebClientResponse(expectation.response.status, expectation.response.body.toString()))
184         }
185         return restClient
186     }
187
188     @Throws(AssertionError::class)
189     private fun uploadBlueprint(client: HttpClient, cbaBytes: ByteArray) {
190         val multipartEntity = MultipartEntityBuilder.create()
191                 .setMode(HttpMultipartMode.BROWSER_COMPATIBLE)
192                 .addBinaryBody("file", cbaBytes, ContentType.DEFAULT_BINARY, "cba.zip")
193                 .build()
194         val request = HttpPost("$baseUrl/api/v1/blueprint-model/publish").apply {
195             entity = multipartEntity
196         }
197         client.execute(request) { response ->
198             val statusLine = response.statusLine
199             assertThat(statusLine.statusCode, equalTo(HttpStatus.SC_OK))
200         }
201     }
202
203     @Throws(AssertionError::class)
204     private fun processBlueprint(client: HttpClient, requestBody: JsonNode,
205                                  expectedResponse: JsonNode?, responseNormalizer: (String) -> String): JsonNode {
206         val stringEntity = StringEntity(mapper.writeValueAsString(requestBody), ContentType.APPLICATION_JSON)
207         val request = HttpPost("$baseUrl/api/v1/execution-service/process").apply {
208             entity = stringEntity
209         }
210         val response = client.execute(request) { response ->
211             val statusLine = response.statusLine
212             assertThat(statusLine.statusCode, equalTo(HttpStatus.SC_OK))
213             val entity = response.entity
214             assertThat("Response contains no content", entity, notNullValue())
215             entity.content.bufferedReader().use { it.readText() }
216         }
217         val actualResponse = responseNormalizer(response)
218         if (expectedResponse != null) {
219             assertJsonEquals(expectedResponse, actualResponse)
220         }
221         return mapper.readTree(actualResponse)!!
222     }
223
224     @Throws(AssertionError::class)
225     private fun assertJsonEquals(expected: JsonNode?, actual: String): Boolean {
226         // special case
227         if ((expected == null) && actual.isBlank()) {
228             return true
229         }
230         // general case
231         JSONAssert.assertEquals(expected?.toString(), actual, JSONCompareMode.LENIENT)
232         // assertEquals throws an exception whenever match fails
233         return true
234     }
235
236     private fun localServerPort(): Int =
237             (environment.getProperty("local.server.port")
238                     ?: environment.getRequiredProperty("blueprint.httpPort")).toInt()
239
240     private fun clientAuthToken(): String {
241         val username = environment.getRequiredProperty("security.user.name")
242         val password = environment.getRequiredProperty("security.user.password")
243         val plainPassword = when {
244             password.startsWith(NOOP_PASSWORD_PREFIX) -> password.substring(NOOP_PASSWORD_PREFIX.length)
245             else -> username
246         }
247         return "Basic " + Base64Utils.encodeToString("$username:$plainPassword".toByteArray())
248     }
249
250     private class MockPreInterceptor : BluePrintRestLibPropertyService.PreInterceptor {
251         private val mocks = ConcurrentHashMap<String, BlueprintWebClientService>()
252
253         override fun getInstance(jsonNode: JsonNode): BlueprintWebClientService? {
254             TODO("jsonNode-keyed services not yet supported")
255         }
256
257         override fun getInstance(selector: String): BlueprintWebClientService? =
258                 mocks[selector]
259
260         fun registerMock(selector: String, client: BlueprintWebClientService) {
261             mocks[selector] = client
262         }
263     }
264
265     private class SpyPostInterceptor(private val mapper: ObjectMapper) : BluePrintRestLibPropertyService.PostInterceptor {
266
267         private val spies = ConcurrentHashMap<String, SpyService>()
268
269         override fun getInstance(jsonNode: JsonNode, service: BlueprintWebClientService): BlueprintWebClientService {
270             TODO("jsonNode-keyed services not yet supported")
271         }
272
273         override fun getInstance(selector: String, service: BlueprintWebClientService): BlueprintWebClientService {
274             val spiedService = SpyService(mapper, selector, service)
275             spies[selector] = spiedService
276             return spiedService
277         }
278
279         fun getSpies(): List<SpyService> =
280                 spies.values.toList()
281     }
282
283     private class SpyService(private val mapper: ObjectMapper,
284                              val selector: String,
285                              private val realService: BlueprintWebClientService) :
286             BlueprintWebClientService by realService {
287
288         private val expectations: MutableList<ExpectationDefinition> = mutableListOf()
289
290         override fun exchangeResource(methodType: String, path: String, request: String): WebClientResponse<String> =
291                 exchangeResource(methodType, path, request, DEFAULT_HEADERS)
292
293         override fun exchangeResource(methodType: String, path: String, request: String,
294                                       headers: Map<String, String>): WebClientResponse<String> {
295             val requestDefinition = RequestDefinition(methodType, path, headers, toJson(request))
296             val realAnswer = realService.exchangeResource(methodType, path, request, headers)
297             val responseBody = when {
298                 // TODO: confirm if we need to normalize the response here
299                 realAnswer.status == HttpStatus.SC_OK -> toJson(realAnswer.body)
300                 else -> null
301             }
302             val responseDefinition = ResponseDefinition(realAnswer.status, responseBody)
303             expectations.add(ExpectationDefinition(requestDefinition, responseDefinition))
304             return realAnswer
305         }
306
307         fun asServiceDefinition() =
308                 ServiceDefinition(selector, expectations)
309
310         private fun toJson(str: String): JsonNode? {
311             return when {
312                 str.isNotBlank() -> mapper.readTree(str)
313                 else -> null
314             }
315         }
316
317         companion object {
318             private val DEFAULT_HEADERS = mapOf(
319                     HttpHeaders.CONTENT_TYPE to MediaType.APPLICATION_JSON_VALUE,
320                     HttpHeaders.ACCEPT to MediaType.APPLICATION_JSON_VALUE
321             )
322         }
323     }
324 }