Merge "Improve Remote Python Executor error handling and allow for structured response"
[ccsdk/cds.git] / ms / blueprintsprocessor / application / src / test / kotlin / org / onap / ccsdk / cds / blueprintsprocessor / uat / UatServicesTest.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.ObjectMapper
23 import com.github.tomakehurst.wiremock.WireMockServer
24 import com.github.tomakehurst.wiremock.client.MappingBuilder
25 import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder
26 import com.github.tomakehurst.wiremock.client.VerificationException
27 import com.github.tomakehurst.wiremock.client.WireMock.aResponse
28 import com.github.tomakehurst.wiremock.client.WireMock.equalTo
29 import com.github.tomakehurst.wiremock.client.WireMock.equalToJson
30 import com.github.tomakehurst.wiremock.client.WireMock.request
31 import com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo
32 import com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig
33 import org.apache.http.HttpStatus
34 import org.apache.http.client.methods.HttpPost
35 import org.apache.http.entity.ContentType
36 import org.apache.http.entity.mime.HttpMultipartMode
37 import org.apache.http.entity.mime.MultipartEntityBuilder
38 import org.apache.http.impl.client.CloseableHttpClient
39 import org.apache.http.impl.client.HttpClientBuilder
40 import org.apache.http.message.BasicHeader
41 import org.hamcrest.CoreMatchers
42 import org.hamcrest.MatcherAssert.assertThat
43 import org.hamcrest.Matchers.equalToIgnoringCase
44 import org.jetbrains.kotlin.konan.util.prefixIfNot
45 import org.onap.ccsdk.cds.blueprintsprocessor.uat.logging.LogColor.COLOR_WIREMOCK
46 import org.onap.ccsdk.cds.blueprintsprocessor.uat.logging.LogColor.markerOf
47 import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintConstants.UAT_SPECIFICATION_FILE
48 import org.onap.ccsdk.cds.controllerblueprints.core.utils.BluePrintArchiveUtils.Companion.compressToBytes
49 import org.skyscreamer.jsonassert.JSONAssert
50 import org.skyscreamer.jsonassert.JSONCompareMode
51 import org.springframework.beans.factory.annotation.Autowired
52 import org.springframework.boot.web.server.LocalServerPort
53 import org.springframework.core.env.ConfigurableEnvironment
54 import org.springframework.core.env.MapPropertySource
55 import org.springframework.http.HttpHeaders
56 import org.springframework.http.MediaType
57 import org.springframework.test.context.ActiveProfiles
58 import org.springframework.test.context.support.TestPropertySourceUtils.INLINED_PROPERTIES_PROPERTY_SOURCE_NAME
59 import org.yaml.snakeyaml.Yaml
60 import java.nio.file.Paths
61 import kotlin.test.AfterTest
62 import kotlin.test.BeforeTest
63 import kotlin.test.Test
64 import kotlin.test.assertNotNull
65
66 @ActiveProfiles("uat")
67 @Suppress("MemberVisibilityCanBePrivate")
68 class UatServicesTest : BaseUatTest() {
69
70     companion object {
71         private const val BLUEPRINT_NAME = "pnf_config"
72         private val BLUEPRINT_BASE_DIR = Paths.get(UAT_BLUEPRINTS_BASE_DIR, BLUEPRINT_NAME)
73         private val UAT_PATH = BLUEPRINT_BASE_DIR.resolve(UAT_SPECIFICATION_FILE)
74         private val wireMockMarker = markerOf(COLOR_WIREMOCK)
75     }
76
77     @Autowired
78     lateinit var mapper: ObjectMapper
79
80     @Autowired
81     lateinit var environment: ConfigurableEnvironment
82
83     private val ephemeralProperties = mutableSetOf<String>()
84     private val startedMockServers = mutableListOf<WireMockServer>()
85
86     private fun setProperties(properties: Map<String, String>) {
87         inlinedPropertySource().putAll(properties)
88         ephemeralProperties += properties.keys
89     }
90
91     @AfterTest
92     fun resetProperties() {
93         val source = inlinedPropertySource()
94         ephemeralProperties.forEach { key -> source.remove(key) }
95         ephemeralProperties.clear()
96     }
97
98     @AfterTest
99     fun stopMockServers() {
100         startedMockServers.forEach { mockServer ->
101             try {
102                 mockServer.checkForUnmatchedRequests()
103             } finally {
104                 mockServer.stop()
105             }
106         }
107         startedMockServers.clear()
108     }
109
110     private fun inlinedPropertySource(): MutableMap<String, Any> =
111             (environment.propertySources[INLINED_PROPERTIES_PROPERTY_SOURCE_NAME] as MapPropertySource).source
112
113     @LocalServerPort
114     var localServerPort: Int = 0
115
116     // use lazy evaluation to postpone until localServerPort is injected by Spring
117     val baseUrl: String by lazy {
118         "http://127.0.0.1:$localServerPort"
119     }
120
121     lateinit var httpClient: CloseableHttpClient
122
123     @BeforeTest
124     fun setupHttpClient() {
125         val defaultHeaders = listOf(BasicHeader(org.apache.http.HttpHeaders.AUTHORIZATION,
126                 TestSecuritySettings.clientAuthToken()))
127         httpClient = HttpClientBuilder.create()
128                 .setDefaultHeaders(defaultHeaders)
129                 .build()
130     }
131
132     @Test
133     fun `verify service validates candidate UAT`() {
134         // GIVEN
135         val cbaBytes = compressToBytes(BLUEPRINT_BASE_DIR)
136         val multipartEntity = MultipartEntityBuilder.create()
137                 .setMode(HttpMultipartMode.BROWSER_COMPATIBLE)
138                 .addBinaryBody("cba", cbaBytes, ContentType.DEFAULT_BINARY, "cba.zip")
139                 .build()
140         val request = HttpPost("$baseUrl/api/v1/uat/verify").apply {
141             entity = multipartEntity
142         }
143
144         // WHEN
145         httpClient.execute(request) { response ->
146
147             // THEN
148             val statusLine = response.statusLine
149             assertThat(statusLine.statusCode, CoreMatchers.equalTo(HttpStatus.SC_OK))
150         }
151     }
152
153     @Test
154     fun `spy service generates complete UAT from bare UAT`() {
155         // GIVEN
156         val uatSpec = UAT_PATH.toFile().readText()
157         val fullUat = UatDefinition.load(mapper, uatSpec)
158         val expectedJson = mapper.writeValueAsString(fullUat)
159
160         val bareUatBytes = fullUat.toBare().dump(mapper).toByteArray()
161
162         fullUat.externalServices.forEach { service ->
163             val mockServer = createMockServer(service)
164             mockServer.start()
165             startedMockServers += mockServer
166             setPropertiesForMockServer(service, mockServer)
167         }
168
169         val cbaBytes = compressToBytes(BLUEPRINT_BASE_DIR)
170         val multipartEntity = MultipartEntityBuilder.create()
171                 .setMode(HttpMultipartMode.BROWSER_COMPATIBLE)
172                 .addBinaryBody("cba", cbaBytes, ContentType.DEFAULT_BINARY, "cba.zip")
173                 .addBinaryBody("uat", bareUatBytes, ContentType.DEFAULT_BINARY, "uat.yaml")
174                 .build()
175         val request = HttpPost("$baseUrl/api/v1/uat/spy").apply {
176             entity = multipartEntity
177         }
178
179         // WHEN
180         httpClient.execute(request) { response ->
181
182             // THEN
183             val statusLine = response.statusLine
184             assertThat(statusLine.statusCode, CoreMatchers.equalTo(HttpStatus.SC_OK))
185             val entity = response.entity
186             assertNotNull(entity)
187             val contentType = ContentType.get(entity)
188             assertThat(contentType.mimeType, equalToIgnoringCase("text/vnd.yaml"))
189             val yamlResponse = entity.content.bufferedReader().readText()
190             val jsonResponse = yamlToJson(yamlResponse)
191             JSONAssert.assertEquals(expectedJson, jsonResponse, JSONCompareMode.LENIENT)
192         }
193     }
194
195     private fun createMockServer(service: ServiceDefinition): WireMockServer {
196         val mockServer = WireMockServer(wireMockConfig()
197                 .dynamicPort()
198                 .notifier(MarkedSlf4jNotifier(wireMockMarker))
199         )
200         service.expectations.forEach { expectation ->
201
202             val request = expectation.request
203             val response = expectation.response
204             // WebTestClient always use absolute path, prefixing with "/" if necessary
205             val urlPattern = urlEqualTo(request.path.prefixIfNot("/"))
206             val mappingBuilder: MappingBuilder = request(request.method, urlPattern)
207             request.headers.forEach { (key, value) ->
208                 mappingBuilder.withHeader(key, equalTo(value))
209             }
210             if (request.body != null) {
211                 mappingBuilder.withRequestBody(equalToJson(mapper.writeValueAsString(request.body), true, true))
212             }
213
214             val responseDefinitionBuilder: ResponseDefinitionBuilder = aResponse()
215                     .withStatus(response.status)
216             if (response.body != null) {
217                 responseDefinitionBuilder.withBody(mapper.writeValueAsBytes(response.body))
218                         .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
219             }
220
221             mappingBuilder.willReturn(responseDefinitionBuilder)
222
223             mockServer.stubFor(mappingBuilder)
224         }
225         return mockServer
226     }
227
228     private fun setPropertiesForMockServer(service: ServiceDefinition, mockServer: WireMockServer) {
229         val selector = service.selector
230         val httpPort = mockServer.port()
231         val properties = mapOf(
232                 "blueprintsprocessor.restclient.$selector.type" to "basic-auth",
233                 "blueprintsprocessor.restclient.$selector.url" to "http://localhost:$httpPort/",
234                 // TODO credentials should be validated
235                 "blueprintsprocessor.restclient.$selector.username" to "admin",
236                 "blueprintsprocessor.restclient.$selector.password" to "Kp8bJ4SXszM0WXlhak3eHlcse2gAw84vaoGGmJvUy2U"
237         )
238         setProperties(properties)
239     }
240
241     /**
242      * Borrowed from com.github.tomakehurst.wiremock.junit.WireMockRule.checkForUnmatchedRequests
243      */
244     private fun WireMockServer.checkForUnmatchedRequests() {
245         val unmatchedRequests = findAllUnmatchedRequests()
246         if (unmatchedRequests.isNotEmpty()) {
247             val nearMisses = findNearMissesForAllUnmatchedRequests()
248             if (nearMisses.isEmpty()) {
249                 throw VerificationException.forUnmatchedRequests(unmatchedRequests)
250             } else {
251                 throw VerificationException.forUnmatchedNearMisses(nearMisses)
252             }
253         }
254     }
255
256     private fun yamlToJson(yaml: String): String {
257         val map: Map<String, Any> = Yaml().load(yaml)
258         return mapper.writeValueAsString(map)
259     }
260 }