diff --git a/build.gradle b/build.gradle index 1dd5e30..ec2c304 100644 --- a/build.gradle +++ b/build.gradle @@ -110,6 +110,7 @@ dependencies { testFixturesApi "org.hamcrest:hamcrest-core:3.0" testFixturesApi "org.hamcrest:hamcrest-library:3.0" testFixturesApi 'org.awaitility:awaitility:4.2.2' + testFixturesApi 'org.slf4j:slf4j-simple:2.0.16' testFixturesApi "io.grpc:grpc-okhttp" diff --git a/src/main/java/org/wiremock/grpc/dsl/WireMockGrpc.java b/src/main/java/org/wiremock/grpc/dsl/WireMockGrpc.java index ebaa24c..69d0129 100644 --- a/src/main/java/org/wiremock/grpc/dsl/WireMockGrpc.java +++ b/src/main/java/org/wiremock/grpc/dsl/WireMockGrpc.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023-2024 Thomas Akehurst + * Copyright (C) 2023-2025 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,8 @@ import com.github.tomakehurst.wiremock.common.Json; import com.github.tomakehurst.wiremock.matching.StringValuePattern; import com.google.protobuf.MessageOrBuilder; +import java.util.List; +import java.util.stream.Collectors; import org.wiremock.annotations.Beta; import org.wiremock.grpc.internal.JsonMessageUtils; @@ -50,6 +52,17 @@ public static GrpcResponseDefinitionBuilder message(MessageOrBuilder messageOrBu return new GrpcResponseDefinitionBuilder(Status.OK).fromJson(json); } + public static GrpcResponseDefinitionBuilder messages( + List messageOrBuilderList) { + final String json = + "[\n" + + messageOrBuilderList.stream() + .map(JsonMessageUtils::toJson) + .collect(Collectors.joining(",\n")) + + "\n]"; + return new GrpcResponseDefinitionBuilder(Status.OK).fromJson(json); + } + public static GrpcResponseDefinitionBuilder messageAsAny(MessageOrBuilder messageOrBuilder) { final String initialJson = JsonMessageUtils.toJson(messageOrBuilder); final ObjectNode jsonObject = Json.read(initialJson, ObjectNode.class); diff --git a/src/main/java/org/wiremock/grpc/internal/UnaryServerCallHandler.java b/src/main/java/org/wiremock/grpc/internal/UnaryServerCallHandler.java index f7b08fc..e6ec894 100644 --- a/src/main/java/org/wiremock/grpc/internal/UnaryServerCallHandler.java +++ b/src/main/java/org/wiremock/grpc/internal/UnaryServerCallHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023-2024 Thomas Akehurst + * Copyright (C) 2023-2025 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,9 @@ import static org.wiremock.grpc.dsl.GrpcResponseDefinitionBuilder.GRPC_STATUS_REASON; import static org.wiremock.grpc.internal.Delays.delayIfRequired; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.MappingIterator; +import com.fasterxml.jackson.databind.ObjectMapper; import com.github.tomakehurst.wiremock.common.Pair; import com.github.tomakehurst.wiremock.http.HttpHeader; import com.github.tomakehurst.wiremock.http.StubRequestHandler; @@ -28,11 +31,14 @@ import io.grpc.Status; import io.grpc.stub.ServerCalls; import io.grpc.stub.StreamObserver; +import java.io.IOException; import org.wiremock.grpc.dsl.WireMockGrpc; public class UnaryServerCallHandler extends BaseCallHandler implements ServerCalls.UnaryMethod { + private final ObjectMapper objectMapper = new ObjectMapper(); + public UnaryServerCallHandler( StubRequestHandler stubRequestHandler, Descriptors.ServiceDescriptor serviceDescriptor, @@ -85,12 +91,26 @@ public void invoke(DynamicMessage request, StreamObserver respon return; } - DynamicMessage.Builder messageBuilder = - DynamicMessage.newBuilder(methodDescriptor.getOutputType()); + try (MappingIterator iterator = + objectMapper.readerFor(JsonNode.class).readValues(resp.getBody())) { + + while (iterator.hasNext()) { + JsonNode node = iterator.next(); + + final DynamicMessage.Builder messageBuilder = + DynamicMessage.newBuilder(methodDescriptor.getOutputType()); + final DynamicMessage response = + jsonMessageConverter.toMessage(node.toString(), messageBuilder); - final DynamicMessage response = - jsonMessageConverter.toMessage(resp.getBodyAsString(), messageBuilder); - responseObserver.onNext(response); + responseObserver.onNext(response); + } + } catch (IOException e) { + responseObserver.onError( + Status.INTERNAL + .withDescription("Error parsing response") + .withCause(e) + .asRuntimeException()); + } responseObserver.onCompleted(); }, ServeEvent.of(wireMockRequest)); diff --git a/src/test/java/org/wiremock/grpc/GrpcAcceptanceTest.java b/src/test/java/org/wiremock/grpc/GrpcAcceptanceTest.java index f30b1c0..0825508 100644 --- a/src/test/java/org/wiremock/grpc/GrpcAcceptanceTest.java +++ b/src/test/java/org/wiremock/grpc/GrpcAcceptanceTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023-2024 Thomas Akehurst + * Copyright (C) 2023-2025 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasItems; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.iterableWithSize; @@ -39,6 +40,7 @@ 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.messages; import static org.wiremock.grpc.dsl.WireMockGrpc.method; import com.example.grpc.AnotherGreetingServiceGrpc; @@ -282,7 +284,7 @@ void throwsReturnedErrorFromStreamingClientCallWhenServerOnlyReturnsAHttpStatus( } @Test - void returnsStreamedResponseToUnaryRequest() { + void returnsStreamedResponseToUnaryRequestWithSingleItem() { mockGreetingService.stubFor( method("oneGreetingManyReplies") .willReturn(message(HelloResponse.newBuilder().setGreeting("Hi Tom")))); @@ -290,6 +292,19 @@ void returnsStreamedResponseToUnaryRequest() { assertThat(greetingsClient.oneGreetingManyReplies("Tom"), hasItem("Hi Tom")); } + @Test + void returnsStreamedResponseToUnaryRequest() { + mockGreetingService.stubFor( + method("oneGreetingManyReplies") + .willReturn( + messages( + List.of( + HelloResponse.newBuilder().setGreeting("Hi Tom"), + HelloResponse.newBuilder().setGreeting("Hi Tom again"))))); + + assertThat(greetingsClient.oneGreetingManyReplies("Tom"), hasItems("Hi Tom", "Hi Tom again")); + } + @Test void returnsResponseWithImportedType() { mockGreetingService.stubFor( @@ -337,7 +352,7 @@ void networkFault() { Exception exception = assertThrows(StatusRuntimeException.class, () -> greetingsClient.greet("Alan")); - assertThat(exception.getMessage(), startsWith("UNKNOWN")); + assertThat(exception.getMessage(), startsWith("CANCELLED")); } @Test 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 index 971c100..1559cb1 100644 --- 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 @@ -26,6 +26,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasItems; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.iterableWithSize; @@ -39,6 +40,7 @@ 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.messages; import static org.wiremock.grpc.dsl.WireMockGrpc.method; import com.example.grpc.AnotherGreetingServiceGrpc; @@ -283,7 +285,7 @@ void throwsReturnedErrorFromStreamingClientCallWhenServerOnlyReturnsAHttpStatus( } @Test - void returnsStreamedResponseToUnaryRequest() { + void returnsStreamedResponseToUnaryRequestWithSingleItem() { mockGreetingService.stubFor( method("oneGreetingManyReplies") .willReturn(message(HelloResponse.newBuilder().setGreeting("Hi Tom")))); @@ -291,6 +293,19 @@ void returnsStreamedResponseToUnaryRequest() { assertThat(greetingsClient.oneGreetingManyReplies("Tom"), hasItem("Hi Tom")); } + @Test + void returnsStreamedResponseToUnaryRequest() { + mockGreetingService.stubFor( + method("oneGreetingManyReplies") + .willReturn( + messages( + List.of( + HelloResponse.newBuilder().setGreeting("Hi Tom"), + HelloResponse.newBuilder().setGreeting("Hi Tom again"))))); + + assertThat(greetingsClient.oneGreetingManyReplies("Tom"), hasItems("Hi Tom", "Hi Tom again")); + } + @Test void returnsResponseWithImportedType() { mockGreetingService.stubFor( @@ -338,7 +353,7 @@ void networkFault() { Exception exception = assertThrows(StatusRuntimeException.class, () -> greetingsClient.greet("Alan")); - assertThat(exception.getMessage(), startsWith("UNKNOWN")); + assertThat(exception.getMessage(), startsWith("CANCELLED")); } @Test