diff --git a/api-gateway/caching/build.gradle.kts b/api-gateway/caching/build.gradle.kts index 64b0812..d1dede6 100644 --- a/api-gateway/caching/build.gradle.kts +++ b/api-gateway/caching/build.gradle.kts @@ -26,6 +26,7 @@ configurations { dependencies { subprojects.forEach { "dist"(project(":${it.name}")) } "wiremockExtensions"("com.opentable:wiremock-body-transformer:1.1.3") { isTransitive = false } + testCompile("com.github.tomakehurst:wiremock:2.24.1") } sourceSets.named("test") { @@ -50,5 +51,5 @@ tasks.named("build") { dependsOn(downloadWireMockExtensions, "runFunctionalTest") } -apply(from = "https://raw.githubusercontent.com/Knotx/knotx-starter-kit/${project.property("knotxVersion")}/gradle/docker.gradle.kts") +apply(from = "docker.gradle.kts") apply(from = "https://raw.githubusercontent.com/Knotx/knotx-starter-kit/${project.property("knotxVersion")}/gradle/javaAndUnitTests.gradle.kts") \ No newline at end of file diff --git a/api-gateway/caching/docker.gradle.kts b/api-gateway/caching/docker.gradle.kts new file mode 100644 index 0000000..cadfae9 --- /dev/null +++ b/api-gateway/caching/docker.gradle.kts @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2019 Knot.x Project + * + * 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. + */ +import com.bmuschko.gradle.docker.shaded.io.netty.util.internal.PlatformDependent.isWindows +import com.bmuschko.gradle.docker.tasks.container.DockerCreateContainer +import com.bmuschko.gradle.docker.tasks.container.DockerStartContainer +import com.bmuschko.gradle.docker.tasks.container.DockerStopContainer +import com.bmuschko.gradle.docker.tasks.container.extras.DockerWaitHealthyContainer +import com.bmuschko.gradle.docker.tasks.image.DockerBuildImage +import com.bmuschko.gradle.docker.tasks.image.DockerRemoveImage +import org.apache.tools.ant.taskdefs.condition.Os.FAMILY_UNIX +import org.apache.tools.ant.taskdefs.condition.Os.isFamily +import java.time.Duration + +val dockerImageRef = "$buildDir/.docker/buildImage-imageId.txt" + +tasks.register("copyDockerfile") { + group = "docker" + from("docker") + into("$buildDir") + expand("knotx_version" to project.property("knotxVersion")) + mustRunAfter("cleanDistribution") +} + +tasks.register("removeImage") { + group = "docker" + val spec = object : Spec { + override fun isSatisfiedBy(task: Task): Boolean { + return File(dockerImageRef).exists() + } + } + onlyIf(spec) + + targetImageId(if (File(dockerImageRef).exists()) File(dockerImageRef).readText() else "") + onError { + if (!this.message!!.contains("No such image")) + throw this + } +} + +tasks.register ("buildImage") { + group = "docker" + inputDir.set(file("$buildDir")) + images.add("${project.property("docker.image.name")}:latest") + dependsOn("removeImage", "prepareDocker") +} +val buildImage = tasks.named("buildImage") + +// FUNCTIONAL TESTS + +tasks.register("createContainer") { + group = "docker-functional-tests" + dependsOn(buildImage) + targetImageId(buildImage.get().imageId) + hostConfig.portBindings.set(listOf("8092:8092", "18092:18092")) + hostConfig.autoRemove.set(true) + exposePorts("tcp", listOf(8092, 18092)) + // allow container to access host's network + dependsOn("configureUnix", "configureNonUnix") +} + +tasks.register("configureUnix"){ + doLast { + createContainer { + hostConfig.network.set("host") + withEnvVar("EXTERNAL_API_DOMAIN", "localhost") + } + } + onlyIf { isFamily(FAMILY_UNIX) } +} + +tasks.register("configureNonUnix"){ + doLast { + createContainer { + withEnvVar("EXTERNAL_API_DOMAIN", "host.docker.internal") + } + } + onlyIf { !isFamily(FAMILY_UNIX) } +} + +val createContainer = tasks.named("createContainer") + +tasks.register("startContainer") { + group = "docker-functional-tests" + dependsOn(createContainer) + targetContainerId(createContainer.get().containerId) +} + +tasks.register("waitContainer") { + group = "docker-functional-tests" + dependsOn(tasks.named("startContainer")) + targetContainerId(createContainer.get().containerId) + awaitStatusTimeout.set(60) // in seconds +} + +tasks.register("stopContainer") { + group = "docker-functional-tests" + targetContainerId(createContainer.get().containerId) +} + +/** Deprecated */ +tasks.register("runTest", Test::class) { + dependsOn(tasks.named("runFunctionalTest")) +} + +tasks.register("runFunctionalTest", Test::class) { + group = "docker-functional-tests" + dependsOn(tasks.named("waitContainer")) + finalizedBy(tasks.named("stopContainer")) + include("**/*ITCase*") +} + +tasks.register("prepareDocker") { + dependsOn("cleanDistribution", "overwriteCustomFiles", "copyDockerfile") +} diff --git a/api-gateway/caching/functional/src/test/java/com/project/test/functional/CacheITCase.java b/api-gateway/caching/functional/src/test/java/com/project/test/functional/CacheITCase.java new file mode 100644 index 0000000..06c49fd --- /dev/null +++ b/api-gateway/caching/functional/src/test/java/com/project/test/functional/CacheITCase.java @@ -0,0 +1,61 @@ +package com.project.test.functional; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static io.restassured.RestAssured.given; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.extension.responsetemplating.ResponseTemplateTransformer; +import org.junit.Assert; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + + +class CacheITCase { + + private WireMockServer wireMockServer = new WireMockServer(options() + .port(8080) + .usingFilesUnderDirectory("../../common-services/webapi") + .extensions(new ResponseTemplateTransformer(true))); + + @BeforeEach + public void setup() { + wireMockServer.start(); + wireMockServer.stubFor(get(urlEqualTo("/product/id")) + .willReturn(aResponse().withStatus(200) + .withTransformers("response-template") + .withBodyFile("product.json"))); + } + + @Test + @DisplayName("Expect the same response when invoking API twice.") + void callProxyApiAndExpectCachedResponse() { + // @formatter:off + String firstReponse = + given() + .port(8092) + .when() + .get("/product/id") + .then() + .statusCode(200) + .extract() + .asString(); + + String secondRespone = + given() + .port(8092) + .when() + .get("/product/id") + .then() + .statusCode(200) + .extract() + .asString(); + + Assert.assertEquals(firstReponse, secondRespone); + // @formatter:on + } + +} diff --git a/api-gateway/caching/knotx/conf/logback.xml b/api-gateway/caching/knotx/conf/logback.xml new file mode 100644 index 0000000..afa7885 --- /dev/null +++ b/api-gateway/caching/knotx/conf/logback.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n + + + + + + + + + + + \ No newline at end of file diff --git a/api-gateway/caching/knotx/conf/routes/handlers/fragments-handler.conf b/api-gateway/caching/knotx/conf/routes/handlers/fragments-handler.conf index 1899a1b..4ccb01b 100644 --- a/api-gateway/caching/knotx/conf/routes/handlers/fragments-handler.conf +++ b/api-gateway/caching/knotx/conf/routes/handlers/fragments-handler.conf @@ -33,6 +33,7 @@ actions { endpointOptions { path = /product/id domain = webapi + domain = ${?EXTERNAL_API_DOMAIN} // environment variable provided during functional tests port = 8080 allowedRequestHeaders = ["Content-Type"] } diff --git a/api-gateway/caching/knotx/conf/routes/operations.conf b/api-gateway/caching/knotx/conf/routes/operations.conf index e635d06..54d7cbe 100644 --- a/api-gateway/caching/knotx/conf/routes/operations.conf +++ b/api-gateway/caching/knotx/conf/routes/operations.conf @@ -1,3 +1,8 @@ +fragmentsHandler = { + name = fragmentsHandler + config = {include required(classpath("routes/handlers/fragments-handler.conf"))} +} + routingOperations = ${routingOperations} [ { operationId = product-api-caching-proxy-operation @@ -11,11 +16,7 @@ routingOperations = ${routingOperations} [ } } }, - { - name = fragmentsHandler - config = { include required(classpath("routes/handlers/fragments-handler.conf")) } - }, - + ${fragmentsHandler}, { name = fragmentsAssembler }