Skip to content

Commit

Permalink
feat: add grpc endpoint test harness
Browse files Browse the repository at this point in the history
  • Loading branch information
cif committed Oct 19, 2024
1 parent bb6481d commit 547fd1e
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 17 deletions.
25 changes: 12 additions & 13 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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")

Expand All @@ -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")

Expand Down Expand Up @@ -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")
}
6 changes: 3 additions & 3 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
kotlin.code.style=official
ktor_version=2.3.12
kotlin_version=2.0.20
logback_version=1.4.14
ktorVersion=2.3.12
kotlinVersion=2.0.20
logbackVersion=1.4.14
4 changes: 3 additions & 1 deletion src/main/kotlin/com/stabledata/grpc/GrpcService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,14 @@ class SchemaService : SchemaServiceGrpc.SchemaServiceImplBase() {
responseObserver: StreamObserver<LogEntryMessage>
) {
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
)
Expand Down
51 changes: 51 additions & 0 deletions src/test/kotlin/com/stabledata/ApplicationTestHelpers.kt
Original file line number Diff line number Diff line change
@@ -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.*
Expand Down Expand Up @@ -31,4 +35,51 @@ suspend fun ApplicationTestBuilder.postJson(
contentType(ContentType.Application.Json)
setBody(body())
}
}

class MetadataInterceptor(private val metadata: Metadata) : ClientInterceptor {
override fun <ReqT : Any?, RespT : Any?> interceptCall(
method: MethodDescriptor<ReqT, RespT>,
callOptions: CallOptions,
next: Channel
): ClientCall<ReqT, RespT> {
return object : ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(
next.newCall(method, callOptions)
) {
override fun start(responseListener: Listener<RespT>, headers: Metadata) {
// Add the metadata to the call
headers.merge(metadata)
super.start(responseListener, headers)
}
}
}
}

fun <T : Any> 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()
}
}
36 changes: 36 additions & 0 deletions src/test/kotlin/com/stabledata/SchemaGrpcTest.kt
Original file line number Diff line number Diff line change
@@ -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)
}

}
})

0 comments on commit 547fd1e

Please sign in to comment.