Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 165 additions & 11 deletions tests/system/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import collections

import grpc
from unittest import mock
import os
import pytest

from typing import Sequence, Tuple

from google.api_core.client_options import ClientOptions # type: ignore
from google.showcase_v1beta1.services.echo.transports import EchoRestInterceptor

try:
from google.auth.aio import credentials as ga_credentials_async
Expand All @@ -42,6 +45,7 @@
try:
from google.showcase_v1beta1.services.echo.transports import (
AsyncEchoRestTransport,
AsyncEchoRestInterceptor,
)

HAS_ASYNC_REST_ECHO_TRANSPORT = True
Expand Down Expand Up @@ -248,7 +252,51 @@ def messaging(use_mtls, request):
return construct_client(MessagingClient, use_mtls, transport_name=request.param)


class MetadataClientInterceptor(
class EchoMetadataClientRestInterceptor(EchoRestInterceptor):
request_metadata: Sequence[Tuple[str, str]] = []
response_metadata: Sequence[Tuple[str, str]] = []

def pre_echo(self, request, metadata):
self.request_metadata = metadata
return request, metadata

def post_echo_with_metadata(self, request, metadata):
self.response_metadata = metadata
return request, metadata

def pre_expand(self, request, metadata):
self.request_metadata = metadata
return request, metadata

def post_expand_with_metadata(self, request, metadata):
self.response_metadata = metadata
return request, metadata


if HAS_ASYNC_REST_ECHO_TRANSPORT:

class EchoMetadataClientRestAsyncInterceptor(AsyncEchoRestInterceptor):
request_metadata: Sequence[Tuple[str, str]] = []
response_metadata: Sequence[Tuple[str, str]] = []

async def pre_echo(self, request, metadata):
self.request_metadata = metadata
return request, metadata

async def post_echo_with_metadata(self, request, metadata):
self.response_metadata = metadata
return request, metadata

async def pre_expand(self, request, metadata):
self.request_metadata = metadata
return request, metadata

async def post_expand_with_metadata(self, request, metadata):
self.response_metadata = metadata
return request, metadata


class EchoMetadataClientGrpcInterceptor(
grpc.UnaryUnaryClientInterceptor,
grpc.UnaryStreamClientInterceptor,
grpc.StreamUnaryClientInterceptor,
Expand All @@ -257,42 +305,94 @@ class MetadataClientInterceptor(
def __init__(self, key, value):
self._key = key
self._value = value
self.request_metadata = []
self.response_metadata = []

def _add_metadata(self, client_call_details):
def _add_request_metadata(self, client_call_details):
if client_call_details.metadata is not None:
client_call_details.metadata.append((self._key, self._value))
self.request_metadata = client_call_details.metadata

def intercept_unary_unary(self, continuation, client_call_details, request):
self._add_metadata(client_call_details)
self._add_request_metadata(client_call_details)
response = continuation(client_call_details, request)
metadata = [(k, str(v)) for k, v in response.trailing_metadata()]
self.response_metadata = metadata
return response

def intercept_unary_stream(self, continuation, client_call_details, request):
self._add_metadata(client_call_details)
self._add_request_metadata(client_call_details)
response_it = continuation(client_call_details, request)
return response_it

def intercept_stream_unary(
self, continuation, client_call_details, request_iterator
):
self._add_metadata(client_call_details)
self._add_request_metadata(client_call_details)
response = continuation(client_call_details, request_iterator)
return response

def intercept_stream_stream(
self, continuation, client_call_details, request_iterator
):
self._add_metadata(client_call_details)
self._add_request_metadata(client_call_details)
response_it = continuation(client_call_details, request_iterator)
return response_it


class EchoMetadataClientGrpcAsyncInterceptor(
grpc.aio.UnaryUnaryClientInterceptor,
grpc.aio.UnaryStreamClientInterceptor,
grpc.aio.StreamUnaryClientInterceptor,
grpc.aio.StreamStreamClientInterceptor,
):
def __init__(self, key, value):
self._key = key
self._value = value
self.request_metadata = []
self.response_metadata = []

async def _add_request_metadata(self, client_call_details):
if client_call_details.metadata is not None:
client_call_details.metadata.append((self._key, self._value))
self.request_metadata = client_call_details.metadata

async def intercept_unary_unary(self, continuation, client_call_details, request):
await self._add_request_metadata(client_call_details)
response = await continuation(client_call_details, request)
metadata = [(k, str(v)) for k, v in await response.trailing_metadata()]
self.response_metadata = metadata
return response

async def intercept_unary_stream(self, continuation, client_call_details, request):
self._add_request_metadata(client_call_details)
response_it = continuation(client_call_details, request)
return response_it

async def intercept_stream_unary(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use more descriptive names to differentiate this method from the previous one. I realize previously existing classes established this pattern, but we should clarify.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not something that we can change as it's an abstract method in gRPC that we're overriding

https://grpc.github.io/grpc/python/grpc.html#grpc.StreamUnaryClientInterceptor.intercept_stream_unary

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack. Too bad.

self, continuation, client_call_details, request_iterator
):
self._add_request_metadata(client_call_details)
response = continuation(client_call_details, request_iterator)
return response

async def intercept_stream_stream(
self, continuation, client_call_details, request_iterator
):
self._add_request_metadata(client_call_details)
response_it = continuation(client_call_details, request_iterator)
return response_it


@pytest.fixture
def intercepted_echo(use_mtls):
def intercepted_echo_grpc(use_mtls):
# The interceptor adds 'showcase-trailer' client metadata. Showcase server
# echos any metadata with key 'showcase-trailer', so the same metadata
# echoes any metadata with key 'showcase-trailer', so the same metadata
# should appear as trailing metadata in the response.
interceptor = MetadataClientInterceptor("showcase-trailer", "intercepted")
interceptor = EchoMetadataClientGrpcInterceptor(
"showcase-trailer",
"intercepted",
)
host = "localhost:7469"
channel = (
grpc.secure_channel(host, ssl_credentials)
Expand All @@ -304,4 +404,58 @@ def intercepted_echo(use_mtls):
credentials=ga_credentials.AnonymousCredentials(),
channel=intercept_channel,
)
return EchoClient(transport=transport)
return EchoClient(transport=transport), interceptor


@pytest.fixture
def intercepted_echo_grpc_async():
# The interceptor adds 'showcase-trailer' client metadata. Showcase server
# echoes any metadata with key 'showcase-trailer', so the same metadata
# should appear as trailing metadata in the response.
interceptor = EchoMetadataClientGrpcAsyncInterceptor(
"showcase-trailer",
"intercepted",
)
host = "localhost:7469"
channel = grpc.aio.insecure_channel(host, interceptors=[interceptor])
# intercept_channel = grpc.aio.intercept_channel(channel, interceptor)
transport = EchoAsyncClient.get_transport_class("grpc_asyncio")(
credentials=ga_credentials.AnonymousCredentials(),
channel=channel,
)
return EchoAsyncClient(transport=transport), interceptor


@pytest.fixture
def intercepted_echo_rest():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're waiting for REST async or can we test that now?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 05f6c65

transport_name = "rest"
transport_cls = EchoClient.get_transport_class(transport_name)
interceptor = EchoMetadataClientRestInterceptor()

# The custom host explicitly bypasses https.
transport = transport_cls(
credentials=ga_credentials.AnonymousCredentials(),
host="localhost:7469",
url_scheme="http",
interceptor=interceptor,
)
return EchoClient(transport=transport), interceptor


@pytest.fixture
def intercepted_echo_rest_async():
if not HAS_ASYNC_REST_ECHO_TRANSPORT:
pytest.skip("Skipping test with async rest.")

transport_name = "rest_asyncio"
transport_cls = EchoAsyncClient.get_transport_class(transport_name)
interceptor = EchoMetadataClientRestAsyncInterceptor()

# The custom host explicitly bypasses https.
transport = transport_cls(
credentials=async_anonymous_credentials(),
host="localhost:7469",
url_scheme="http",
interceptor=interceptor,
)
return EchoAsyncClient(transport=transport), interceptor
12 changes: 8 additions & 4 deletions tests/system/test_grpc_interceptor_streams.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@
intercepted_metadata = (("showcase-trailer", "intercepted"),)


def test_unary_stream(intercepted_echo):
def test_unary_stream(intercepted_echo_grpc):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to test async grpc here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 05f6c65

client, interceptor = intercepted_echo_grpc
content = "The hail in Wales falls mainly on the snails."
responses = intercepted_echo.expand(
responses = client.expand(
{
"content": content,
}
Expand All @@ -37,13 +38,15 @@ def test_unary_stream(intercepted_echo):
(metadata.key, metadata.value) for metadata in responses.trailing_metadata()
]
assert intercepted_metadata[0] in response_metadata
interceptor.response_metadata = response_metadata


def test_stream_stream(intercepted_echo):
def test_stream_stream(intercepted_echo_grpc):
client, interceptor = intercepted_echo_grpc
requests = []
requests.append(showcase.EchoRequest(content="hello"))
requests.append(showcase.EchoRequest(content="world!"))
responses = intercepted_echo.chat(iter(requests))
responses = client.chat(iter(requests))

contents = [response.content for response in responses]
assert contents == ["hello", "world!"]
Expand All @@ -52,3 +55,4 @@ def test_stream_stream(intercepted_echo):
(metadata.key, metadata.value) for metadata in responses.trailing_metadata()
]
assert intercepted_metadata[0] in response_metadata
interceptor.response_metadata = response_metadata
107 changes: 107 additions & 0 deletions tests/system/test_response_metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Copyright 2025 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.

import os
import pytest

from google import showcase


@pytest.mark.parametrize(
"transport,response_metadata",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do the interceptors used int his file know about the response metadata they should be injecting? Or is this real response metadata from Showcase? I'm not finding it by doing a search n the Showcase repo.

Copy link
Contributor Author

@parthea parthea Mar 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The interceptors aren't injecting anything here. There is a feature of the echo rpc which adds response headers(metadata) with the X-Showcase-Request prefix automatically for REST , using information from the request. This was added in googleapis/gapic-showcase#1509. For gRPC, I believe this feature was added in googleapis/gapic-showcase#970

[
("grpc", ("something1", "something_value1")),
("rest", ("X-Showcase-Request-Something1", "something_value1")),
],
)
def test_metadata_response_unary(
intercepted_echo_rest, intercepted_echo_grpc, transport, response_metadata
):
request_content = "The hail in Wales falls mainly on the snails."
request_metadata = ("something1", "something_value1")
if transport == "grpc":
client, interceptor = intercepted_echo_grpc
else:
client, interceptor = intercepted_echo_rest
response = client.echo(
request=showcase.EchoRequest(content=request_content),
metadata=(request_metadata,),
)
assert response.content == request_content
assert request_metadata in interceptor.request_metadata
assert response_metadata in interceptor.response_metadata


def test_metadata_response_rest_streams(intercepted_echo_rest):
request_content = "The hail in Wales falls mainly on the snails."
request_metadata = ("something2", "something_value2")
response_metadata = ("X-Showcase-Request-Something2", "something_value2")
client, interceptor = intercepted_echo_rest
client.expand(
{
"content": request_content,
},
metadata=(request_metadata,),
)

assert request_metadata in interceptor.request_metadata
assert response_metadata in interceptor.response_metadata


if os.environ.get("GAPIC_PYTHON_ASYNC", "true") == "true":

@pytest.mark.asyncio
async def test_metadata_response_rest_streams_async(intercepted_echo_rest_async):
request_content = "The hail in Wales falls mainly on the snails."
request_metadata = ("something2", "something_value2")
response_metadata = ("X-Showcase-Request-Something2", "something_value2")
client, interceptor = intercepted_echo_rest_async
await client.expand(
{
"content": request_content,
},
metadata=(request_metadata,),
)

assert request_metadata in interceptor.request_metadata
assert response_metadata in interceptor.response_metadata

@pytest.mark.parametrize(
"transport,response_metadata",
[
("grpc_asyncio", ("something3", "something_value3")),
("rest_asyncio", ("X-Showcase-Request-Something3", "something_value3")),
],
)
@pytest.mark.asyncio
async def test_metadata_response_unary_async(
intercepted_echo_grpc_async,
intercepted_echo_rest_async,
transport,
response_metadata,
):
request_content = "The hail in Wales falls mainly on the snails."
request_metadata = ("something3", "something_value3")

if transport == "grpc_asyncio":
client, interceptor = intercepted_echo_grpc_async
else:
client, interceptor = intercepted_echo_rest_async
response = await client.echo(
request=showcase.EchoRequest(content=request_content),
metadata=(request_metadata,),
)
assert response.content == request_content
assert request_metadata in interceptor.request_metadata
assert response_metadata in interceptor.response_metadata
Loading