diff options
Diffstat (limited to 'ms/blueprintsprocessor/application/src/test/kotlin')
5 files changed, 444 insertions, 0 deletions
diff --git a/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/BlueprintsAcceptanceTests.kt b/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/BlueprintsAcceptanceTests.kt new file mode 100644 index 000000000..0a57277ea --- /dev/null +++ b/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/BlueprintsAcceptanceTests.kt @@ -0,0 +1,280 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2019 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ +package org.onap.ccsdk.cds.blueprintsprocessor + +import com.fasterxml.jackson.databind.ObjectMapper +import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.argThat +import com.nhaarman.mockitokotlin2.atLeast +import com.nhaarman.mockitokotlin2.atLeastOnce +import com.nhaarman.mockitokotlin2.eq +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions +import com.nhaarman.mockitokotlin2.whenever +import org.junit.ClassRule +import org.junit.Rule +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.onap.ccsdk.cds.blueprintsprocessor.rest.RestLibConstants +import org.onap.ccsdk.cds.blueprintsprocessor.rest.service.BluePrintRestLibPropertyService +import org.onap.ccsdk.cds.blueprintsprocessor.rest.service.BlueprintWebClientService +import org.onap.ccsdk.cds.blueprintsprocessor.rest.service.BlueprintWebClientService.WebClientResponse +import org.onap.ccsdk.cds.controllerblueprints.core.utils.BluePrintArchiveUtils.Companion.compressToBytes +import org.skyscreamer.jsonassert.JSONAssert +import org.skyscreamer.jsonassert.JSONCompareMode +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.mock.mockito.MockBean +import org.springframework.core.io.ByteArrayResource +import org.springframework.core.io.Resource +import org.springframework.http.MediaType +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.context.TestPropertySource +import org.springframework.test.context.junit4.rules.SpringClassRule +import org.springframework.test.context.junit4.rules.SpringMethodRule +import org.springframework.test.web.reactive.server.WebTestClient +import org.yaml.snakeyaml.Yaml +import reactor.core.publisher.Mono +import java.io.File +import java.nio.file.Path +import java.nio.file.Paths +import kotlin.test.BeforeTest +import kotlin.test.Test + +@RunWith(Parameterized::class) +// Set blueprintsprocessor.httpPort=0 to trigger a random port selection +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +@AutoConfigureWebTestClient(timeout = "PT10S") +@ContextConfiguration(initializers = [ + WorkingFoldersInitializer::class, + TestSecuritySettings.ServerContextInitializer::class +]) +@TestPropertySource(locations = ["classpath:application-test.properties"]) +@Suppress("UNCHECKED_CAST") +class BlueprintsAcceptanceTests(private val blueprintName: String, private val filename: String) { + + companion object { + const val UAT_BLUEPRINTS_BASE_DIR = "../../../components/model-catalog/blueprint-model/uat-blueprints" + const val EMBEDDED_UAT_FILE = "Tests/uat.yaml" + + @ClassRule + @JvmField + val springClassRule = SpringClassRule() + + val log: Logger = LoggerFactory.getLogger(BlueprintsAcceptanceTests::class.java) + + @Parameterized.Parameters(name = "{index} {0}") + @JvmStatic + fun filenames(): List<Array<String>> { + return File(UAT_BLUEPRINTS_BASE_DIR) + .listFiles { file -> file.isDirectory && File(file, EMBEDDED_UAT_FILE).isFile } + ?.map { file -> arrayOf(file.nameWithoutExtension, file.canonicalPath) } + ?: emptyList() + } + } + + @Rule + @JvmField + val springMethodRule = SpringMethodRule() + + @MockBean(name = RestLibConstants.SERVICE_BLUEPRINT_REST_LIB_PROPERTY) + lateinit var restClientFactory: BluePrintRestLibPropertyService + + @Autowired + // Bean is created programmatically by {@link WorkingFoldersInitializer#initialize(String)} + @Suppress("SpringJavaInjectionPointsAutowiringInspection") + lateinit var tempFolder: ExtendedTemporaryFolder + + @Autowired + lateinit var webTestClient: WebTestClient + + @Autowired + lateinit var mapper: ObjectMapper + + @BeforeTest + fun cleanupTemporaryFolder() { + tempFolder.deleteAllFiles() + } + + @Test + fun testBlueprint() { + val yaml: Map<String, *> = loadYaml(Paths.get(filename, EMBEDDED_UAT_FILE)) + + uploadBlueprint(blueprintName) + + // Configure mocked external services + val services = yaml["external-services"] as List<Map<String, *>>? ?: emptyList() + val expectationPerClient = services.map { service -> + val selector = service["selector"] as String + val expectations = (service["expectations"] as List<Map<String, *>>).map { + parseExpectation(it) + } + val mockClient = createRestClientMock(selector, expectations) + mockClient to expectations + }.toMap() + + // Run processes + for (process in (yaml["processes"] as List<Map<String, *>>)) { + val processName = process["name"] + log.info("Executing process '$processName'") + val request = mapper.writeValueAsString(process["request"]) + val expectedResponse = mapper.writeValueAsString(process["expectedResponse"]) + processBlueprint(request, expectedResponse) + } + + // Validate request payloads + for ((mockClient, expectations) in expectationPerClient) { + expectations.forEach { expectation -> + verify(mockClient, atLeastOnce()).exchangeResource( + eq(expectation.method), + eq(expectation.path), + argThat { assertJsonEqual(expectation.expectedRequestBody, this) }, + expectation.requestHeadersMatcher()) + } + // Don't mind the invocations to the overloaded exchangeResource(String, String, String) + verify(mockClient, atLeast(0)).exchangeResource(any(), any(), any()) + verifyNoMoreInteractions(mockClient) + } + } + + private fun createRestClientMock(selector: String, restExpectations: List<RestExpectation>): BlueprintWebClientService { + val restClient = mock<BlueprintWebClientService>(verboseLogging = true) + + // Delegates to overloaded exchangeResource(String, String, String, Map<String, String>) + whenever(restClient.exchangeResource(any(), any(), any())) + .thenAnswer { invocation -> + val method = invocation.arguments[0] as String + val path = invocation.arguments[1] as String + val request = invocation.arguments[2] as String + restClient.exchangeResource(method, path, request, emptyMap()) + } + for (expectation in restExpectations) { + whenever(restClient.exchangeResource( + eq(expectation.method), + eq(expectation.path), + any(), + any())) + .thenReturn(WebClientResponse(expectation.statusCode, expectation.responseBody)) + } + + whenever(restClientFactory.blueprintWebClientService(selector)) + .thenReturn(restClient) + return restClient + } + + private fun uploadBlueprint(blueprintName: String) { + val body = toMultiValueMap("file", getBlueprintAsResource(blueprintName)) + webTestClient + .post() + .uri("/api/v1/execution-service/upload") + .header("Authorization", TestSecuritySettings.clientAuthToken()) + .syncBody(body) + .exchange() + .expectStatus().isOk + } + + private fun processBlueprint(request: String, expectedResponse: String) { + webTestClient + .post() + .uri("/api/v1/execution-service/process") + .header("Authorization", TestSecuritySettings.clientAuthToken()) + .contentType(MediaType.APPLICATION_JSON_UTF8) + .body(Mono.just(request), String::class.java) + .exchange() + .expectStatus().isOk + .expectBody() + .json(expectedResponse) + } + + private fun getBlueprintAsResource(blueprintName: String): Resource { + val baseDir = Paths.get(UAT_BLUEPRINTS_BASE_DIR, blueprintName) + val zipBytes = compressToBytes(baseDir) + return object : ByteArrayResource(zipBytes) { + // Filename has to be returned in order to be able to post + override fun getFilename() = "$blueprintName.zip" + } + } + + private fun loadYaml(path: Path): Map<String, Any> { + return path.toFile().reader().use { reader -> + Yaml().load(reader) + } + } + + private fun assertJsonEqual(expected: Any, actual: String): Boolean { + if (actual != expected) { + // assertEquals throws an exception whenever match fails + JSONAssert.assertEquals(mapper.writeValueAsString(expected), actual, JSONCompareMode.LENIENT) + } + return true + } + + private fun parseExpectation(expectation: Map<String, *>): RestExpectation { + val request = expectation["request"] as Map<String, Any> + val method = request["method"] as String + val path = joinPath(request.getValue("path")) + val contentType = request["content-type"] as String? + val requestBody = request.getOrDefault("body", "") + + val response = expectation["response"] as Map<String, Any>? ?: emptyMap() + val status = response["status"] as Int? ?: 200 + val responseBody = when (val body = response["body"] ?: "") { + is String -> body + else -> mapper.writeValueAsString(body) + } + + return RestExpectation(method, path, contentType, requestBody, status, responseBody) + } + + /** + * Join a multilevel lists of strings. + * Example: joinPath(listOf("a", listOf("b", "c"), "d")) will result in "a/b/c/d". + */ + private fun joinPath(any: Any): String { + fun recursiveJoin(any: Any, sb: StringBuilder): StringBuilder { + when (any) { + is List<*> -> any.filterNotNull().forEach { recursiveJoin(it, sb) } + is String -> { + if (sb.isNotEmpty()) { + sb.append('/') + } + sb.append(any) + } + else -> throw IllegalArgumentException("Unsupported type: ${any.javaClass}") + } + return sb + } + + return recursiveJoin(any, StringBuilder()).toString() + } + + data class RestExpectation(val method: String, val path: String, val contentType: String?, + val expectedRequestBody: Any, + val statusCode: Int, val responseBody: String) { + + fun requestHeadersMatcher(): Map<String, String> { + return if (contentType != null) eq(mapOf("Content-Type" to contentType)) else any() + } + } +}
\ No newline at end of file diff --git a/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/CollectionUtils2.kt b/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/CollectionUtils2.kt new file mode 100644 index 000000000..63d64cae4 --- /dev/null +++ b/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/CollectionUtils2.kt @@ -0,0 +1,31 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2019 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ +package org.onap.ccsdk.cds.blueprintsprocessor + +import org.springframework.util.CollectionUtils +import org.springframework.util.MultiValueMap + + +/** + * Convenient method to create a single-entry MultiValueMap. + */ +fun <K, V> toMultiValueMap(key: K, vararg values: V): MultiValueMap<K, V> { + return CollectionUtils.toMultiValueMap(mapOf(key to values.asList())) +} diff --git a/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ExtendedTemporaryFolder.kt b/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ExtendedTemporaryFolder.kt new file mode 100644 index 000000000..4576f2761 --- /dev/null +++ b/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ExtendedTemporaryFolder.kt @@ -0,0 +1,40 @@ +package org.onap.ccsdk.cds.blueprintsprocessor + +import org.junit.rules.TemporaryFolder +import java.io.File +import java.io.IOException +import java.nio.file.FileVisitResult +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.SimpleFileVisitor +import java.nio.file.attribute.BasicFileAttributes +import javax.annotation.PreDestroy + +class ExtendedTemporaryFolder { + private val tempFolder = TemporaryFolder() + + init { + tempFolder.create() + } + + @PreDestroy + fun delete() = tempFolder.delete() + + /** + * A delegate to org.junit.rules.TemporaryFolder.TemporaryFolder.newFolder(String). + */ + fun newFolder(folder: String): File = tempFolder.newFolder(folder) + + /** + * Delete all files under the root temporary folder recursively. The folders are preserved. + */ + fun deleteAllFiles() { + Files.walkFileTree(tempFolder.root.toPath(), object : SimpleFileVisitor<Path>() { + @Throws(IOException::class) + override fun visitFile(file: Path?, attrs: BasicFileAttributes?): FileVisitResult { + file?.toFile()?.delete() + return FileVisitResult.CONTINUE + } + }) + } +} diff --git a/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/TestSecuritySettings.kt b/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/TestSecuritySettings.kt new file mode 100644 index 000000000..f7ab2554c --- /dev/null +++ b/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/TestSecuritySettings.kt @@ -0,0 +1,45 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2019 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ +package org.onap.ccsdk.cds.blueprintsprocessor + +import org.springframework.context.ApplicationContextInitializer +import org.springframework.context.ConfigurableApplicationContext +import org.springframework.test.context.support.TestPropertySourceUtils +import org.springframework.util.Base64Utils +import java.nio.charset.StandardCharsets + +class TestSecuritySettings { + companion object { + private const val authUsername = "walter.white" + private const val authPassword = "Heisenberg" + + fun clientAuthToken() = + "Basic " + Base64Utils.encodeToString("$authUsername:$authPassword".toByteArray(StandardCharsets.UTF_8)) + } + + class ServerContextInitializer : ApplicationContextInitializer<ConfigurableApplicationContext> { + override fun initialize(context: ConfigurableApplicationContext) { + TestPropertySourceUtils.addInlinedPropertiesToEnvironment(context, + "security.user.name=$authUsername", + "security.user.password={noop}$authPassword" + ) + } + } +} diff --git a/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/WorkingFoldersInitializer.kt b/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/WorkingFoldersInitializer.kt new file mode 100644 index 000000000..37615cb1a --- /dev/null +++ b/ms/blueprintsprocessor/application/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/WorkingFoldersInitializer.kt @@ -0,0 +1,48 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2019 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ +package org.onap.ccsdk.cds.blueprintsprocessor + +import org.springframework.beans.factory.support.BeanDefinitionBuilder +import org.springframework.beans.factory.support.BeanDefinitionRegistry +import org.springframework.context.ApplicationContextInitializer +import org.springframework.context.ConfigurableApplicationContext +import org.springframework.stereotype.Component +import org.springframework.test.context.support.TestPropertySourceUtils + +@Component +class WorkingFoldersInitializer : ApplicationContextInitializer<ConfigurableApplicationContext> { + + override fun initialize(context: ConfigurableApplicationContext) { + val tempFolder = ExtendedTemporaryFolder() + val properties = listOf("Deploy", "Archive", "Working") + .map { "blueprintsprocessor.blueprint${it}Path=${tempFolder.newFolder(it)}" } + .toTypedArray() + TestPropertySourceUtils.addInlinedPropertiesToEnvironment(context, *properties) + // Expose tempFolder as a bean so it can be accessed via DI + registerSingleton(context, "tempFolder", ExtendedTemporaryFolder::class.java, tempFolder) + } + + @Suppress("SameParameterValue") + private fun <T> registerSingleton(context: ConfigurableApplicationContext, + beanName: String, beanClass: Class<T>, instance: T) { + val builder = BeanDefinitionBuilder.genericBeanDefinition(beanClass) { instance } + (context.beanFactory as BeanDefinitionRegistry).registerBeanDefinition(beanName, builder.beanDefinition) + } +} |