6
6
import asyncio
7
7
import json
8
8
import logging
9
+ from collections .abc import Callable
9
10
from dataclasses import dataclass
10
11
from http import HTTPStatus
11
12
from typing import Any
31
32
)
32
33
from servicelib .error_codes import parse_error_code
33
34
from servicelib .json_serialization import json_dumps
35
+ from servicelib .mimetype_constants import MIMETYPE_APPLICATION_JSON
34
36
from servicelib .status_codes_utils import (
35
37
get_http_status_codes ,
36
38
is_2xx_success ,
@@ -157,9 +159,33 @@ async def raise_exception(cls, request: web.Request):
157
159
case _: # unexpected
158
160
raise SomeUnexpectedError (cls .EXPECTED_RAISE_UNEXPECTED_REASON )
159
161
162
+ @staticmethod
163
+ async def raise_error (request : web .Request ):
164
+ raise web .HTTPNotFound
165
+
166
+ @staticmethod
167
+ async def raise_error_with_reason (request : web .Request ):
168
+ raise web .HTTPNotFound (reason = "I did not find it" )
169
+
170
+ @staticmethod
171
+ async def raise_success (request : web .Request ):
172
+ raise web .HTTPOk
173
+
174
+ @staticmethod
175
+ async def raise_success_with_reason (request : web .Request ):
176
+ raise web .HTTPOk (reason = "I'm ok" )
177
+
178
+ @staticmethod
179
+ async def raise_success_with_text (request : web .Request ):
180
+ # NOTE: explicitly NOT enveloped!
181
+ raise web .HTTPOk (reason = "I'm ok" , text = json .dumps ({"ok" : True }))
182
+
160
183
161
184
@pytest .fixture
162
- def client (event_loop , aiohttp_client ):
185
+ def client (
186
+ event_loop : asyncio .AbstractEventLoop ,
187
+ aiohttp_client : Callable ,
188
+ ):
163
189
app = web .Application ()
164
190
165
191
# routes
@@ -176,7 +202,13 @@ def client(event_loop, aiohttp_client):
176
202
("/v1/number" , Handlers .get_number ),
177
203
("/v1/mixed" , Handlers .get_mixed ),
178
204
("/v1/get_http_response" , Handlers .get_http_response ),
205
+ # custom use cases
179
206
("/v1/raise_exception" , Handlers .raise_exception ),
207
+ ("/v1/raise_error" , Handlers .raise_error ),
208
+ ("/v1/raise_error_with_reason" , Handlers .raise_error_with_reason ),
209
+ ("/v1/raise_success" , Handlers .raise_success ),
210
+ ("/v1/raise_success_with_reason" , Handlers .raise_success_with_reason ),
211
+ ("/v1/raise_success_with_text" , Handlers .raise_success_with_text ),
180
212
]
181
213
]
182
214
)
@@ -292,13 +324,14 @@ async def test_fails_with_http_successful(client: TestClient, status_code: int):
292
324
assert response .reason == Handlers .EXPECTED_HTTP_RESPONSE_REASON .format (status_code )
293
325
294
326
# NOTE: non-json response are sometimes necessary mostly on redirects
295
- # NOTE: this is how aiohptt defaults text using status and reason when empty_body is not expected
327
+ # NOTE: this is how aiohttp defaults text using status and reason when empty_body is not expected
296
328
expected = (
297
329
""
298
330
if all_aiohttp_http_exceptions [status_code ].empty_body
299
331
else f"{ response .status } : { response .reason } "
300
332
)
301
333
assert await response .text () == expected
334
+ # NOTE there is an concerning asymmetry between returning and raising web.HTTPSuccessful!!!
302
335
303
336
304
337
@pytest .mark .parametrize (
@@ -408,3 +441,55 @@ async def test_aiohttp_exceptions_construction_policies(client: TestClient):
408
441
text = await response .text ()
409
442
assert err .text == f"{ err .status } : { err .reason } "
410
443
print (text )
444
+
445
+
446
+ async def test_raise_error (client : TestClient ):
447
+ # w/o reason
448
+ resp1 = await client .get ("/v1/raise_error" )
449
+ assert resp1 .status == status .HTTP_404_NOT_FOUND
450
+ assert resp1 .content_type == MIMETYPE_APPLICATION_JSON
451
+ assert resp1 .reason == HTTPStatus (resp1 .status ).phrase
452
+
453
+ body = await resp1 .json ()
454
+ assert body ["error" ]["message" ] == resp1 .reason
455
+
456
+ # without
457
+ resp2 = await client .get ("/v1/raise_error_with_reason" )
458
+ assert resp2 .status == resp1 .status
459
+ assert resp2 .content_type == resp1 .content_type
460
+ assert resp2 .reason != resp1 .reason
461
+
462
+ body = await resp2 .json ()
463
+ assert body ["error" ]["message" ] == resp2 .reason
464
+
465
+
466
+ async def test_raise_success (client : TestClient ):
467
+ # w/o reason
468
+ resp_default = await client .get ("/v1/raise_success" )
469
+ assert resp_default .status == status .HTTP_200_OK
470
+ assert resp_default .content_type == MIMETYPE_APPLICATION_JSON
471
+ assert resp_default .reason == HTTPStatus (resp_default .status ).phrase
472
+
473
+ body = await resp_default .json ()
474
+ assert body ["data" ] == resp_default .reason
475
+
476
+ # without
477
+ resp2 = await client .get ("/v1/raise_success_with_reason" )
478
+ assert resp2 .status == resp_default .status
479
+ assert resp2 .content_type == resp_default .content_type
480
+ assert resp2 .reason != resp_default .reason
481
+
482
+ body = await resp2 .json ()
483
+ assert body ["data" ] == resp2 .reason
484
+
485
+ # with text
486
+ # NOTE: in this case, when we enforce text, then `reason` does not reach front-end anymore!
487
+ resp3 = await client .get ("/v1/raise_success_with_text" )
488
+ assert resp3 .status == resp_default .status
489
+ assert resp3 .content_type == resp_default .content_type
490
+ assert resp3 .reason != resp_default .reason
491
+
492
+ body = await resp3 .json ()
493
+ # explicitly NOT enveloped
494
+ assert "data" not in body
495
+ assert body == {"ok" : True }
0 commit comments