Skip to content

Commit

Permalink
Introduce sdk-test module to provide testing utilities (#142)
Browse files Browse the repository at this point in the history
* Generate admin client
* Introduce sdk-test module to provide testing utilities.
This module provides an easy to use opinionated test toolkit to get started with testing your service with a Restate container. The container is automatically configured and deployed together with the services on the local JVM, respecting the lifecycle of JUnit 5 tests. We also provide easy to use parameter injection to send requests to services. See CounterTest for an example.

Added new allowed licenses due to the testcontainers transitive dependencies.
  • Loading branch information
slinkydeveloper authored Nov 14, 2023
1 parent f75ecf4 commit 7a226b3
Show file tree
Hide file tree
Showing 17 changed files with 740 additions and 1 deletion.
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ jobs:
with:
java-version: ${{ matrix.java }}
distribution: 'adopt'
- name: Log in to the Container registry
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
with:
registry: ghcr.io
username: ${{ secrets.GH_PACKAGE_READ_ACCESS_USER }}
password: ${{ secrets.GH_PACKAGE_READ_ACCESS_TOKEN }}
- name: Validate Gradle wrapper
uses: gradle/wrapper-validation-action@v1
- name: Build with Gradle
Expand Down
68 changes: 68 additions & 0 deletions admin-client/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import net.ltgt.gradle.errorprone.errorprone
import org.openapitools.generator.gradle.plugin.tasks.GenerateTask

plugins {
`java-library`
`maven-publish`
id("org.openapi.generator") version "6.6.0"
}

dependencies {
implementation(platform(jacksonLibs.jackson.bom))
implementation(jacksonLibs.jackson.core)
implementation(jacksonLibs.jackson.databind)
implementation(jacksonLibs.jackson.jsr310)
implementation("org.openapitools:jackson-databind-nullable:0.2.6")

// Required for the annotations
compileOnly(coreLibs.javax.annotation.api)
compileOnly("com.google.code.findbugs:jsr305:3.0.2")
}

// Add generated output to source sets
sourceSets { main { java.srcDir(tasks.named("openApiGenerate")) } }

// Configure openapi generator
tasks.withType<GenerateTask> {
inputSpec.set("$projectDir/src/main/openapi/meta.json")

// Java 9+ HTTP Client using Jackson
generatorName.set("java")
library.set("native")

// Package names
invokerPackage.set("dev.restate.admin.client")
apiPackage.set("dev.restate.admin.api")
modelPackage.set("dev.restate.admin.model")

// We don't need these
generateApiTests.set(false)
generateApiDocumentation.set(false)
generateModelTests.set(false)
generateModelDocumentation.set(false)

finalizedBy("spotlessJava")
}

tasks.withType<JavaCompile>().configureEach {
targetCompatibility = "11"
sourceCompatibility = "11"

// Disable errorprone for this module
options.errorprone.disableAllChecks.set(true)
}

configure<com.diffplug.gradle.spotless.SpotlessExtension> {
java { targetExclude(fileTree("$buildDir/generate-resources") { include("**/*.java") }) }
}

publishing {
publications {
register<MavenPublication>("maven") {
groupId = "dev.restate.sdk"
artifactId = "admin-client"

from(components["java"])
}
}
}
1 change: 1 addition & 0 deletions admin-client/src/main/openapi/meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"openapi":"3.0.0","info":{"title":"Admin API","version":"0.2.2"},"paths":{"/subscriptions":{"post":{"tags":["subscription"],"summary":"Create subscription","description":"Create subscription.","operationId":"create_subscription","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSubscriptionRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionResponse"}}}},"400":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"403":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"404":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"409":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"500":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"503":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}}}}},"/health":{"get":{"tags":["health"],"summary":"Health check","description":"Check REST API Health.","operationId":"health","responses":{"200":{"description":"OK"}}}},"/services/{service}/methods/{method}":{"get":{"tags":["service_method"],"summary":"Get service method","description":"Get the method of a service","operationId":"get_service_method","parameters":[{"name":"service","in":"path","description":"Fully qualified service name.","required":true,"schema":{"type":"string"}},{"name":"method","in":"path","description":"Method name.","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetServiceMethodResponse"}}}},"400":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"403":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"404":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"409":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"500":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"503":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}}}}},"/services/{service}":{"patch":{"tags":["service"],"summary":"Modify a service","description":"Modify a registered service.","operationId":"modify_service","parameters":[{"name":"service","in":"path","description":"Fully qualified service name.","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ModifyServiceRequest"}}},"required":true},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServiceMetadata"}}}},"400":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"403":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"404":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"409":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"500":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"503":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}}}}},"/subscriptions/{subscription}":{"delete":{"tags":["subscription"],"summary":"Delete subscription","description":"Delete subscription.","operationId":"delete_subscription","parameters":[{"name":"subscription","in":"path","description":"Subscription identifier","required":true,"schema":{"type":"string"}}],"responses":{"202":{"description":"Accepted"},"400":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"403":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"404":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"409":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"500":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"503":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}}}}},"/openapi":{"get":{"tags":["openapi"],"summary":"OpenAPI specification","externalDocs":{"url":"https://swagger.io/specification/"},"operationId":"openapi_spec","responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"}}}}}}}},"/endpoints":{"post":{"tags":["service_endpoint"],"summary":"Create service endpoint","description":"Create service endpoint. Restate will invoke the endpoint to gather additional information required for registration, such as the services exposed by the service endpoint and their Protobuf descriptor. If the service endpoint is already registered, this method will fail unless `force` is set to `true`.","operationId":"create_service_endpoint","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterServiceEndpointRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterServiceEndpointResponse"}}}},"400":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"403":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"404":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"409":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"500":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"503":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}}}}},"/services":{"get":{"tags":["service"],"summary":"List services","description":"List all registered services.","operationId":"list_services","responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListServicesResponse"}}}},"400":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"403":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"404":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"409":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"500":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"503":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}}}}},"/services/{service}/methods":{"get":{"tags":["service_method"],"summary":"List service methods","description":"List all the methods of the given service.","operationId":"list_service_methods","parameters":[{"name":"service","in":"path","description":"Fully qualified service name.","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListServiceMethodsResponse"}}}},"400":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"403":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"404":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"409":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"500":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"503":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}}}}},"/endpoints/{endpoint}":{"delete":{"tags":["service_endpoint"],"summary":"Delete service endpoint","description":"Delete service endpoint. Currently it's supported to remove a service endpoint only using the force flag","operationId":"delete_service_endpoint","parameters":[{"name":"endpoint","in":"path","description":"Endpoint identifier","required":true,"schema":{"type":"string"}},{"name":"force","in":"query","description":"If true, the service endpoint will be forcefully deleted. This might break in-flight invocations, use with caution.","style":"simple","schema":{"type":"boolean"}}],"responses":{"202":{"description":"Accepted"},"501":{"description":"Not implemented. Only using the force flag is supported at the moment."},"400":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"403":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"404":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"409":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"500":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"503":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}}}}},"/invocations/{invocation_id}":{"delete":{"tags":["invocation"],"summary":"Kill an invocation","description":"Kill the given invocation. When killing, consistency is not guaranteed for service instance state, in-flight invocation to other services, etc. Future releases will support graceful invocation cancellation.","operationId":"cancel_invocation","parameters":[{"name":"invocation_id","in":"path","description":"Invocation identifier.","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":""},"400":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"403":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"404":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"409":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"500":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}},"503":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDescriptionResponse"}}}}}}}},"components":{"schemas":{"CreateSubscriptionRequest":{"type":"object","required":["sink","source"],"properties":{"id":{"title":"Identifier","description":"Identifier of the subscription. If not specified, one will be auto-generated.","type":"string","nullable":true},"source":{"title":"Source","description":"Source uri. Accepted forms:\n\n* `kafka://<cluster_name>/<topic_name>`, e.g. `service://my-cluster/my-topic`","type":"string"},"sink":{"title":"Sink","description":"Sink uri. Accepted forms:\n\n* `service://<service_name>/<method_name>`, e.g. `service://com.example.MySvc/MyMethod`","type":"string"},"options":{"title":"Options","description":"Additional options to apply to the subscription.","type":"object","additionalProperties":{"type":"string"},"nullable":true}}},"SubscriptionResponse":{"type":"object","required":["id","options","sink","source"],"properties":{"id":{"type":"string"},"source":{"type":"string"},"sink":{"type":"string"},"options":{"type":"object","additionalProperties":{"type":"string"}}}},"ErrorDescriptionResponse":{"title":"Error description response","description":"Error details of the response","type":"object","required":["message"],"properties":{"message":{"type":"string"},"restate_code":{"title":"Restate code","description":"Restate error code describing this error","type":"string","nullable":true}}},"GetServiceMethodResponse":{"type":"object","required":["method_name","service_name"],"properties":{"service_name":{"type":"string"},"method_name":{"type":"string"}}},"ModifyServiceRequest":{"type":"object","required":["public"],"properties":{"public":{"title":"Public","description":"If true, the service can be invoked through the ingress. If false, the service can be invoked only from another Restate service.","type":"boolean"}}},"ServiceMetadata":{"type":"object","required":["endpoint_id","instance_type","methods","name","public","revision"],"properties":{"name":{"type":"string"},"methods":{"type":"array","items":{"type":"string"}},"instance_type":{"$ref":"#/components/schemas/InstanceType"},"endpoint_id":{"title":"Endpoint Id","description":"Endpoint exposing the latest revision of the service.","type":"string"},"revision":{"title":"Revision","description":"Latest revision of the service.","type":"integer","format":"uint32","minimum":0.0},"public":{"title":"Public","description":"If true, the service can be invoked through the ingress. If false, the service can be invoked only from another Restate service.","type":"boolean"}}},"InstanceType":{"type":"string","enum":["Keyed","Unkeyed","Singleton"]},"RegisterServiceEndpointRequest":{"type":"object","required":["uri"],"properties":{"uri":{"title":"Uri","description":"Uri to use to discover/invoke the service endpoint.","type":"string"},"additional_headers":{"title":"Additional headers","description":"Additional headers added to the discover/invoke requests to the service endpoint.","type":"object","additionalProperties":{"type":"string"},"nullable":true},"force":{"title":"Force","description":"If `true`, it will override, if existing, any endpoint using the same `uri`. Beware that this can lead in-flight invocations to an unrecoverable error state.\n\nBy default, this is `true` but it might change in future to `false`.\n\nSee the [versioning documentation](https://docs.restate.dev/services/upgrades-removal) for more information.","default":true,"type":"boolean"}}},"RegisterServiceEndpointResponse":{"type":"object","required":["id","services"],"properties":{"id":{"type":"string"},"services":{"type":"array","items":{"$ref":"#/components/schemas/RegisterServiceResponse"}}}},"RegisterServiceResponse":{"type":"object","required":["name","revision"],"properties":{"name":{"type":"string"},"revision":{"type":"integer","format":"uint32","minimum":0.0}}},"ListServicesResponse":{"type":"object","required":["services"],"properties":{"services":{"type":"array","items":{"$ref":"#/components/schemas/ServiceMetadata"}}}},"ListServiceMethodsResponse":{"type":"object","required":["methods"],"properties":{"methods":{"type":"array","items":{"type":"string"}}}}}}}
9 changes: 9 additions & 0 deletions config/allowed-licenses.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@
},
{
"moduleLicense": "The 3-Clause BSD License"
},
{
"moduleLicense": "The 2-Clause BSD License"
},
{
"moduleLicense": "Eclipse Public License - v 2.0"
},
{
"moduleLicense": "Eclipse Public License - v 1.0"
}
]
}
48 changes: 48 additions & 0 deletions sdk-test/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import com.google.protobuf.gradle.id

plugins {
`java-library`
`maven-publish`
}

dependencies {
api(project(":sdk-core"))
api(testingLibs.junit.api)
api(testingLibs.testcontainers.core)

implementation(project(":admin-client"))
implementation(project(":sdk-http-vertx"))
implementation(coreLibs.log4j.api)
implementation(platform(vertxLibs.vertx.bom))
implementation(vertxLibs.vertx.core)
implementation(coreLibs.grpc.netty.shaded)

testCompileOnly(coreLibs.javax.annotation.api)
testImplementation(project(":sdk-java-blocking"))
testImplementation(testingLibs.assertj)
testImplementation(testingLibs.junit.jupiter)
testImplementation(coreLibs.grpc.stub)
testImplementation(coreLibs.grpc.protobuf)
testImplementation(coreLibs.log4j.core)
}

publishing {
publications {
register<MavenPublication>("maven") {
groupId = "dev.restate.sdk"
artifactId = "sdk-test"

from(components["java"])
}
}
}

// Protobuf codegen for tests

protobuf {
plugins {
id("grpc") { artifact = "io.grpc:protoc-gen-grpc-java:${coreLibs.versions.grpc.get()}" }
}

generateProtoTasks { ofSourceSet("test").forEach { it.plugins { id("grpc") } } }
}
Loading

0 comments on commit 7a226b3

Please sign in to comment.