From 4aae4a2ae3e32e81e543ab26d01f16f5a6ccab41 Mon Sep 17 00:00:00 2001 From: slinkydeveloper Date: Tue, 23 Aug 2022 14:09:58 +0200 Subject: [PATCH] Test the HTTP Endpoint --- contracts/build.gradle.kts | 2 +- .../e2e/utils/InjectHttpEndpointURL.kt | 3 + .../dev/restate/e2e/utils/RestateDeployer.kt | 45 ++++++++++++-- .../e2e/utils/RestateDeployerExtension.kt | 17 ++++-- tests/build.gradle.kts | 4 +- .../dev/restate/e2e/HttpEndpointTest.kt | 61 +++++++++++++++++++ 6 files changed, 120 insertions(+), 12 deletions(-) create mode 100644 test-utils/src/main/kotlin/dev/restate/e2e/utils/InjectHttpEndpointURL.kt create mode 100644 tests/src/test/kotlin/dev/restate/e2e/HttpEndpointTest.kt diff --git a/contracts/build.gradle.kts b/contracts/build.gradle.kts index ebfcf8eb..b0e487b0 100644 --- a/contracts/build.gradle.kts +++ b/contracts/build.gradle.kts @@ -29,7 +29,7 @@ protobuf { it.generateDescriptorSet = true it.descriptorSetOptions.includeImports = true it.descriptorSetOptions.path = - "${rootProject.projectDir}/.restate/descriptors/${it.sourceSet.name}.descriptor" + "${rootProject.projectDir}/.restate/descriptors/${project.name}.descriptor" } } } diff --git a/test-utils/src/main/kotlin/dev/restate/e2e/utils/InjectHttpEndpointURL.kt b/test-utils/src/main/kotlin/dev/restate/e2e/utils/InjectHttpEndpointURL.kt new file mode 100644 index 00000000..4ded028c --- /dev/null +++ b/test-utils/src/main/kotlin/dev/restate/e2e/utils/InjectHttpEndpointURL.kt @@ -0,0 +1,3 @@ +package dev.restate.e2e.utils + +@Target(AnnotationTarget.VALUE_PARAMETER) annotation class InjectHttpEndpointURL diff --git a/test-utils/src/main/kotlin/dev/restate/e2e/utils/RestateDeployer.kt b/test-utils/src/main/kotlin/dev/restate/e2e/utils/RestateDeployer.kt index c76f3671..54c3b2a9 100644 --- a/test-utils/src/main/kotlin/dev/restate/e2e/utils/RestateDeployer.kt +++ b/test-utils/src/main/kotlin/dev/restate/e2e/utils/RestateDeployer.kt @@ -23,17 +23,24 @@ private constructor( private val additionalContainers: Map>, private val additionalConfig: Map, private val runtimeContainerName: String, + private val descriptorFile: String?, ) { companion object { private const val RESTATE_RUNTIME_CONTAINER_ENV = "RESTATE_RUNTIME_CONTAINER" private const val DEFAULT_RUNTIME_CONTAINER = "restatedev.jfrog.io/restatedev-docker-local/runtime" - private const val RUNTIME_GRPC_ENDPOINT = 8090 - private const val IMAGE_PULL_POLICY = "E2E_IMAGE_PULL_POLICY" + private const val CONTAINER_LOGS_DIR_ENV = "CONTAINER_LOGS_DIR" + + private const val IMAGE_PULL_POLICY_ENV = "E2E_IMAGE_PULL_POLICY" private const val ALWAYS_PULL = "always" + private const val DESCRIPTORS_FILE_ENV = "DESCRIPTORS_FILE" + + private const val RUNTIME_GRPC_ENDPOINT = 8090 + private const val RUNTIME_HTTP_ENDPOINT = 8091 + private val logger = LogManager.getLogger(RestateDeployer::class.java) @JvmStatic @@ -58,6 +65,7 @@ private constructor( private var additionalConfig: MutableMap = mutableMapOf(), private var runtimeContainer: String = System.getenv(RESTATE_RUNTIME_CONTAINER_ENV) ?: DEFAULT_RUNTIME_CONTAINER, + private var descriptorDirectory: String? = System.getenv(DESCRIPTORS_FILE_ENV), ) { fun withFunction(functionSpec: FunctionSpec) = apply { this.functions.add(functionSpec) } @@ -96,7 +104,12 @@ private constructor( fun build() = RestateDeployer( - runtimeDeployments, functions, additionalContainers, additionalConfig, runtimeContainer) + runtimeDeployments, + functions, + additionalContainers, + additionalConfig, + runtimeContainer, + descriptorDirectory) } fun deploy(testClass: Class<*>) { @@ -141,6 +154,10 @@ private constructor( config["consensus"] = mapOf("storage_type" to "Memory") config["grpc_port"] = RUNTIME_GRPC_ENDPOINT config["services"] = functionContainers.values.flatMap { it.first.toManifests(mapper) } + if (descriptorFile != null) { + config["http_endpoint"] = + mapOf("port" to RUNTIME_HTTP_ENDPOINT, "descriptors_path" to "/contracts.descriptor") + } config.putAll(additionalConfig) mapper.writeValue(configFile, config) @@ -163,9 +180,15 @@ private constructor( MountableFile.forHostPath(configFile.toPath()), "/restate.yaml") .withCommand("--id 1 --configuration-file /restate.yaml") - if (System.getenv(IMAGE_PULL_POLICY) == ALWAYS_PULL) { + if (System.getenv(IMAGE_PULL_POLICY_ENV) == ALWAYS_PULL) { runtimeContainer!!.withImagePullPolicy(PullPolicy.alwaysPull()) } + if (descriptorFile != null) { + runtimeContainer!! + .withCopyFileToContainer( + MountableFile.forHostPath(descriptorFile), "/contracts.descriptor") + .addExposedPort(RUNTIME_HTTP_ENDPOINT) + } runtimeContainer!!.start() @@ -187,6 +210,18 @@ private constructor( "Runtime is not configured, as RestateDeployer::deploy has not been invoked") } + fun getRuntimeHttpEndpointUrl(): URL { + checkNotNull(descriptorFile) { + "No descriptor directory is configured to start the HTTP Endpoint. " + + "Make sure when running tests you have the $DESCRIPTORS_FILE_ENV environment variable correctly configured" + } + return runtimeContainer?.getMappedPort(RUNTIME_HTTP_ENDPOINT)?.let { + URL("http", "127.0.0.1", it, "/") + } + ?: throw java.lang.IllegalStateException( + "Runtime is not configured, as RestateDeployer::deploy has not been invoked") + } + fun getAdditionalContainerExposedPort(hostName: String, port: Int): String { return additionalContainers[hostName]?.let { "${it.host}:${it.getMappedPort(port)}" } ?: throw java.lang.IllegalStateException( @@ -196,7 +231,7 @@ private constructor( private fun computeContainerTestLogsDir(testClass: Class<*>): Path { val formatter = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss") return Path.of( - System.getenv("CONTAINER_LOGS_DIR")!!, + System.getenv(CONTAINER_LOGS_DIR_ENV)!!, "${testClass.canonicalName}_${LocalDateTime.now().format(formatter)}") } } diff --git a/test-utils/src/main/kotlin/dev/restate/e2e/utils/RestateDeployerExtension.kt b/test-utils/src/main/kotlin/dev/restate/e2e/utils/RestateDeployerExtension.kt index 64358d84..0875b851 100644 --- a/test-utils/src/main/kotlin/dev/restate/e2e/utils/RestateDeployerExtension.kt +++ b/test-utils/src/main/kotlin/dev/restate/e2e/utils/RestateDeployerExtension.kt @@ -5,6 +5,7 @@ import io.grpc.ManagedChannel import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder import io.grpc.stub.AbstractBlockingStub import io.grpc.stub.AbstractStub +import java.net.URL import java.util.concurrent.TimeUnit import org.junit.jupiter.api.extension.* import org.junit.jupiter.api.extension.ExtensionContext.Namespace @@ -15,7 +16,7 @@ class RestateDeployerExtension(private val deployer: RestateDeployer) : companion object { private val NAMESPACE = Namespace.create(RestateDeployerExtension::class.java) - private val MANAGED_CHANNEL_KEY = "ManagedChannelKey" + private const val MANAGED_CHANNEL_KEY = "ManagedChannelKey" } override fun beforeAll(context: ExtensionContext) { @@ -45,7 +46,9 @@ class RestateDeployerExtension(private val deployer: RestateDeployer) : return (parameterContext.isAnnotated(InjectBlockingStub::class.java) && AbstractBlockingStub::class.java.isAssignableFrom(parameterContext.parameter.type)) || (parameterContext.isAnnotated(InjectContainerAddress::class.java) && - String::class.java.isAssignableFrom(parameterContext.parameter.type)) + String::class.java.isAssignableFrom(parameterContext.parameter.type)) || + (parameterContext.isAnnotated(InjectHttpEndpointURL::class.java) && + URL::class.java.isAssignableFrom(parameterContext.parameter.type)) } override fun resolveParameter( @@ -56,6 +59,8 @@ class RestateDeployerExtension(private val deployer: RestateDeployer) : resolveBlockingStub(parameterContext, extensionContext) } else if (parameterContext.isAnnotated(InjectContainerAddress::class.java)) { resolveContainerAddress(parameterContext) + } else if (parameterContext.isAnnotated(InjectHttpEndpointURL::class.java)) { + resolveHttpEndpointURL() } else { null } @@ -75,9 +80,7 @@ class RestateDeployerExtension(private val deployer: RestateDeployer) : .getStore(NAMESPACE) .get(MANAGED_CHANNEL_KEY, ManagedChannelResource::class.java) - var stub: T = stubFactoryMethod.invoke(null, channelResource.channel) as T - - return stub + return stubFactoryMethod.invoke(null, channelResource.channel) as T } private fun resolveContainerAddress(parameterContext: ParameterContext): Any { @@ -86,6 +89,10 @@ class RestateDeployerExtension(private val deployer: RestateDeployer) : return deployer.getAdditionalContainerExposedPort(annotation.hostName, annotation.port) } + private fun resolveHttpEndpointURL(): Any { + return deployer.getRuntimeHttpEndpointUrl() + } + private class ManagedChannelResource(val channel: ManagedChannel) : Store.CloseableResource { override fun close() { // Shutdown channel diff --git a/tests/build.gradle.kts b/tests/build.gradle.kts index 0319617b..20b23809 100644 --- a/tests/build.gradle.kts +++ b/tests/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { testImplementation(platform(libs.jackson.bom)) testImplementation(libs.jackson.core) + testImplementation(libs.jackson.databind) testImplementation(libs.jackson.yaml) testImplementation(platform(libs.cloudevents.bom)) @@ -42,5 +43,6 @@ tasks.withType { mapOf( "CONTAINER_LOGS_DIR" to "$buildDir/test-results/container-logs", "RESTATE_RUNTIME_CONTAINER" to "ghcr.io/restatedev/runtime:main", - ) + "DESCRIPTORS_FILE" to + "${rootProject.projectDir}/.restate/descriptors/contracts.descriptor") } diff --git a/tests/src/test/kotlin/dev/restate/e2e/HttpEndpointTest.kt b/tests/src/test/kotlin/dev/restate/e2e/HttpEndpointTest.kt new file mode 100644 index 00000000..44994b70 --- /dev/null +++ b/tests/src/test/kotlin/dev/restate/e2e/HttpEndpointTest.kt @@ -0,0 +1,61 @@ +package dev.restate.e2e + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import dev.restate.e2e.functions.counter.CounterGrpc +import dev.restate.e2e.utils.InjectHttpEndpointURL +import dev.restate.e2e.utils.RestateDeployer +import dev.restate.e2e.utils.RestateDeployerExtension +import java.net.URI +import java.net.URL +import java.net.http.HttpClient +import java.net.http.HttpRequest +import java.net.http.HttpResponse +import java.nio.charset.StandardCharsets +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +class HttpEndpointTest { + + companion object { + @RegisterExtension + val deployerExt: RestateDeployerExtension = + RestateDeployerExtension( + RestateDeployer.Builder().withFunction(Containers.COUNTER_FUNCTION_SPEC).build()) + + private val objMapper = ObjectMapper() + + private val jacksonBodySubscriber: HttpResponse.BodySubscriber = + HttpResponse.BodySubscribers.mapping( + HttpResponse.BodySubscribers.ofString(StandardCharsets.UTF_8), objMapper::readTree) + + private val jacksonBodyHandler: HttpResponse.BodyHandler = + HttpResponse.BodyHandler { jacksonBodySubscriber } + + private fun jacksonBodyPublisher(value: Any): HttpRequest.BodyPublisher { + return HttpRequest.BodyPublishers.ofString(objMapper.writeValueAsString(value)) + } + } + + @Test + fun getAndAdd(@InjectHttpEndpointURL httpEndpointURL: URL) { + val client = HttpClient.newHttpClient() + + val req = + HttpRequest.newBuilder( + URI.create("$httpEndpointURL${CounterGrpc.getGetAndAddMethod().fullMethodName}")) + .POST(jacksonBodyPublisher(mapOf("counter_name" to "my-counter", "value" to 1))) + .headers("Content-Type", "application/json") + .build() + + val response = client.send(req, jacksonBodyHandler) + + assertThat(response.statusCode()).isEqualTo(200) + assertThat(response.headers().firstValue("content-type")) + .get() + .asString() + .contains("application/json") + assertThat(response.body().get("newValue").asInt()).isEqualTo(1) + } +}