diff --git a/build.gradle b/build.gradle index 9b20896..90cf761 100644 --- a/build.gradle +++ b/build.gradle @@ -9,6 +9,7 @@ buildscript { plugins { id 'java-library' + id 'java-test-fixtures' id 'signing' id 'maven-publish' id 'idea' @@ -34,7 +35,7 @@ allprojects { ext { versions = [ - wiremock: "3.9.1", + wiremock: "3.10.0", grpc : "1.68.0", protobuf: "3.25.5" ] @@ -104,14 +105,20 @@ dependencies { implementation 'javax.annotation:javax.annotation-api:1.3.2' - // testImplementation project(":") - testImplementation(platform('org.junit:junit-bom:5.11.0')) - testImplementation "org.junit.jupiter:junit-jupiter" - testImplementation "org.hamcrest:hamcrest-core:3.0" - testImplementation "org.hamcrest:hamcrest-library:3.0" - testImplementation 'org.awaitility:awaitility:4.2.2' + testFixturesApi(platform('org.junit:junit-bom:5.11.0')) + testFixturesApi "org.junit.jupiter:junit-jupiter" + testFixturesApi "org.hamcrest:hamcrest-core:3.0" + testFixturesApi "org.hamcrest:hamcrest-library:3.0" + testFixturesApi 'org.awaitility:awaitility:4.2.2' - testImplementation "io.grpc:grpc-okhttp" + testFixturesApi "io.grpc:grpc-okhttp" + + testFixturesApi 'javax.annotation:javax.annotation-api:1.3.2' + + testImplementation(testFixtures(project(":")), { + exclude group: 'org.eclipse.jetty' + exclude group: 'org.eclipse.jetty.http2' + }) } task sourcesJar(type: Jar, dependsOn: classes) { @@ -231,8 +238,8 @@ protobuf { task.generateDescriptorSet = true task.descriptorSetOptions.includeImports = true } - ofSourceSet('test').each { task -> - task.descriptorSetOptions.path = "$projectDir/src/test/resources/wiremock/grpc/greetings.dsc" + ofSourceSet('testFixtures').each { task -> + task.descriptorSetOptions.path = "$projectDir/src/testFixtures/resources/wiremock/grpc/greetings.dsc" } ofSourceSet('bookings').each { task -> task.descriptorSetOptions.path = "$projectDir/src/test/resources/wiremock/grpc/bookings.dsc" @@ -243,6 +250,7 @@ protobuf { processTestResources.dependsOn generateProto processTestResources.dependsOn generateTestProto processTestResources.dependsOn generateBookingsProto +processTestFixturesResources.dependsOn generateTestFixturesProto nexusPublishing { // See https://github.com/wiremock/community/blob/main/infra/maven-central.md diff --git a/settings.gradle b/settings.gradle index 820a9dc..3a50d41 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,4 @@ rootProject.name = 'wiremock-grpc-extension' include 'wiremock-grpc-extension-standalone' +include 'wiremock-grpc-extension-jetty12' diff --git a/src/main/java/org/wiremock/grpc/GrpcExtensionFactory.java b/src/main/java/org/wiremock/grpc/GrpcExtensionFactory.java index c59acce..923580d 100644 --- a/src/main/java/org/wiremock/grpc/GrpcExtensionFactory.java +++ b/src/main/java/org/wiremock/grpc/GrpcExtensionFactory.java @@ -18,18 +18,19 @@ import com.github.tomakehurst.wiremock.extension.Extension; import com.github.tomakehurst.wiremock.extension.ExtensionFactory; import com.github.tomakehurst.wiremock.extension.WireMockServices; +import com.github.tomakehurst.wiremock.http.HttpServerFactoryLoader; import java.util.List; -import org.wiremock.annotations.Beta; -import org.wiremock.grpc.internal.GrpcAdminApiExtension; import org.wiremock.grpc.internal.GrpcHttpServerFactory; -@Beta(justification = "Incubating extension: https://github.com/wiremock/wiremock/issues/2383") public class GrpcExtensionFactory implements ExtensionFactory { @Override public List create(WireMockServices services) { - GrpcHttpServerFactory grpcHttpServerFactory = - new GrpcHttpServerFactory(services.getStores().getBlobStore("grpc")); - return List.of(new GrpcAdminApiExtension(grpcHttpServerFactory), grpcHttpServerFactory); + return List.of(new GrpcHttpServerFactory(services.getStores().getBlobStore("grpc"))); + } + + @Override + public boolean isLoadable() { + return HttpServerFactoryLoader.isJetty11(); } } diff --git a/src/main/java/org/wiremock/grpc/internal/GrpcHttpServerFactory.java b/src/main/java/org/wiremock/grpc/internal/GrpcHttpServerFactory.java index c6d23ce..bf96f3a 100644 --- a/src/main/java/org/wiremock/grpc/internal/GrpcHttpServerFactory.java +++ b/src/main/java/org/wiremock/grpc/internal/GrpcHttpServerFactory.java @@ -15,12 +15,11 @@ */ package org.wiremock.grpc.internal; +import com.github.tomakehurst.wiremock.admin.Router; import com.github.tomakehurst.wiremock.common.Exceptions; import com.github.tomakehurst.wiremock.core.Options; -import com.github.tomakehurst.wiremock.http.AdminRequestHandler; -import com.github.tomakehurst.wiremock.http.HttpServer; -import com.github.tomakehurst.wiremock.http.HttpServerFactory; -import com.github.tomakehurst.wiremock.http.StubRequestHandler; +import com.github.tomakehurst.wiremock.extension.AdminApiExtension; +import com.github.tomakehurst.wiremock.http.*; import com.github.tomakehurst.wiremock.jetty11.Jetty11HttpServer; import com.github.tomakehurst.wiremock.store.BlobStore; import com.google.protobuf.DescriptorProtos; @@ -33,10 +32,10 @@ import org.eclipse.jetty.servlet.FilterHolder; import org.eclipse.jetty.servlet.ServletContextHandler; -public class GrpcHttpServerFactory implements HttpServerFactory { +public class GrpcHttpServerFactory implements HttpServerFactory, AdminApiExtension { private final BlobStore protoDescriptorStore; - private GrpcFilter grpcFilter; + protected GrpcFilter grpcFilter; public GrpcHttpServerFactory(BlobStore protoDescriptorStore) { this.protoDescriptorStore = protoDescriptorStore; @@ -93,4 +92,9 @@ protected void decorateMockServiceContextBeforeConfig( } }; } + + @Override + public void contributeAdminApiRoutes(Router router) { + router.add(RequestMethod.POST, "/ext/grpc/reset", new GrpcResetAdminApiTask(this)); + } } diff --git a/src/test/java/org/wiremock/grpc/client/AnotherGreetingsClient.java b/src/testFixtures/java/org/wiremock/grpc/client/AnotherGreetingsClient.java similarity index 100% rename from src/test/java/org/wiremock/grpc/client/AnotherGreetingsClient.java rename to src/testFixtures/java/org/wiremock/grpc/client/AnotherGreetingsClient.java diff --git a/src/test/java/org/wiremock/grpc/client/GreetingsClient.java b/src/testFixtures/java/org/wiremock/grpc/client/GreetingsClient.java similarity index 100% rename from src/test/java/org/wiremock/grpc/client/GreetingsClient.java rename to src/testFixtures/java/org/wiremock/grpc/client/GreetingsClient.java diff --git a/src/test/proto/ExampleServices.proto b/src/testFixtures/proto/ExampleServices.proto similarity index 100% rename from src/test/proto/ExampleServices.proto rename to src/testFixtures/proto/ExampleServices.proto diff --git a/src/test/proto/request/helloRequest.proto b/src/testFixtures/proto/request/helloRequest.proto similarity index 100% rename from src/test/proto/request/helloRequest.proto rename to src/testFixtures/proto/request/helloRequest.proto diff --git a/src/test/proto/request/models/sentiment.proto b/src/testFixtures/proto/request/models/sentiment.proto similarity index 100% rename from src/test/proto/request/models/sentiment.proto rename to src/testFixtures/proto/request/models/sentiment.proto diff --git a/src/test/proto/response/helloResponse.proto b/src/testFixtures/proto/response/helloResponse.proto similarity index 100% rename from src/test/proto/response/helloResponse.proto rename to src/testFixtures/proto/response/helloResponse.proto diff --git a/src/testFixtures/resources/wiremock/grpc/greetings.dsc b/src/testFixtures/resources/wiremock/grpc/greetings.dsc new file mode 100644 index 0000000..adc96c5 Binary files /dev/null and b/src/testFixtures/resources/wiremock/grpc/greetings.dsc differ diff --git a/src/testFixtures/resources/wiremock/mappings/hello-world.json b/src/testFixtures/resources/wiremock/mappings/hello-world.json new file mode 100644 index 0000000..66bd934 --- /dev/null +++ b/src/testFixtures/resources/wiremock/mappings/hello-world.json @@ -0,0 +1,37 @@ +{ + "mappings": [{ + "request": { + "headers": {"Accept": {"or": [ + {"equalTo": "*/*"}, + {"equalTo": "text/plain"} + ]}}, + "method": "GET", + "urlPath": "/hello" + }, + "metadata": {"mocklab": { + "response-example-attachment": "Hello World", + "created": { + "at": "2024-01-09T15:32:35.424189986Z", + "by": "vqe9q", + "via": "ADMIN_API" + }, + "updated": { + "at": "2024-01-09T15:34:11.584464236Z", + "by": "vqe9q", + "via": "ADMIN_API" + } + }}, + "response": { + "headers": {"Content-Type": "text/plain"}, + "body": "Hello World!", + "status": 200 + }, + "name": "Hello", + "postServeActions": [], + "id": "d469c289-22fb-4186-90ab-deb6ffb6db5c", + "persistent": true, + "priority": 5, + "uuid": "d469c289-22fb-4186-90ab-deb6ffb6db5c" + }], + "meta": {"total": 1} +} diff --git a/wiremock-grpc-extension-jetty12/build.gradle b/wiremock-grpc-extension-jetty12/build.gradle new file mode 100644 index 0000000..36ed123 --- /dev/null +++ b/wiremock-grpc-extension-jetty12/build.gradle @@ -0,0 +1,146 @@ +buildscript { + repositories { + maven { + url "https://oss.sonatype.org" + } + mavenCentral() + } +} + +plugins { + id 'java-library' + id 'java-test-fixtures' + id 'signing' + id 'maven-publish' + id 'idea' + id 'eclipse' + id 'project-report' + id 'com.diffplug.spotless' version '6.25.0' + id 'com.github.johnrengelman.shadow' version '8.1.1' + id "com.google.protobuf" version "0.9.4" +} + +repositories { + mavenLocal() + mavenCentral() +} + +group 'org.wiremock' + +dependencies { + api project(":"), { + exclude group: 'org.eclipse.jetty' + exclude group: 'org.eclipse.jetty.http2' + } + api "org.wiremock:wiremock-jetty12:$versions.wiremock" + + testImplementation(testFixtures(project(":")), { + exclude group: 'org.eclipse.jetty' + exclude group: 'org.eclipse.jetty.http2' + }) +} + +task sourcesJar(type: Jar, dependsOn: classes) { + archiveClassifier.set('sources') + from sourceSets.main.allSource +} + +task javadocJar(type: Jar, dependsOn: javadoc) { + archiveClassifier.set('javadoc') + from javadoc.destinationDir +} + +task testJar(type: Jar, dependsOn: testClasses) { + archiveClassifier.set('tests') + from sourceSets.test.output +} + +publishing { + repositories { + maven { + name = "GitHubPackages" + url = "https://maven.pkg.github.com/wiremock/wiremock-grpc-extension" + credentials { + username = System.getenv("GITHUB_ACTOR") + password = System.getenv("GITHUB_TOKEN") + } + } + } + + publications { + main(MavenPublication) { publication -> + from components.java + artifact sourcesJar + artifact javadocJar + artifact testJar + + pom.packaging 'jar' + pom.withXml { + asNode().appendNode('description', 'Mock gRPC services with WireMock') + asNode().children().last() + pomInfo + } + } + } +} + +signing { + // Docs: https://github.com/wiremock/community/blob/main/infra/maven-central.md + required { + !version.toString().contains("SNAPSHOT") && (gradle.taskGraph.hasTask("uploadArchives") || gradle.taskGraph.hasTask("publish") || gradle.taskGraph.hasTask("publishToMavenLocal")) + } + def signingKey = providers.environmentVariable("OSSRH_GPG_SECRET_KEY").orElse("").get() + def signingPassphrase = providers.environmentVariable("OSSRH_GPG_SECRET_KEY_PASSWORD").orElse("").get() + if (!signingKey.isEmpty() && !signingPassphrase.isEmpty()) { + println "Using PGP key from env vars" + useInMemoryPgpKeys(signingKey, signingPassphrase) + } else { + println "Using default PGP key" + } + + sign publishing.publications +} + +assemble.dependsOn clean, shadowJar +publishMainPublicationToMavenLocal.dependsOn jar +publishMainPublicationToGitHubPackagesRepository.dependsOn jar + + +task localRelease { + dependsOn clean, assemble, publishToMavenLocal +} + + +test { + useJUnitPlatform() + testLogging { + events "PASSED", "FAILED", "SKIPPED" + exceptionFormat "full" + } +} + +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:4.27.5" + } + + plugins { + grpc { + artifact = "io.grpc:protoc-gen-grpc-java:$versions.grpc" + } + } + generateProtoTasks { + all()*.plugins { + grpc { + outputSubDir = 'java' + } + } + + all().each { task -> + task.generateDescriptorSet = true + task.descriptorSetOptions.path = "$projectDir/src/test/resources/wiremock/grpc/services.dsc" + } + } +} + +processTestResources.dependsOn generateProto +processTestResources.dependsOn generateTestProto diff --git a/wiremock-grpc-extension-jetty12/src/main/java/org/wiremock/grpc/Jetty12GrpcExtensionFactory.java b/wiremock-grpc-extension-jetty12/src/main/java/org/wiremock/grpc/Jetty12GrpcExtensionFactory.java new file mode 100644 index 0000000..bb9e482 --- /dev/null +++ b/wiremock-grpc-extension-jetty12/src/main/java/org/wiremock/grpc/Jetty12GrpcExtensionFactory.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023-2024 Thomas Akehurst + * + * 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. + */ +package org.wiremock.grpc; + +import com.github.tomakehurst.wiremock.extension.Extension; +import com.github.tomakehurst.wiremock.extension.ExtensionFactory; +import com.github.tomakehurst.wiremock.extension.WireMockServices; +import java.util.List; + +import com.github.tomakehurst.wiremock.http.HttpServerFactoryLoader; +import org.wiremock.annotations.Beta; +import org.wiremock.grpc.internal.GrpcAdminApiExtension; +import org.wiremock.grpc.internal.GrpcHttpServerFactory; +import org.wiremock.grpc.internal.Jetty12GrpcHttpServerFactory; + +@Beta(justification = "Incubating extension: https://github.com/wiremock/wiremock/issues/2383") +public class Jetty12GrpcExtensionFactory implements ExtensionFactory { + + @Override + public List create(WireMockServices services) { + return List.of(new Jetty12GrpcHttpServerFactory(services.getStores().getBlobStore("grpc"))); + } + + @Override + public boolean isLoadable() { + return !HttpServerFactoryLoader.isJetty11(); + } +} diff --git a/wiremock-grpc-extension-jetty12/src/main/java/org/wiremock/grpc/internal/Jetty12GrpcHttpServerFactory.java b/wiremock-grpc-extension-jetty12/src/main/java/org/wiremock/grpc/internal/Jetty12GrpcHttpServerFactory.java new file mode 100644 index 0000000..ccbb21b --- /dev/null +++ b/wiremock-grpc-extension-jetty12/src/main/java/org/wiremock/grpc/internal/Jetty12GrpcHttpServerFactory.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2023-2024 Thomas Akehurst + * + * 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. + */ +package org.wiremock.grpc.internal; + +import com.github.tomakehurst.wiremock.core.Options; +import com.github.tomakehurst.wiremock.http.AdminRequestHandler; +import com.github.tomakehurst.wiremock.http.HttpServer; +import com.github.tomakehurst.wiremock.http.StubRequestHandler; +import com.github.tomakehurst.wiremock.jetty12.Jetty12HttpServer; +import com.github.tomakehurst.wiremock.store.BlobStore; +import jakarta.servlet.DispatcherType; +import java.util.EnumSet; + +import org.eclipse.jetty.ee10.servlet.FilterHolder; +import org.eclipse.jetty.ee10.servlet.ServletContextHandler; + +public class Jetty12GrpcHttpServerFactory extends GrpcHttpServerFactory { + + public Jetty12GrpcHttpServerFactory(BlobStore protoDescriptorStore) { + super(protoDescriptorStore); + } + + @Override + public String getName() { + return "grpc-jetty12"; + } + + @Override + public HttpServer buildHttpServer( + Options options, + AdminRequestHandler adminRequestHandler, + StubRequestHandler stubRequestHandler) { + return new Jetty12HttpServer(options, adminRequestHandler, stubRequestHandler) { + @Override + protected void decorateMockServiceContextBeforeConfig(ServletContextHandler mockServiceContext) { + grpcFilter = new GrpcFilter(stubRequestHandler); + loadFileDescriptors(); + final FilterHolder filterHolder = new FilterHolder(grpcFilter); + mockServiceContext.addFilter(filterHolder, "/*", EnumSet.of(DispatcherType.REQUEST)); + } + }; + } +} diff --git a/wiremock-grpc-extension-jetty12/src/main/resources/META-INF/services/com.github.tomakehurst.wiremock.extension.ExtensionFactory b/wiremock-grpc-extension-jetty12/src/main/resources/META-INF/services/com.github.tomakehurst.wiremock.extension.ExtensionFactory new file mode 100644 index 0000000..7cadddb --- /dev/null +++ b/wiremock-grpc-extension-jetty12/src/main/resources/META-INF/services/com.github.tomakehurst.wiremock.extension.ExtensionFactory @@ -0,0 +1 @@ +org.wiremock.grpc.Jetty12GrpcExtensionFactory \ No newline at end of file diff --git a/wiremock-grpc-extension-jetty12/src/test/java/org/wiremock/grpc/Jetty12GrpcAcceptanceTest.java b/wiremock-grpc-extension-jetty12/src/test/java/org/wiremock/grpc/Jetty12GrpcAcceptanceTest.java new file mode 100644 index 0000000..971c100 --- /dev/null +++ b/wiremock-grpc-extension-jetty12/src/test/java/org/wiremock/grpc/Jetty12GrpcAcceptanceTest.java @@ -0,0 +1,533 @@ +/* + * Copyright (C) 2023-2024 Thomas Akehurst + * + * 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. + */ +package org.wiremock.grpc; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.moreThanOrExactly; +import static com.github.tomakehurst.wiremock.client.WireMock.okJson; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.iterableWithSize; +import static org.hamcrest.Matchers.startsWith; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.wiremock.grpc.dsl.WireMockGrpc.Status; +import static org.wiremock.grpc.dsl.WireMockGrpc.equalToMessage; +import static org.wiremock.grpc.dsl.WireMockGrpc.json; +import static org.wiremock.grpc.dsl.WireMockGrpc.jsonTemplate; +import static org.wiremock.grpc.dsl.WireMockGrpc.message; +import static org.wiremock.grpc.dsl.WireMockGrpc.messageAsAny; +import static org.wiremock.grpc.dsl.WireMockGrpc.method; + +import com.example.grpc.AnotherGreetingServiceGrpc; +import com.example.grpc.GreetingServiceGrpc; +import com.example.grpc.request.HelloRequest; +import com.example.grpc.response.HelloResponse; +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.http.Fault; +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; +import com.google.common.base.Stopwatch; +import com.google.protobuf.Empty; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.StatusRuntimeException; +import io.grpc.reflection.v1.ServerReflectionGrpc; +import io.grpc.reflection.v1.ServerReflectionRequest; +import io.grpc.reflection.v1.ServerReflectionResponse; +import io.grpc.reflection.v1.ServiceResponse; +import io.grpc.stub.StreamObserver; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.stream.Stream; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.wiremock.grpc.client.AnotherGreetingsClient; +import org.wiremock.grpc.client.GreetingsClient; +import org.wiremock.grpc.dsl.WireMockGrpcService; +import org.wiremock.grpc.internal.GrpcStatusUtils; +import org.wiremock.grpc.internal.Jetty12GrpcHttpServerFactory; + +public class Jetty12GrpcAcceptanceTest { + + WireMockGrpcService mockGreetingService; + WireMockGrpcService anotherMockGreetingService; + ManagedChannel channel; + ManagedChannel anotherChannel; + GreetingsClient greetingsClient; + AnotherGreetingsClient anotherGreetingsClient; + WireMock wireMock; + + @RegisterExtension + public static WireMockExtension wm = + WireMockExtension.newInstance() + .options( + wireMockConfig() + .dynamicPort() + .withRootDirectory("src/test/resources/wiremock") + .extensions(new Jetty12GrpcExtensionFactory())) + .build(); + + public static Stream statusProvider() { + return GrpcStatusUtils.errorHttpToGrpcStatusMappings.entrySet().stream() + .map( + entry -> + Arguments.of( + entry.getKey(), entry.getValue().a.getCode().name(), entry.getValue().b)); + } + + @BeforeEach + void init() { + wireMock = wm.getRuntimeInfo().getWireMock(); + mockGreetingService = new WireMockGrpcService(wireMock, GreetingServiceGrpc.SERVICE_NAME); + anotherMockGreetingService = + new WireMockGrpcService(wireMock, AnotherGreetingServiceGrpc.SERVICE_NAME); + + channel = ManagedChannelBuilder.forAddress("localhost", wm.getPort()).usePlaintext().build(); + greetingsClient = new GreetingsClient(channel); + + anotherChannel = + ManagedChannelBuilder.forAddress("localhost", wm.getPort()).usePlaintext().build(); + anotherGreetingsClient = new AnotherGreetingsClient(anotherChannel); + } + + @AfterEach + void tearDown() { + channel.shutdown(); + anotherChannel.shutdown(); + } + + @Test + void shouldReturnGreetingBuiltViaTemplatedJsonWithRawStubbing() { + wm.stubFor( + post(urlPathEqualTo("/com.example.grpc.GreetingService/greeting")) + .willReturn( + okJson( + "{\n" + + " \"greeting\": \"Hello {{jsonPath request.body '$.name'}}\"\n" + + "}") + .withTransformers("response-template"))); + + String greeting = greetingsClient.greet("Tom"); + + assertThat(greeting, is("Hello Tom")); + } + + @Test + void shouldReturnGreetingBuiltViaTemplatedJson() { + mockGreetingService.stubFor( + method("greeting") + .willReturn( + jsonTemplate("{ \"greeting\": \"Hello {{jsonPath request.body '$.name'}}\" }"))); + + String greeting = greetingsClient.greet("Tom"); + + assertThat(greeting, is("Hello Tom")); + } + + @Test + void returnsResponseBuiltFromJson() { + mockGreetingService.stubFor( + method("greeting").willReturn(json("{ \"greeting\": \"Hi Tom from JSON\" }"))); + + String greeting = greetingsClient.greet("Whatever"); + + assertThat(greeting, is("Hi Tom from JSON")); + } + + @Test + void returnsResponseBuiltFromMessageObject() { + mockGreetingService.stubFor( + method("greeting") + .willReturn(message(HelloResponse.newBuilder().setGreeting("Hi Tom from object")))); + + String greeting = greetingsClient.greet("Whatever"); + + assertThat(greeting, is("Hi Tom from object")); + } + + @Test + void matchesRequestViaCoreJsonMatcher() { + mockGreetingService.stubFor( + method("greeting") + .withRequestMessage(equalToJson("{ \"name\": \"Tom\" }")) + .willReturn(message(HelloResponse.newBuilder().setGreeting("OK")))); + + assertThat(greetingsClient.greet("Tom"), is("OK")); + + assertThrows(StatusRuntimeException.class, () -> greetingsClient.greet("Wrong")); + } + + @Test + void matchesRequestViaExactMessageEquality() { + mockGreetingService.stubFor( + method("greeting") + .withRequestMessage(equalToMessage(HelloRequest.newBuilder().setName("Tom"))) + .willReturn(message(HelloResponse.newBuilder().setGreeting("OK")))); + + assertThat(greetingsClient.greet("Tom"), is("OK")); + + StatusRuntimeException exception = + assertThrows(StatusRuntimeException.class, () -> greetingsClient.greet("Wrong")); + assertThat( + exception.getMessage(), + is("UNIMPLEMENTED: No matching stub mapping found for gRPC request")); + } + + @ParameterizedTest + @MethodSource("statusProvider") + void shouldReturnTheCorrectGrpcErrorStatusForCorrespondingHttpStatus( + Integer httpStatus, String grpcStatus, String message) { + wm.stubFor( + post(urlPathEqualTo("/com.example.grpc.GreetingService/greeting")) + .willReturn(aResponse().withStatus(httpStatus))); + + StatusRuntimeException exception = + assertThrows(StatusRuntimeException.class, () -> greetingsClient.greet("Tom")); + assertThat(exception.getMessage(), is(grpcStatus + ": " + message)); + } + + @Test + void returnsResponseWithStatus() { + mockGreetingService.stubFor( + method("greeting").willReturn(Status.FAILED_PRECONDITION, "Failed some blah prerequisite")); + + StatusRuntimeException exception = + assertThrows(StatusRuntimeException.class, () -> greetingsClient.greet("Whatever")); + assertThat(exception.getMessage(), is("FAILED_PRECONDITION: Failed some blah prerequisite")); + } + + @Test + void returnsUnaryResponseToFirstMatchingMessagesInStreamingRequest() { + mockGreetingService.stubFor( + method("manyGreetingsOneReply") + .withRequestMessage(equalToMessage(HelloRequest.newBuilder().setName("Rob").build())) + .willReturn(message(HelloResponse.newBuilder().setGreeting("Hi Rob")))); + + assertThat(greetingsClient.manyGreetingsOneReply("Tom", "Uri", "Rob", "Mark"), is("Hi Rob")); + } + + @Test + void throwsNotFoundWhenNoStreamingClientMessageMatches() { + mockGreetingService.stubFor( + method("manyGreetingsOneReply") + .withRequestMessage(equalToMessage(HelloRequest.newBuilder().setName("Jeff").build())) + .willReturn(message(HelloResponse.newBuilder().setGreeting("Hi Rob")))); + + Exception exception = + assertThrows( + Exception.class, + () -> greetingsClient.manyGreetingsOneReply("Tom", "Uri", "Rob", "Mark")); + assertThat(exception.getCause(), instanceOf(StatusRuntimeException.class)); + assertThat( + exception.getCause().getMessage(), + is("UNIMPLEMENTED: No matching stub mapping found for gRPC request")); + } + + @Test + void throwsReturnedErrorFromStreamingClientCall() { + mockGreetingService.stubFor( + method("manyGreetingsOneReply") + .withRequestMessage(equalToMessage(HelloRequest.newBuilder().setName("Jerf").build())) + .willReturn(Status.INVALID_ARGUMENT, "Jerf is not a valid name")); + + Exception exception = + assertThrows( + Exception.class, () -> greetingsClient.manyGreetingsOneReply("Tom", "Jerf", "Rob")); + assertThat(exception.getCause(), instanceOf(StatusRuntimeException.class)); + assertThat(exception.getCause().getMessage(), is("INVALID_ARGUMENT: Jerf is not a valid name")); + } + + @ParameterizedTest + @MethodSource("statusProvider") + void throwsReturnedErrorFromStreamingClientCallWhenServerOnlyReturnsAHttpStatus( + Integer httpStatus, String grpcStatus, String message) { + wm.stubFor( + post(urlPathEqualTo("/com.example.grpc.GreetingService/manyGreetingsOneReply")) + .willReturn(aResponse().withStatus(httpStatus))); + + Exception exception = + assertThrows( + Exception.class, () -> greetingsClient.manyGreetingsOneReply("Tom", "Jerf", "Rob")); + assertThat(exception.getCause(), instanceOf(StatusRuntimeException.class)); + assertThat(exception.getCause().getMessage(), is(grpcStatus + ": " + message)); + } + + @Test + void returnsStreamedResponseToUnaryRequest() { + mockGreetingService.stubFor( + method("oneGreetingManyReplies") + .willReturn(message(HelloResponse.newBuilder().setGreeting("Hi Tom")))); + + assertThat(greetingsClient.oneGreetingManyReplies("Tom"), hasItem("Hi Tom")); + } + + @Test + void returnsResponseWithImportedType() { + mockGreetingService.stubFor( + method("oneGreetingEmptyReply").willReturn(message(Empty.newBuilder()))); + + assertThat(greetingsClient.oneGreetingEmptyReply("Tom"), is(true)); + } + + @Test + void verifiesViaJson() { + mockGreetingService.stubFor( + method("greeting").willReturn(message(HelloResponse.newBuilder().setGreeting("Hi")))); + + greetingsClient.greet("Peter"); + greetingsClient.greet("Peter"); + + mockGreetingService + .verify(moreThanOrExactly(2), "greeting") + .withRequestMessage(equalToJson("{ \"name\": \"Peter\" }")); + + mockGreetingService.verify(0, "oneGreetingEmptyReply"); + + mockGreetingService + .verify(0, "greeting") + .withRequestMessage(equalToJson("{ \"name\": \"Chris\" }")); + } + + @Test + void verifiesViaMessage() { + mockGreetingService.stubFor( + method("greeting").willReturn(message(HelloResponse.newBuilder().setGreeting("Hi")))); + + greetingsClient.greet("Peter"); + + mockGreetingService + .verify("greeting") + .withRequestMessage(equalToMessage(HelloRequest.newBuilder().setName("Peter"))); + + mockGreetingService.verify(0, "oneGreetingEmptyReply"); + } + + @Test + void networkFault() { + mockGreetingService.stubFor(method("greeting").willReturn(Fault.CONNECTION_RESET_BY_PEER)); + + Exception exception = + assertThrows(StatusRuntimeException.class, () -> greetingsClient.greet("Alan")); + assertThat(exception.getMessage(), startsWith("UNKNOWN")); + } + + @Test + void fixedDelay() { + mockGreetingService.stubFor( + method("greeting") + .willReturn(json("{ \"greeting\": \"Delayed hello\" }")) + .withFixedDelay(1000)); + + Stopwatch stopwatch = Stopwatch.createStarted(); + String greeting = greetingsClient.greet("Tom"); + stopwatch.stop(); + + assertThat(greeting, is("Delayed hello")); + assertThat(stopwatch.elapsed(), greaterThanOrEqualTo(Duration.ofMillis(990L))); + } + + @Test + void randomDelay() { + mockGreetingService.stubFor( + method("greeting") + .willReturn(json("{ \"greeting\": \"Delayed hello\" }")) + .withUniformRandomDelay(500, 1000)); + + Stopwatch stopwatch = Stopwatch.createStarted(); + String greeting = greetingsClient.greet("Tom"); + stopwatch.stop(); + + assertThat(greeting, is("Delayed hello")); + assertThat(stopwatch.elapsed(), greaterThanOrEqualTo(Duration.ofMillis(500L))); + } + + @Test + void resetStubs() { + // Starting point assertion + // There should be a single mapping (the hello-world one) + verifyDefaultMappings(); + + anotherMockGreetingService.stubFor( + method("anotherGreeting") + .willReturn(message(HelloResponse.newBuilder().setGreeting("Hello")))); + + mockGreetingService.stubFor( + method("greeting").willReturn(message(HelloResponse.newBuilder().setGreeting("Hi")))); + + mockGreetingService.stubFor( + method("oneGreetingEmptyReply").willReturn(message(Empty.newBuilder()))); + + assertThat(wireMock.allStubMappings().getMappings(), iterableWithSize(4)); + + mockGreetingService.removeAllStubs(); + assertThat(wireMock.allStubMappings().getMappings(), iterableWithSize(2)); + + anotherMockGreetingService.removeAllStubs(); + + verifyDefaultMappings(); + } + + private void verifyDefaultMappings() { + var mappings = wireMock.allStubMappings().getMappings(); + assertThat(mappings, iterableWithSize(1)); + + var mapping = mappings.get(0); + assertNotNull(mapping); + assertThat(mapping.getName(), Matchers.equalTo("Hello")); + + var request = mapping.getRequest(); + assertThat(request.getMethod().value(), Matchers.equalTo("GET")); + assertThat(request.getUrlPath(), Matchers.equalTo("/hello")); + } + + @Test + void resetAll() { + // Create a single stub for 2 different services + anotherMockGreetingService.stubFor( + method("anotherGreeting") + .willReturn(message(HelloResponse.newBuilder().setGreeting("Hello")))); + + mockGreetingService.stubFor( + method("greeting").willReturn(message(HelloResponse.newBuilder().setGreeting("Hi")))); + + // Perform some actions on each + assertThat(greetingsClient.greet("Tom"), is("Hi")); + assertThat(greetingsClient.greet("Tom"), is("Hi")); + assertThat(anotherGreetingsClient.greet("Tom"), is("Hello")); + assertThat(anotherGreetingsClient.greet("Tom"), is("Hello")); + + // Verify the interactions with each + mockGreetingService + .verify(2, "greeting") + .withRequestMessage(equalToMessage(HelloRequest.newBuilder().setName("Tom"))); + + anotherMockGreetingService + .verify(2, "anotherGreeting") + .withRequestMessage(equalToMessage(HelloRequest.newBuilder().setName("Tom"))); + + // Remove all from one of the services + mockGreetingService.resetAll(); + + // Create a new stub + mockGreetingService.stubFor( + method("greeting").willReturn(message(HelloResponse.newBuilder().setGreeting("Hello")))); + + // Perform some actions on each + assertThat(greetingsClient.greet("Tom"), is("Hello")); + assertThat(greetingsClient.greet("Tom"), is("Hello")); + assertThat(greetingsClient.greet("Tom"), is("Hello")); + assertThat(greetingsClient.greet("Tom"), is("Hello")); + assertThat(anotherGreetingsClient.greet("Tom"), is("Hello")); + assertThat(anotherGreetingsClient.greet("Tom"), is("Hello")); + assertThat(anotherGreetingsClient.greet("Tom"), is("Hello")); + assertThat(anotherGreetingsClient.greet("Tom"), is("Hello")); + + // Verify the interactions with each + mockGreetingService + .verify(4, "greeting") + .withRequestMessage(equalToMessage(HelloRequest.newBuilder().setName("Tom"))); + + anotherMockGreetingService + .verify(6, "anotherGreeting") + .withRequestMessage(equalToMessage(HelloRequest.newBuilder().setName("Tom"))); + } + + @Test + void unaryMethodWithAnyRequest() { + mockGreetingService.stubFor( + method("greetingAnyRequest") + .willReturn(message(HelloResponse.newBuilder().setGreeting("Hiya").build()))); + + String greeting = greetingsClient.greetAnyRequest(); + + assertThat(greeting, is("Hiya")); + } + + @Test + void unaryMethodWithAnyResponse() { + mockGreetingService.stubFor( + method("greetingAnyResponse") + .willReturn(messageAsAny(HelloResponse.newBuilder().setGreeting("Hiya").build()))); + + String typeUrl = greetingsClient.greetAnyResponse(); + + assertThat(typeUrl, is("type.googleapis.com/com.example.grpc.response.HelloResponse")); + } + + @Test + void unaryMethodWithAnyResponseFromJson() { + mockGreetingService.stubFor( + method("greetingAnyResponse") + .willReturn( + json( + "{ \"@type\": \"type.googleapis.com/com.example.grpc.response.HelloResponse\", \"greeting\": \"Hiya\" }"))); + + String typeUrl = greetingsClient.greetAnyResponse(); + + assertThat(typeUrl, is("type.googleapis.com/com.example.grpc.response.HelloResponse")); + } + + @Test + void reflection() throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + List serverReflectionResponses = new ArrayList<>(); + StreamObserver stream = + ServerReflectionGrpc.newStub(channel) + .serverReflectionInfo( + new StreamObserver<>() { + @Override + public void onNext(ServerReflectionResponse value) { + serverReflectionResponses.add(value); + } + + @Override + public void onError(Throwable t) { + t.printStackTrace(System.err); + } + + @Override + public void onCompleted() { + latch.countDown(); + } + }); + stream.onNext(ServerReflectionRequest.newBuilder().setListServices("").build()); + stream.onCompleted(); + assertTrue(latch.await(5, SECONDS)); + + System.out.println(serverReflectionResponses); + + List serviceList = + serverReflectionResponses.get(0).getListServicesResponse().getServiceList(); + assertThat(serviceList.size(), is(4)); + } +} diff --git a/wiremock-grpc-extension-jetty12/src/test/java/org/wiremock/grpc/Jetty12GrpcViaExtensionScanningTest.java b/wiremock-grpc-extension-jetty12/src/test/java/org/wiremock/grpc/Jetty12GrpcViaExtensionScanningTest.java new file mode 100644 index 0000000..2d7e0eb --- /dev/null +++ b/wiremock-grpc-extension-jetty12/src/test/java/org/wiremock/grpc/Jetty12GrpcViaExtensionScanningTest.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2023-2024 Thomas Akehurst + * + * 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. + */ +package org.wiremock.grpc; + +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.wiremock.grpc.dsl.WireMockGrpc.json; +import static org.wiremock.grpc.dsl.WireMockGrpc.method; + +import com.example.grpc.AnotherGreetingServiceGrpc; +import com.example.grpc.GreetingServiceGrpc; +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.provider.Arguments; +import org.wiremock.grpc.client.AnotherGreetingsClient; +import org.wiremock.grpc.client.GreetingsClient; +import org.wiremock.grpc.dsl.WireMockGrpcService; +import org.wiremock.grpc.internal.GrpcStatusUtils; + +public class Jetty12GrpcViaExtensionScanningTest { + + WireMockGrpcService mockGreetingService; + WireMockGrpcService anotherMockGreetingService; + ManagedChannel channel; + ManagedChannel anotherChannel; + GreetingsClient greetingsClient; + AnotherGreetingsClient anotherGreetingsClient; + WireMock wireMock; + + @RegisterExtension + public static WireMockExtension wm = + WireMockExtension.newInstance() + .options( + wireMockConfig() + .dynamicPort() + .withRootDirectory("src/test/resources/wiremock") + .extensionScanningEnabled(true)) + .build(); + + public static Stream statusProvider() { + return GrpcStatusUtils.errorHttpToGrpcStatusMappings.entrySet().stream() + .map( + entry -> + Arguments.of( + entry.getKey(), entry.getValue().a.getCode().name(), entry.getValue().b)); + } + + @BeforeEach + void init() { + wireMock = wm.getRuntimeInfo().getWireMock(); + mockGreetingService = new WireMockGrpcService(wireMock, GreetingServiceGrpc.SERVICE_NAME); + anotherMockGreetingService = + new WireMockGrpcService(wireMock, AnotherGreetingServiceGrpc.SERVICE_NAME); + + channel = ManagedChannelBuilder.forAddress("localhost", wm.getPort()).usePlaintext().build(); + greetingsClient = new GreetingsClient(channel); + + anotherChannel = + ManagedChannelBuilder.forAddress("localhost", wm.getPort()).usePlaintext().build(); + anotherGreetingsClient = new AnotherGreetingsClient(anotherChannel); + } + + @AfterEach + void tearDown() { + channel.shutdown(); + anotherChannel.shutdown(); + } + + @Test + void returnsResponseBuiltFromJson() { + mockGreetingService.stubFor( + method("greeting").willReturn(json("{ \"greeting\": \"Hi Tom from JSON\" }"))); + + String greeting = greetingsClient.greet("Whatever"); + + assertThat(greeting, is("Hi Tom from JSON")); + } +} diff --git a/wiremock-grpc-extension-jetty12/src/test/resources/wiremock/grpc/bookings.dsc b/wiremock-grpc-extension-jetty12/src/test/resources/wiremock/grpc/bookings.dsc new file mode 100644 index 0000000..ddcccc0 --- /dev/null +++ b/wiremock-grpc-extension-jetty12/src/test/resources/wiremock/grpc/bookings.dsc @@ -0,0 +1,9 @@ + +Ü +BookingServices.protocom.example.grpc" +BookingRequest +id ( Rid"! +BookingResponse +id ( Rid2` +BookingServiceN +booking .com.example.grpc.BookingRequest!.com.example.grpc.BookingResponseBPbproto3 \ No newline at end of file diff --git a/wiremock-grpc-extension-jetty12/src/test/resources/wiremock/grpc/greetings.dsc b/wiremock-grpc-extension-jetty12/src/test/resources/wiremock/grpc/greetings.dsc new file mode 100644 index 0000000..adc96c5 Binary files /dev/null and b/wiremock-grpc-extension-jetty12/src/test/resources/wiremock/grpc/greetings.dsc differ diff --git a/wiremock-grpc-extension-jetty12/src/test/resources/wiremock/mappings/hello-world.json b/wiremock-grpc-extension-jetty12/src/test/resources/wiremock/mappings/hello-world.json new file mode 100644 index 0000000..66bd934 --- /dev/null +++ b/wiremock-grpc-extension-jetty12/src/test/resources/wiremock/mappings/hello-world.json @@ -0,0 +1,37 @@ +{ + "mappings": [{ + "request": { + "headers": {"Accept": {"or": [ + {"equalTo": "*/*"}, + {"equalTo": "text/plain"} + ]}}, + "method": "GET", + "urlPath": "/hello" + }, + "metadata": {"mocklab": { + "response-example-attachment": "Hello World", + "created": { + "at": "2024-01-09T15:32:35.424189986Z", + "by": "vqe9q", + "via": "ADMIN_API" + }, + "updated": { + "at": "2024-01-09T15:34:11.584464236Z", + "by": "vqe9q", + "via": "ADMIN_API" + } + }}, + "response": { + "headers": {"Content-Type": "text/plain"}, + "body": "Hello World!", + "status": 200 + }, + "name": "Hello", + "postServeActions": [], + "id": "d469c289-22fb-4186-90ab-deb6ffb6db5c", + "persistent": true, + "priority": 5, + "uuid": "d469c289-22fb-4186-90ab-deb6ffb6db5c" + }], + "meta": {"total": 1} +}