Skip to content

Commit 3d8c0cb

Browse files
authored
grpclib_client: handle trailer-only responses (#127)
Resolves: #123
1 parent c513853 commit 3d8c0cb

File tree

2 files changed

+52
-4
lines changed

2 files changed

+52
-4
lines changed

src/betterproto/grpc/grpclib_client.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,8 @@ async def _unary_unary(
7676
) as stream:
7777
await stream.send_message(request, end=True)
7878
response = await stream.recv_message()
79-
assert response is not None
80-
return response
79+
assert response is not None
80+
return response
8181

8282
async def _unary_stream(
8383
self,
@@ -122,8 +122,8 @@ async def _stream_unary(
122122
) as stream:
123123
await self._send_messages(stream, request_iterator)
124124
response = await stream.recv_message()
125-
assert response is not None
126-
return response
125+
assert response is not None
126+
return response
127127

128128
async def _stream_stream(
129129
self,

tests/grpc/test_grpclib_client.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
)
1010
import grpclib
1111
import grpclib.metadata
12+
import grpclib.server
1213
from grpclib.testing import ChannelFor
1314
import pytest
1415
from betterproto.grpc.util.async_channel import AsyncChannel
@@ -32,12 +33,59 @@ def server_side_test(stream):
3233
return server_side_test
3334

3435

36+
@pytest.fixture
37+
def handler_trailer_only_unauthenticated():
38+
async def handler(stream: grpclib.server.Stream):
39+
await stream.recv_message()
40+
await stream.send_initial_metadata()
41+
await stream.send_trailing_metadata(status=grpclib.Status.UNAUTHENTICATED)
42+
43+
return handler
44+
45+
3546
@pytest.mark.asyncio
3647
async def test_simple_service_call():
3748
async with ChannelFor([ThingService()]) as channel:
3849
await _test_client(ThingServiceClient(channel))
3950

4051

52+
@pytest.mark.asyncio
53+
async def test_trailer_only_error_unary_unary(
54+
mocker, handler_trailer_only_unauthenticated
55+
):
56+
service = ThingService()
57+
mocker.patch.object(
58+
service,
59+
"do_thing",
60+
side_effect=handler_trailer_only_unauthenticated,
61+
autospec=True,
62+
)
63+
async with ChannelFor([service]) as channel:
64+
with pytest.raises(grpclib.exceptions.GRPCError) as e:
65+
await ThingServiceClient(channel).do_thing(name="something")
66+
assert e.value.status == grpclib.Status.UNAUTHENTICATED
67+
68+
69+
@pytest.mark.asyncio
70+
async def test_trailer_only_error_stream_unary(
71+
mocker, handler_trailer_only_unauthenticated
72+
):
73+
service = ThingService()
74+
mocker.patch.object(
75+
service,
76+
"do_many_things",
77+
side_effect=handler_trailer_only_unauthenticated,
78+
autospec=True,
79+
)
80+
async with ChannelFor([service]) as channel:
81+
with pytest.raises(grpclib.exceptions.GRPCError) as e:
82+
await ThingServiceClient(channel).do_many_things(
83+
request_iterator=[DoThingRequest(name="something")]
84+
)
85+
await _test_client(ThingServiceClient(channel))
86+
assert e.value.status == grpclib.Status.UNAUTHENTICATED
87+
88+
4189
@pytest.mark.asyncio
4290
@pytest.mark.skipif(
4391
sys.version_info < (3, 8), reason="async mock spy does works for python3.8+"

0 commit comments

Comments
 (0)