From 547fd1eca28a5b6ba767ece57f2307f944a857a3 Mon Sep 17 00:00:00 2001 From: cif Date: Sat, 19 Oct 2024 08:23:34 +0100 Subject: [PATCH] feat: add grpc endpoint test harness --- build.gradle.kts | 25 +++++---- gradle.properties | 6 +-- .../kotlin/com/stabledata/grpc/GrpcService.kt | 4 +- .../com/stabledata/ApplicationTestHelpers.kt | 51 +++++++++++++++++++ .../kotlin/com/stabledata/SchemaGrpcTest.kt | 36 +++++++++++++ 5 files changed, 105 insertions(+), 17 deletions(-) create mode 100644 src/test/kotlin/com/stabledata/SchemaGrpcTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index ae895de..19aac6e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,8 +1,8 @@ import com.google.gradle.osdetector.OsDetector -val kotlin_version: String by project -val logback_version: String by project -val ktor_version: String by project +val kotlinVersion: String by project +val logbackVersion: String by project +val ktorVersion: String by project plugins { kotlin("jvm") version "2.0.20" @@ -148,11 +148,11 @@ dependencies { implementation("io.ktor:ktor-server-netty-jvm") implementation("io.ktor:ktor-server-content-negotiation:2.2.4") - implementation("io.ktor:ktor-server-openapi:$ktor_version") - implementation("io.ktor:ktor-server-cors:$ktor_version") + implementation("io.ktor:ktor-server-openapi:$ktorVersion") + implementation("io.ktor:ktor-server-cors:$ktorVersion") implementation("io.ktor:ktor-server-status-pages:2.0.0") - implementation("io.ktor:ktor-server-auth:$ktor_version") - implementation("io.ktor:ktor-server-auth-jwt:$ktor_version") + implementation("io.ktor:ktor-server-auth:$ktorVersion") + implementation("io.ktor:ktor-server-auth-jwt:$ktorVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0") @@ -165,11 +165,11 @@ dependencies { // grpc - implementation("io.grpc:grpc-netty-shaded:1.42.1") + implementation("io.grpc:grpc-netty-shaded:1.48.1") // FIXME: this is still vulnerable! // https://github.com/protocolbuffers/protobuf/security/advisories/GHSA-735f-pc8j-v9w8 implementation("io.grpc:grpc-protobuf:1.68.0") - implementation("io.grpc:grpc-stub:1.57.2") + implementation("io.grpc:grpc-stub:1.68.0") implementation("javax.annotation:javax.annotation-api:1.3.2") implementation("io.grpc:grpc-services:1.42.1") @@ -197,12 +197,11 @@ dependencies { // tests testImplementation("io.ktor:ktor-server-test-host-jvm") - testImplementation("org.jetbrains.kotlin:kotlin-test:$kotlin_version") - testImplementation("io.ktor:ktor-server-test-host:$ktor_version") + testImplementation("org.jetbrains.kotlin:kotlin-test:$kotlinVersion") + testImplementation("io.ktor:ktor-server-test-host:$ktorVersion") testImplementation("io.kotest:kotest-runner-junit5:5.9.0") testImplementation("io.kotest:kotest-framework-engine:5.9.0") testImplementation("io.github.serpro69:kotlin-faker:1.16.0") testImplementation("io.mockk:mockk:1.13.4") - testImplementation("io.grpc:grpc-testing:1.66.0") - + testImplementation("io.grpc:grpc-testing:1.68.0") } diff --git a/gradle.properties b/gradle.properties index d507b58..f1ef81c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ kotlin.code.style=official -ktor_version=2.3.12 -kotlin_version=2.0.20 -logback_version=1.4.14 \ No newline at end of file +ktorVersion=2.3.12 +kotlinVersion=2.0.20 +logbackVersion=1.4.14 \ No newline at end of file diff --git a/src/main/kotlin/com/stabledata/grpc/GrpcService.kt b/src/main/kotlin/com/stabledata/grpc/GrpcService.kt index d9f1f08..0688fba 100644 --- a/src/main/kotlin/com/stabledata/grpc/GrpcService.kt +++ b/src/main/kotlin/com/stabledata/grpc/GrpcService.kt @@ -65,12 +65,14 @@ class SchemaService : SchemaServiceGrpc.SchemaServiceImplBase() { responseObserver: StreamObserver ) { val collection = Collection.fromMessage(request) + val token = GrpcContextInterceptor.tokenContext.get(Context.current()) + val tmp = LogEntry( id = collection.id, teamId = "bar", path = collection.path, actorId = "123", - eventType = "an event", + eventType = "an event $token", createdAt = 1232312342343, confirmedAt = 234234324234 ) diff --git a/src/test/kotlin/com/stabledata/ApplicationTestHelpers.kt b/src/test/kotlin/com/stabledata/ApplicationTestHelpers.kt index 4515298..c91061f 100644 --- a/src/test/kotlin/com/stabledata/ApplicationTestHelpers.kt +++ b/src/test/kotlin/com/stabledata/ApplicationTestHelpers.kt @@ -1,6 +1,10 @@ package com.stabledata import com.stabledata.context.StableEventIdHeader +import com.stabledata.grpc.GrpcContextInterceptor +import io.grpc.* +import io.grpc.inprocess.InProcessChannelBuilder +import io.grpc.inprocess.InProcessServerBuilder import io.ktor.client.request.* import io.ktor.client.statement.* import io.ktor.http.* @@ -31,4 +35,51 @@ suspend fun ApplicationTestBuilder.postJson( contentType(ContentType.Application.Json) setBody(body()) } +} + +class MetadataInterceptor(private val metadata: Metadata) : ClientInterceptor { + override fun interceptCall( + method: MethodDescriptor, + callOptions: CallOptions, + next: Channel + ): ClientCall { + return object : ForwardingClientCall.SimpleForwardingClientCall( + next.newCall(method, callOptions) + ) { + override fun start(responseListener: Listener, headers: Metadata) { + // Add the metadata to the call + headers.merge(metadata) + super.start(responseListener, headers) + } + } + } +} + +fun grpcTest( + serviceImpl: BindableService, // The actual gRPC service implementation + stubCreator: (ManagedChannel) -> T, // Function to create the service stub + test: (stub: T) -> Unit // The test function, receiving the stub +) { + val serverName = InProcessServerBuilder.generateName() + + val server = InProcessServerBuilder + .forName(serverName) + .directExecutor() + .intercept(GrpcContextInterceptor()) + .addService(serviceImpl) + .build() + .start() + + val channel = InProcessChannelBuilder + .forName(serverName) + .directExecutor() + .build() + + try { + val stub = stubCreator(channel) + test(stub) + } finally { + channel.shutdown() + server.shutdown() + } } \ No newline at end of file diff --git a/src/test/kotlin/com/stabledata/SchemaGrpcTest.kt b/src/test/kotlin/com/stabledata/SchemaGrpcTest.kt new file mode 100644 index 0000000..6bc445f --- /dev/null +++ b/src/test/kotlin/com/stabledata/SchemaGrpcTest.kt @@ -0,0 +1,36 @@ +package com.stabledata + +import com.stabledata.context.StableEventIdHeader +import com.stabledata.grpc.SchemaService +import io.grpc.Metadata +import io.kotest.core.spec.style.WordSpec +import io.ktor.http.* +import stable.Schema +import stable.SchemaServiceGrpc + +class SchemaGrpcTest:WordSpec({ + "returns unauthorized" should { + grpcTest( + serviceImpl = SchemaService(), + stubCreator = { channel -> SchemaServiceGrpc.newBlockingStub(channel) } + ) { stub -> + + val metadata = Metadata().apply { + put(Metadata.Key.of(HttpHeaders.Authorization, Metadata.ASCII_STRING_MARSHALLER), "Bearer some-token") + put(Metadata.Key.of(StableEventIdHeader, Metadata.ASCII_STRING_MARSHALLER), "12345") + } + + // Create a call options object to pass the metadata interceptor + val callOptions = stub.withInterceptors(MetadataInterceptor(metadata)) + + val request = Schema.CollectionRequest.newBuilder() + .setId("collection-123") + .setPath("/example/path") + .build() + + val response = callOptions.createCollection(request) + assert(response != null) + } + + } +}) \ No newline at end of file