From f7af718b5adc39ad16bc9790e41e00343d5f8e51 Mon Sep 17 00:00:00 2001 From: Nicole Lee Date: Tue, 28 Jul 2026 18:17:39 +0000 Subject: [PATCH 1/4] test(showcase): add integration tests for error details --- .../showcase/v1beta1/it/ITErrorDetails.java | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITErrorDetails.java diff --git a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITErrorDetails.java b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITErrorDetails.java new file mode 100644 index 000000000000..d246137dee9b --- /dev/null +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITErrorDetails.java @@ -0,0 +1,172 @@ +/* + * Copyright 2026 Google LLC + * + * 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 + * + * https://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 com.google.showcase.v1beta1.it; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.google.api.gax.rpc.ApiException; +import com.google.api.gax.rpc.ErrorDetails; +import com.google.api.gax.rpc.StatusCode; +import com.google.rpc.BadRequest; +import com.google.rpc.DebugInfo; +import com.google.rpc.ErrorInfo; +import com.google.rpc.Help; +import com.google.rpc.LocalizedMessage; +import com.google.rpc.PreconditionFailure; +import com.google.rpc.QuotaFailure; +import com.google.rpc.RequestInfo; +import com.google.rpc.ResourceInfo; +import com.google.rpc.RetryInfo; +import com.google.showcase.v1beta1.EchoClient; +import com.google.showcase.v1beta1.FailEchoWithDetailsRequest; +import com.google.showcase.v1beta1.PoetryError; +import com.google.showcase.v1beta1.it.util.TestClientInitializer; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class ITErrorDetails { + + private static EchoClient grpcClient; + private static EchoClient httpjsonClient; + + @BeforeAll + static void createClients() throws Exception { + grpcClient = TestClientInitializer.createGrpcEchoClient(); + httpjsonClient = TestClientInitializer.createHttpJsonEchoClient(); + } + + @AfterAll + static void destroyClients() throws InterruptedException { + grpcClient.close(); + httpjsonClient.close(); + + grpcClient.awaitTermination(TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS); + httpjsonClient.awaitTermination( + TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS); + } + + private void verifyErrorDetailsGrpc(ApiException exception) { + assertThat(exception.getStatusCode().getCode()).isEqualTo(StatusCode.Code.ABORTED); + + ErrorDetails errorDetails = exception.getErrorDetails(); + assertThat(errorDetails).isNotNull(); + + // Verify standard error details are present and populated + // We are assuming that the mock server's hardcoded return values will stay the same + // https://github.com/googleapis/gapic-showcase/blob/b6c247f153369044d599969f6929ecdeb066c4c6/server/services/echo_service.go + ErrorInfo errorInfo = errorDetails.getErrorInfo(); + assertThat(errorInfo).isNotNull(); + assertThat(errorInfo.getReason()).isEqualTo("some ErrorInfo reason"); + + RetryInfo retryInfo = errorDetails.getRetryInfo(); + assertThat(retryInfo).isNotNull(); + assertThat(retryInfo.getRetryDelay().getSeconds()).isEqualTo(11); + + DebugInfo debugInfo = errorDetails.getDebugInfo(); + assertThat(debugInfo).isNotNull(); + assertThat(debugInfo.getDetail()).isEqualTo("a DebugInfo detail"); + + QuotaFailure quotaFailure = errorDetails.getQuotaFailure(); + assertThat(quotaFailure).isNotNull(); + assertThat(quotaFailure.getViolations(0).getDescription()) + .isEqualTo("First QuotaFailure description"); + + PreconditionFailure preconditionFailure = errorDetails.getPreconditionFailure(); + assertThat(preconditionFailure).isNotNull(); + assertThat(preconditionFailure.getViolations(0).getDescription()) + .isEqualTo("First PreconditionFailure description"); + + BadRequest badRequest = errorDetails.getBadRequest(); + assertThat(badRequest).isNotNull(); + assertThat(badRequest.getFieldViolations(0).getDescription()) + .isEqualTo("First BadRequest description"); + + RequestInfo requestInfo = errorDetails.getRequestInfo(); + assertThat(requestInfo).isNotNull(); + assertThat(requestInfo.getRequestId()).isEqualTo("RequestInfo: showcase-request-id"); + assertThat(requestInfo.getServingData()).isEqualTo("RequestInfo: showcase serving data"); + + ResourceInfo resourceInfo = errorDetails.getResourceInfo(); + assertThat(resourceInfo).isNotNull(); + assertThat(resourceInfo.getResourceType()).isEqualTo("ResourceInfo: showcase resource"); + + Help help = errorDetails.getHelp(); + assertThat(help).isNotNull(); + assertThat(help.getLinks(0).getDescription()).isEqualTo("Help: first showcase help link"); + + LocalizedMessage localizedMessage = errorDetails.getLocalizedMessage(); + assertThat(localizedMessage).isNotNull(); + assertThat(localizedMessage.getLocale()).isEqualTo("fr-CH"); + assertThat(localizedMessage.getMessage()) + .isEqualTo("This LocalizedMessage should be treated specially"); + + // Verify custom PoetryError can be unpacked + PoetryError poetryError = errorDetails.getMessage(PoetryError.class); + assertThat(poetryError).isNotNull(); + assertThat(poetryError.getPoem()).isEqualTo("roses are red"); + + // Verify mismatched type returns null safely (mismatch unpacking) + com.google.showcase.v1beta1.EchoResponse mismatchedDetail = + errorDetails.getMessage(com.google.showcase.v1beta1.EchoResponse.class); + assertThat(mismatchedDetail).isNull(); + } + + @Test + void testGrpc_failEchoWithDetails() { + FailEchoWithDetailsRequest request = FailEchoWithDetailsRequest.newBuilder().build(); + ApiException exception = + assertThrows(ApiException.class, () -> grpcClient.failEchoWithDetails(request)); + verifyErrorDetailsGrpc(exception); + } + + @Test + void testGrpc_failEchoWithDetails_customMessage() { + String customMessage = "this is a custom message to echo back"; + FailEchoWithDetailsRequest request = + FailEchoWithDetailsRequest.newBuilder().setMessage(customMessage).build(); + ApiException exception = + assertThrows(ApiException.class, () -> grpcClient.failEchoWithDetails(request)); + assertThat(exception.getStatusCode().getCode()).isEqualTo(StatusCode.Code.ABORTED); + + ErrorDetails errorDetails = exception.getErrorDetails(); + assertThat(errorDetails).isNotNull(); + + // Verify custom PoetryError can be unpacked and contains the custom message + PoetryError poetryError = errorDetails.getMessage(PoetryError.class); + assertThat(poetryError).isNotNull(); + assertThat(poetryError.getPoem()).isEqualTo(customMessage); + } + + @Test + void testHttpJson_failEchoWithDetails() { + FailEchoWithDetailsRequest request = FailEchoWithDetailsRequest.newBuilder().build(); + ApiException exception = + assertThrows(ApiException.class, () -> httpjsonClient.failEchoWithDetails(request)); + assertThat(exception.getStatusCode().getCode()).isEqualTo(StatusCode.Code.ABORTED); + + // GAX HTTP/JSON parser limitation: Because the response contains a custom/unregistered type + // (PoetryError) in the Any details list, HttpJsonErrorParser fails to parse the status payload, + // resulting in empty/null ErrorDetails. + ErrorDetails errorDetails = exception.getErrorDetails(); + if (errorDetails != null) { + assertThat(errorDetails.getErrorInfo()).isNull(); + } + } +} From 3d2c286a75faa94968cd6e96177fd2a4cf17466f Mon Sep 17 00:00:00 2001 From: Nicole Lee Date: Tue, 28 Jul 2026 18:35:10 +0000 Subject: [PATCH 2/4] test(showcase): simplify EchoResponse class reference using import --- .../java/com/google/showcase/v1beta1/it/ITErrorDetails.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITErrorDetails.java b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITErrorDetails.java index d246137dee9b..dd56e75c76a8 100644 --- a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITErrorDetails.java +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITErrorDetails.java @@ -33,6 +33,7 @@ import com.google.rpc.ResourceInfo; import com.google.rpc.RetryInfo; import com.google.showcase.v1beta1.EchoClient; +import com.google.showcase.v1beta1.EchoResponse; import com.google.showcase.v1beta1.FailEchoWithDetailsRequest; import com.google.showcase.v1beta1.PoetryError; import com.google.showcase.v1beta1.it.util.TestClientInitializer; @@ -123,8 +124,7 @@ private void verifyErrorDetailsGrpc(ApiException exception) { assertThat(poetryError.getPoem()).isEqualTo("roses are red"); // Verify mismatched type returns null safely (mismatch unpacking) - com.google.showcase.v1beta1.EchoResponse mismatchedDetail = - errorDetails.getMessage(com.google.showcase.v1beta1.EchoResponse.class); + EchoResponse mismatchedDetail = errorDetails.getMessage(EchoResponse.class); assertThat(mismatchedDetail).isNull(); } From dbe16dba33fe4d6d247eeda8d71ad05c579a09a8 Mon Sep 17 00:00:00 2001 From: Nicole Lee Date: Tue, 28 Jul 2026 20:52:18 +0000 Subject: [PATCH 3/4] test(showcase): address code review feedback using custom TypeRegistry manual JSON parsing workaround --- .../showcase/v1beta1/it/ITErrorDetails.java | 87 +++++++++++++++---- 1 file changed, 70 insertions(+), 17 deletions(-) diff --git a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITErrorDetails.java b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITErrorDetails.java index dd56e75c76a8..9a0aae368e6b 100644 --- a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITErrorDetails.java +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITErrorDetails.java @@ -19,9 +19,14 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; +import com.google.api.client.http.HttpResponseException; import com.google.api.gax.rpc.ApiException; import com.google.api.gax.rpc.ErrorDetails; import com.google.api.gax.rpc.StatusCode; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.protobuf.TypeRegistry; +import com.google.protobuf.util.JsonFormat; import com.google.rpc.BadRequest; import com.google.rpc.DebugInfo; import com.google.rpc.ErrorInfo; @@ -32,6 +37,7 @@ import com.google.rpc.RequestInfo; import com.google.rpc.ResourceInfo; import com.google.rpc.RetryInfo; +import com.google.rpc.Status; import com.google.showcase.v1beta1.EchoClient; import com.google.showcase.v1beta1.EchoResponse; import com.google.showcase.v1beta1.FailEchoWithDetailsRequest; @@ -42,6 +48,10 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +/** + * Integration tests for verifying that client libraries correctly propagate and deserialize + * standard and custom error details from {@link ApiException} over gRPC and HTTP/JSON transports. + */ class ITErrorDetails { private static EchoClient grpcClient; @@ -63,12 +73,11 @@ static void destroyClients() throws InterruptedException { TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS); } - private void verifyErrorDetailsGrpc(ApiException exception) { - assertThat(exception.getStatusCode().getCode()).isEqualTo(StatusCode.Code.ABORTED); - - ErrorDetails errorDetails = exception.getErrorDetails(); - assertThat(errorDetails).isNotNull(); - + /** + * Helper method to verify the content of error details. Transport-neutral validation of standard + * and custom error packets. + */ + private void verifyErrorDetailsContent(ErrorDetails errorDetails, String expectedPoem) { // Verify standard error details are present and populated // We are assuming that the mock server's hardcoded return values will stay the same // https://github.com/googleapis/gapic-showcase/blob/b6c247f153369044d599969f6929ecdeb066c4c6/server/services/echo_service.go @@ -118,24 +127,31 @@ private void verifyErrorDetailsGrpc(ApiException exception) { assertThat(localizedMessage.getMessage()) .isEqualTo("This LocalizedMessage should be treated specially"); - // Verify custom PoetryError can be unpacked + // Verify custom PoetryError can be unpacked and matches expected poem PoetryError poetryError = errorDetails.getMessage(PoetryError.class); assertThat(poetryError).isNotNull(); - assertThat(poetryError.getPoem()).isEqualTo("roses are red"); + assertThat(poetryError.getPoem()).isEqualTo(expectedPoem); // Verify mismatched type returns null safely (mismatch unpacking) EchoResponse mismatchedDetail = errorDetails.getMessage(EchoResponse.class); assertThat(mismatchedDetail).isNull(); } + // Verifies error details are correctly propagated and unpacked over standard gRPC protocol @Test void testGrpc_failEchoWithDetails() { FailEchoWithDetailsRequest request = FailEchoWithDetailsRequest.newBuilder().build(); ApiException exception = assertThrows(ApiException.class, () -> grpcClient.failEchoWithDetails(request)); - verifyErrorDetailsGrpc(exception); + + assertThat(exception.getStatusCode().getCode()).isEqualTo(StatusCode.Code.ABORTED); + assertThat(exception.getErrorDetails()).isNotNull(); + + // Reuse Transport-neutral Validation + verifyErrorDetailsContent(exception.getErrorDetails(), "roses are red"); } + // Verifies custom Error Details messages reflect user-defined inputs via gRPC @Test void testGrpc_failEchoWithDetails_customMessage() { String customMessage = "this is a custom message to echo back"; @@ -143,19 +159,18 @@ void testGrpc_failEchoWithDetails_customMessage() { FailEchoWithDetailsRequest.newBuilder().setMessage(customMessage).build(); ApiException exception = assertThrows(ApiException.class, () -> grpcClient.failEchoWithDetails(request)); - assertThat(exception.getStatusCode().getCode()).isEqualTo(StatusCode.Code.ABORTED); - ErrorDetails errorDetails = exception.getErrorDetails(); - assertThat(errorDetails).isNotNull(); + assertThat(exception.getStatusCode().getCode()).isEqualTo(StatusCode.Code.ABORTED); + assertThat(exception.getErrorDetails()).isNotNull(); - // Verify custom PoetryError can be unpacked and contains the custom message - PoetryError poetryError = errorDetails.getMessage(PoetryError.class); - assertThat(poetryError).isNotNull(); - assertThat(poetryError.getPoem()).isEqualTo(customMessage); + // Reuse Transport-neutral Validation + verifyErrorDetailsContent(exception.getErrorDetails(), customMessage); } + // Verifies error details are accessible in raw form over REST/HTTP and validates manually-parsed + // decompression @Test - void testHttpJson_failEchoWithDetails() { + void testHttpJson_failEchoWithDetails() throws Exception { FailEchoWithDetailsRequest request = FailEchoWithDetailsRequest.newBuilder().build(); ApiException exception = assertThrows(ApiException.class, () -> httpjsonClient.failEchoWithDetails(request)); @@ -168,5 +183,43 @@ void testHttpJson_failEchoWithDetails() { if (errorDetails != null) { assertThat(errorDetails.getErrorInfo()).isNull(); } + + // Workaround REST limitation: Parse the raw HTTP JSON error response manually using a custom + // TypeRegistry that registers standard types plus the showcase-specific PoetryError type. + assertThat(exception.getCause()).isInstanceOf(HttpResponseException.class); + HttpResponseException httpException = (HttpResponseException) exception.getCause(); + String errorJson = httpException.getContent(); + assertThat(errorJson).isNotNull(); + + TypeRegistry typeRegistry = + TypeRegistry.newBuilder() + .add(ErrorInfo.getDescriptor()) + .add(RetryInfo.getDescriptor()) + .add(DebugInfo.getDescriptor()) + .add(QuotaFailure.getDescriptor()) + .add(PreconditionFailure.getDescriptor()) + .add(BadRequest.getDescriptor()) + .add(RequestInfo.getDescriptor()) + .add(ResourceInfo.getDescriptor()) + .add(Help.getDescriptor()) + .add(LocalizedMessage.getDescriptor()) + .add(PoetryError.getDescriptor()) + .build(); + JsonFormat.Parser jsonParser = + JsonFormat.parser().ignoringUnknownFields().usingTypeRegistry(typeRegistry); + + // Parse the AIP-193 "error" JSON object into a status builder + JsonObject root = JsonParser.parseString(errorJson).getAsJsonObject(); + JsonObject errorObj = root.getAsJsonObject("error"); + Status.Builder statusBuilder = Status.newBuilder(); + jsonParser.merge(errorObj.toString(), statusBuilder); + Status status = statusBuilder.build(); + + // Verify we can successfully unpack the details from our custom status instance + ErrorDetails parsedDetails = + ErrorDetails.builder().setRawErrorMessages(status.getDetailsList()).build(); + + // Reuse Transport-neutral Validation! + verifyErrorDetailsContent(parsedDetails, "roses are red"); } } From 3f6ce5ad2cde2428e9fd146c5c7bc8c137a2b018 Mon Sep 17 00:00:00 2001 From: Nicole Lee Date: Wed, 29 Jul 2026 15:00:02 +0000 Subject: [PATCH 4/4] test(showcase): address code review feedback on comments and unit test cleanup --- .../com/google/showcase/v1beta1/it/ITErrorDetails.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITErrorDetails.java b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITErrorDetails.java index 9a0aae368e6b..3108dd6a0d7e 100644 --- a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITErrorDetails.java +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITErrorDetails.java @@ -39,7 +39,6 @@ import com.google.rpc.RetryInfo; import com.google.rpc.Status; import com.google.showcase.v1beta1.EchoClient; -import com.google.showcase.v1beta1.EchoResponse; import com.google.showcase.v1beta1.FailEchoWithDetailsRequest; import com.google.showcase.v1beta1.PoetryError; import com.google.showcase.v1beta1.it.util.TestClientInitializer; @@ -131,10 +130,6 @@ private void verifyErrorDetailsContent(ErrorDetails errorDetails, String expecte PoetryError poetryError = errorDetails.getMessage(PoetryError.class); assertThat(poetryError).isNotNull(); assertThat(poetryError.getPoem()).isEqualTo(expectedPoem); - - // Verify mismatched type returns null safely (mismatch unpacking) - EchoResponse mismatchedDetail = errorDetails.getMessage(EchoResponse.class); - assertThat(mismatchedDetail).isNull(); } // Verifies error details are correctly propagated and unpacked over standard gRPC protocol @@ -178,7 +173,7 @@ void testHttpJson_failEchoWithDetails() throws Exception { // GAX HTTP/JSON parser limitation: Because the response contains a custom/unregistered type // (PoetryError) in the Any details list, HttpJsonErrorParser fails to parse the status payload, - // resulting in empty/null ErrorDetails. + // resulting in empty ErrorDetails (where getErrorInfo() returns null). ErrorDetails errorDetails = exception.getErrorDetails(); if (errorDetails != null) { assertThat(errorDetails.getErrorInfo()).isNull(); @@ -186,6 +181,8 @@ void testHttpJson_failEchoWithDetails() throws Exception { // Workaround REST limitation: Parse the raw HTTP JSON error response manually using a custom // TypeRegistry that registers standard types plus the showcase-specific PoetryError type. + // This mimics the behavior of HttpJsonErrorParser, which currently does not support passing in + // a custom TypeRegistry. assertThat(exception.getCause()).isInstanceOf(HttpResponseException.class); HttpResponseException httpException = (HttpResponseException) exception.getCause(); String errorJson = httpException.getContent();