diff --git a/gax-java/gax/src/main/java/com/google/api/gax/rpc/ApiClientHeaderProvider.java b/gax-java/gax/src/main/java/com/google/api/gax/rpc/ApiClientHeaderProvider.java index ae27404335..a5e80e10b1 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/rpc/ApiClientHeaderProvider.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/rpc/ApiClientHeaderProvider.java @@ -42,7 +42,7 @@ public class ApiClientHeaderProvider implements HeaderProvider, Serializable { private static final long serialVersionUID = -8876627296793342119L; static final String QUOTA_PROJECT_ID_HEADER_KEY = "x-goog-user-project"; - static final String API_VERSION_HEADER_KEY = "x-goog-api-version"; + public static final String API_VERSION_HEADER_KEY = "x-goog-api-version"; private final Map headers; diff --git a/showcase/gapic-showcase/pom.xml b/showcase/gapic-showcase/pom.xml index 5bdc31a945..9e1b331ceb 100644 --- a/showcase/gapic-showcase/pom.xml +++ b/showcase/gapic-showcase/pom.xml @@ -19,7 +19,7 @@ - 0.33.0 + 0.35.0 diff --git a/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITApiVersionHeaders.java b/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITApiVersionHeaders.java new file mode 100644 index 0000000000..7ef4019975 --- /dev/null +++ b/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITApiVersionHeaders.java @@ -0,0 +1,324 @@ +/* + * Copyright 2024 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.Assert.assertThrows; + +import com.google.api.gax.httpjson.*; +import com.google.api.gax.rpc.ApiClientHeaderProvider; +import com.google.api.gax.rpc.FixedHeaderProvider; +import com.google.api.gax.rpc.StubSettings; +import com.google.common.collect.ImmutableList; +import com.google.showcase.v1beta1.*; +import com.google.showcase.v1beta1.it.util.TestClientInitializer; +import com.google.showcase.v1beta1.stub.ComplianceStubSettings; +import com.google.showcase.v1beta1.stub.EchoStubSettings; +import io.grpc.*; +import java.io.IOException; +import java.util.ArrayList; +import java.util.concurrent.TimeUnit; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +// TODO: add testing on error responses once feat is implemented in showcase. +// https://github.com/googleapis/gapic-showcase/pull/1456 +// TODO: watch for showcase gRPC trailer changes suggested in +// https://github.com/googleapis/gapic-showcase/pull/1509#issuecomment-2089147103 +public class ITApiVersionHeaders { + private static final String HTTP_RESPONSE_HEADER_STRING = + "x-showcase-request-" + ApiClientHeaderProvider.API_VERSION_HEADER_KEY; + private static final Metadata.Key API_VERSION_HEADER_KEY = + Metadata.Key.of( + ApiClientHeaderProvider.API_VERSION_HEADER_KEY, Metadata.ASCII_STRING_MARSHALLER); + + private static final String EXPECTED_ECHO_API_VERSION = "v1_20240408"; + private static final String CUSTOM_API_VERSION = "user-supplied-version"; + private static final String EXPECTED_EXCEPTION_MESSAGE = + "Header provider can't override the header: " + + ApiClientHeaderProvider.API_VERSION_HEADER_KEY; + private static final int DEFAULT_AWAIT_TERMINATION_SEC = 10; + + // Implement a client interceptor to retrieve the trailing metadata from response. + private static class GrpcCapturingClientInterceptor implements ClientInterceptor { + private Metadata metadata; + + @Override + public ClientCall interceptCall( + MethodDescriptor method, final CallOptions callOptions, Channel next) { + ClientCall call = next.newCall(method, callOptions); + return new ForwardingClientCall.SimpleForwardingClientCall(call) { + @Override + public void start(Listener responseListener, Metadata headers) { + Listener wrappedListener = + new SimpleForwardingClientCallListener(responseListener) { + @Override + public void onClose(Status status, Metadata trailers) { + if (status.isOk()) { + metadata = trailers; + } + super.onClose(status, trailers); + } + }; + + super.start(wrappedListener, headers); + } + }; + } + } + + private static class SimpleForwardingClientCallListener + extends ClientCall.Listener { + private final ClientCall.Listener delegate; + + SimpleForwardingClientCallListener(ClientCall.Listener delegate) { + this.delegate = delegate; + } + + @Override + public void onHeaders(Metadata headers) { + delegate.onHeaders(headers); + } + + @Override + public void onMessage(RespT message) { + delegate.onMessage(message); + } + + @Override + public void onClose(Status status, Metadata trailers) { + delegate.onClose(status, trailers); + } + + @Override + public void onReady() { + delegate.onReady(); + } + } + // Implement a client interceptor to retrieve the response headers + private static class HttpJsonCapturingClientInterceptor implements HttpJsonClientInterceptor { + private HttpJsonMetadata metadata; + + @Override + public HttpJsonClientCall interceptCall( + ApiMethodDescriptor method, + HttpJsonCallOptions callOptions, + HttpJsonChannel next) { + HttpJsonClientCall call = next.newCall(method, callOptions); + return new ForwardingHttpJsonClientCall.SimpleForwardingHttpJsonClientCall< + RequestT, ResponseT>(call) { + @Override + public void start(Listener responseListener, HttpJsonMetadata requestHeaders) { + Listener forwardingResponseListener = + new ForwardingHttpJsonClientCallListener.SimpleForwardingHttpJsonClientCallListener< + ResponseT>(responseListener) { + @Override + public void onHeaders(HttpJsonMetadata responseHeaders) { + metadata = responseHeaders; + super.onHeaders(responseHeaders); + } + + @Override + public void onMessage(ResponseT message) { + super.onMessage(message); + } + + @Override + public void onClose(int statusCode, HttpJsonMetadata trailers) { + super.onClose(statusCode, trailers); + } + }; + + super.start(forwardingResponseListener, requestHeaders); + } + }; + } + } + + private HttpJsonCapturingClientInterceptor httpJsonInterceptor; + private GrpcCapturingClientInterceptor grpcInterceptor; + private HttpJsonCapturingClientInterceptor httpJsonComplianceInterceptor; + private GrpcCapturingClientInterceptor grpcComplianceInterceptor; + private EchoClient grpcClient; + private EchoClient httpJsonClient; + private ComplianceClient grpcComplianceClient; + private ComplianceClient httpJsonComplianceClient; + + @Before + public void createClients() throws Exception { + // Create gRPC Interceptor and Client + grpcInterceptor = new GrpcCapturingClientInterceptor(); + grpcClient = TestClientInitializer.createGrpcEchoClient(ImmutableList.of(grpcInterceptor)); + + // Create HttpJson Interceptor and Client + httpJsonInterceptor = new HttpJsonCapturingClientInterceptor(); + httpJsonClient = + TestClientInitializer.createHttpJsonEchoClient(ImmutableList.of(httpJsonInterceptor)); + + // Create gRPC ComplianceClient and Interceptor + // Creating a compliance client to test case where api version is not set + grpcComplianceInterceptor = new GrpcCapturingClientInterceptor(); + grpcComplianceClient = + TestClientInitializer.createGrpcComplianceClient( + ImmutableList.of(grpcComplianceInterceptor)); + + // Create HttpJson ComplianceClient and Interceptor + httpJsonComplianceInterceptor = new HttpJsonCapturingClientInterceptor(); + httpJsonComplianceClient = + TestClientInitializer.createHttpJsonComplianceClient( + ImmutableList.of(httpJsonComplianceInterceptor)); + } + + @After + public void destroyClient() throws InterruptedException { + grpcClient.close(); + httpJsonClient.close(); + grpcComplianceClient.close(); + httpJsonComplianceClient.close(); + + grpcClient.awaitTermination(DEFAULT_AWAIT_TERMINATION_SEC, TimeUnit.SECONDS); + httpJsonClient.awaitTermination(DEFAULT_AWAIT_TERMINATION_SEC, TimeUnit.SECONDS); + grpcComplianceClient.awaitTermination(DEFAULT_AWAIT_TERMINATION_SEC, TimeUnit.SECONDS); + httpJsonComplianceClient.awaitTermination(DEFAULT_AWAIT_TERMINATION_SEC, TimeUnit.SECONDS); + } + + @Test + public void testGrpc_matchesApiVersion() { + grpcClient.echo(EchoRequest.newBuilder().build()); + String headerValue = grpcInterceptor.metadata.get(API_VERSION_HEADER_KEY); + assertThat(headerValue).isEqualTo(EXPECTED_ECHO_API_VERSION); + } + + @Test + public void testHttpJson_matchesHeaderName() { + httpJsonClient.echo(EchoRequest.newBuilder().build()); + ArrayList headerValues = + (ArrayList) httpJsonInterceptor.metadata.getHeaders().get(HTTP_RESPONSE_HEADER_STRING); + String headerValue = (String) headerValues.get(0); + assertThat(headerValue).isEqualTo(EXPECTED_ECHO_API_VERSION); + } + + @Test + public void testGrpc_noApiVersion() { + RepeatRequest request = + RepeatRequest.newBuilder().setInfo(ComplianceData.newBuilder().setFString("test")).build(); + grpcComplianceClient.repeatDataSimplePath(request); + assertThat(API_VERSION_HEADER_KEY).isNotIn(grpcComplianceInterceptor.metadata.keys()); + } + + @Test + public void testHttpJson_noApiVersion() { + RepeatRequest request = + RepeatRequest.newBuilder().setInfo(ComplianceData.newBuilder().setFString("test")).build(); + httpJsonComplianceClient.repeatDataSimplePath(request); + assertThat(API_VERSION_HEADER_KEY) + .isNotIn(httpJsonComplianceInterceptor.metadata.getHeaders().keySet()); + } + + @Test + public void testGrpcEcho_userApiVersionThrowsException() throws IOException { + StubSettings stubSettings = + grpcClient + .getSettings() + .getStubSettings() + .toBuilder() + .setHeaderProvider( + FixedHeaderProvider.create( + ApiClientHeaderProvider.API_VERSION_HEADER_KEY, CUSTOM_API_VERSION)) + .build(); + + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> EchoClient.create(EchoSettings.create((EchoStubSettings) stubSettings))); + assertThat(exception.getMessage()).isEqualTo(EXPECTED_EXCEPTION_MESSAGE); + } + + @Test + public void testHttpJsonEcho_userApiVersionThrowsException() throws IOException { + StubSettings stubSettings = + httpJsonClient + .getSettings() + .getStubSettings() + .toBuilder() + .setHeaderProvider( + FixedHeaderProvider.create( + ApiClientHeaderProvider.API_VERSION_HEADER_KEY, CUSTOM_API_VERSION)) + .build(); + + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> EchoClient.create(EchoSettings.create((EchoStubSettings) stubSettings))); + assertThat(exception.getMessage()).isEqualTo(EXPECTED_EXCEPTION_MESSAGE); + } + + @Test + public void testGrpcCompliance_userApiVersionSetSuccess() throws IOException { + StubSettings stubSettingsWithApiVersionHeader = + grpcComplianceClient + .getSettings() + .getStubSettings() + .toBuilder() + .setHeaderProvider( + FixedHeaderProvider.create( + ApiClientHeaderProvider.API_VERSION_HEADER_KEY, CUSTOM_API_VERSION)) + .build(); + try (ComplianceClient customComplianceClient = + ComplianceClient.create( + ComplianceSettings.create((ComplianceStubSettings) stubSettingsWithApiVersionHeader))) { + + RepeatRequest request = + RepeatRequest.newBuilder() + .setInfo(ComplianceData.newBuilder().setFString("test")) + .build(); + customComplianceClient.repeatDataSimplePath(request); + String headerValue = grpcComplianceInterceptor.metadata.get(API_VERSION_HEADER_KEY); + assertThat(headerValue).isEqualTo(CUSTOM_API_VERSION); + } + } + + @Test + public void testHttpJsonCompliance_userApiVersionSetSuccess() throws IOException { + StubSettings httpJsonStubSettingsWithApiVersionHeader = + httpJsonComplianceClient + .getSettings() + .getStubSettings() + .toBuilder() + .setHeaderProvider( + FixedHeaderProvider.create( + ApiClientHeaderProvider.API_VERSION_HEADER_KEY, CUSTOM_API_VERSION)) + .build(); + try (ComplianceClient customComplianceClient = + ComplianceClient.create( + ComplianceSettings.create( + (ComplianceStubSettings) httpJsonStubSettingsWithApiVersionHeader))) { + + RepeatRequest request = + RepeatRequest.newBuilder() + .setInfo(ComplianceData.newBuilder().setFString("test")) + .build(); + customComplianceClient.repeatDataSimplePath(request); + + ArrayList headerValues = + (ArrayList) + httpJsonComplianceInterceptor.metadata.getHeaders().get(HTTP_RESPONSE_HEADER_STRING); + String headerValue = (String) headerValues.get(0); + assertThat(headerValue).isEqualTo(CUSTOM_API_VERSION); + } + } +}