7
7
import json
8
8
import logging
9
9
from dataclasses import dataclass
10
+ from http import HTTPStatus
10
11
from typing import Any
11
12
12
13
import pytest
33
34
from servicelib .status_codes_utils import (
34
35
get_http_status_codes ,
35
36
is_client_error ,
37
+ is_error ,
36
38
is_server_error ,
37
39
is_success ,
38
40
)
@@ -107,12 +109,12 @@ def returns_value(cls, suffix):
107
109
returned_value = loop .run_until_complete (coro (None ))
108
110
return json .loads (json_dumps (returned_value ))
109
111
110
- HTTP_RESPONSE_REASON = "Response with code= {}"
112
+ EXPECTED_HTTP_RESPONSE_REASON = "custom reason for code {}"
111
113
112
114
@classmethod
113
- async def raise_http_response (cls , request : web .Request ):
115
+ async def get_http_response (cls , request : web .Request ):
114
116
status_code = int (request .query ["code" ])
115
- reason = cls .HTTP_RESPONSE_REASON .format (status_code )
117
+ reason = cls .EXPECTED_HTTP_RESPONSE_REASON .format (status_code )
116
118
117
119
match status_code :
118
120
case status .HTTP_405_METHOD_NOT_ALLOWED :
@@ -129,14 +131,14 @@ async def raise_http_response(cls, request: web.Request):
129
131
)
130
132
case _:
131
133
http_response_cls = all_aiohttp_http_exceptions [status_code ]
132
- raise http_response_cls (reason = reason )
134
+ if is_error (status_code ):
135
+ # 4XX and 5XX are raised
136
+ raise http_response_cls (reason = reason )
137
+ else :
138
+ # otherwise returned
139
+ return http_response_cls (reason = reason )
133
140
134
- RAISE_UNEXPECTED_REASON = "Unexpected error"
135
-
136
- @classmethod
137
- async def raise_unexpected (cls , request : web .Request ):
138
- assert request
139
- raise SomeUnexpectedError (cls .RAISE_UNEXPECTED_REASON )
141
+ EXPECTED_RAISE_UNEXPECTED_REASON = "Unexpected error"
140
142
141
143
@classmethod
142
144
async def raise_exception (cls , request : web .Request ):
@@ -146,6 +148,14 @@ async def raise_exception(cls, request: web.Request):
146
148
raise NotImplementedError
147
149
case asyncio .TimeoutError .__name__ :
148
150
raise asyncio .TimeoutError
151
+ case web .HTTPOk .__name__ :
152
+ raise web .HTTPOk # 2XX
153
+ case web .HTTPUnauthorized .__name__ :
154
+ raise web .HTTPUnauthorized # 4XX
155
+ case web .HTTPServiceUnavailable .__name__ :
156
+ raise web .HTTPServiceUnavailable # 5XX
157
+ case _: # unexpected
158
+ raise SomeUnexpectedError (cls .EXPECTED_RAISE_UNEXPECTED_REASON )
149
159
150
160
151
161
@pytest .fixture
@@ -165,13 +175,22 @@ def client(event_loop, aiohttp_client):
165
175
("/v1/string" , Handlers .get_string ),
166
176
("/v1/number" , Handlers .get_number ),
167
177
("/v1/mixed" , Handlers .get_mixed ),
168
- ("/v1/raise_http_code" , Handlers .raise_http_response ),
169
- ("/v1/raise_unexpected" , Handlers .raise_unexpected ),
178
+ ("/v1/get_http_response" , Handlers .get_http_response ),
170
179
("/v1/raise_exception" , Handlers .raise_exception ),
171
180
]
172
181
]
173
182
)
174
183
184
+ app .router .add_routes (
185
+ [
186
+ web .get (
187
+ "/free/raise_exception" ,
188
+ Handlers .raise_exception ,
189
+ name = "raise_exception_without_middleware" ,
190
+ )
191
+ ]
192
+ )
193
+
175
194
# middlewares
176
195
app .middlewares .append (error_middleware_factory (api_version = "/v1" ))
177
196
app .middlewares .append (envelope_middleware_factory (api_version = "/v1" ))
@@ -227,13 +246,15 @@ def _is_server_error(code):
227
246
228
247
@pytest .mark .parametrize ("status_code" , get_http_status_codes (status , _is_server_error ))
229
248
async def test_fails_with_http_server_error (client : TestClient , status_code : int ):
230
- response = await client .get ("/v1/raise_http_code " , params = {"code" : status_code })
249
+ response = await client .get ("/v1/get_http_response " , params = {"code" : status_code })
231
250
assert response .status == status_code
232
251
233
252
data , error = unwrap_envelope (await response .json ())
234
253
assert not data
235
254
assert error
236
- assert error ["message" ] == Handlers .HTTP_RESPONSE_REASON .format (status_code )
255
+ assert error ["message" ] == Handlers .EXPECTED_HTTP_RESPONSE_REASON .format (
256
+ status_code
257
+ )
237
258
238
259
239
260
def _is_client_error (code ):
@@ -244,13 +265,15 @@ def _is_client_error(code):
244
265
245
266
@pytest .mark .parametrize ("status_code" , get_http_status_codes (status , _is_client_error ))
246
267
async def test_fails_with_http_client_error (client : TestClient , status_code : int ):
247
- response = await client .get ("/v1/raise_http_code " , params = {"code" : status_code })
268
+ response = await client .get ("/v1/get_http_response " , params = {"code" : status_code })
248
269
assert response .status == status_code
249
270
250
271
data , error = unwrap_envelope (await response .json ())
251
272
assert not data
252
273
assert error
253
- assert error ["message" ] == Handlers .HTTP_RESPONSE_REASON .format (status_code )
274
+ assert error ["message" ] == Handlers .EXPECTED_HTTP_RESPONSE_REASON .format (
275
+ status_code
276
+ )
254
277
assert error ["errors" ]
255
278
256
279
@@ -260,9 +283,11 @@ def _is_success(code):
260
283
261
284
@pytest .mark .parametrize ("status_code" , get_http_status_codes (status , _is_success ))
262
285
async def test_fails_with_http_successful (client : TestClient , status_code : int ):
263
- response = await client .get ("/v1/raise_http_code " , params = {"code" : status_code })
286
+ response = await client .get ("/v1/get_http_response " , params = {"code" : status_code })
264
287
assert response .status == status_code
265
288
289
+ print (await response .text ())
290
+
266
291
data , error = unwrap_envelope (await response .json ())
267
292
assert not error
268
293
assert data
@@ -291,7 +316,7 @@ async def test_raised_unhandled_exception(
291
316
client : TestClient , caplog : pytest .LogCaptureFixture
292
317
):
293
318
caplog .set_level (logging .ERROR )
294
- response = await client .get ("/v1/raise_unexpected " )
319
+ response = await client .get ("/v1/raise_exception " )
295
320
296
321
# respond the client with 500
297
322
assert response .status == status .HTTP_500_INTERNAL_SERVER_ERROR
@@ -314,23 +339,63 @@ async def test_raised_unhandled_exception(
314
339
315
340
# log sufficient information to diagnose the issue
316
341
#
317
- # ERROR servicelib.aiohttp.rest_middlewares:rest_middlewares.py:96 Request 'GET /v1/raise_unexpected ' raised 'SomeUnhandledError' [OEC:140555466658464]
342
+ # ERROR servicelib.aiohttp.rest_middlewares:rest_middlewares.py:96 Request 'GET /v1/raise_exception ' raised 'SomeUnhandledError' [OEC:140555466658464]
318
343
# request.remote='127.0.0.1'
319
344
# request.headers={b'Host': b'127.0.0.1:33461', b'Accept': b'*/*', b'Accept-Encoding': b'gzip, deflate', b'User-Agent': b'Python/3.10 aiohttp/3.8.6'}
320
345
# Traceback (most recent call last):
321
346
# File "osparc-simcore/packages/service-library/src/servicelib/aiohttp/rest_middlewares.py", line 120, in _middleware_handler
322
347
# return await handler(request)
323
348
# File "osparc-simcore/packages/service-library/src/servicelib/aiohttp/rest_middlewares.py", line 177, in _middleware_handler
324
349
# resp_or_data = await handler(request)
325
- # File "osparc-simcore/packages/service-library/tests/aiohttp/test_rest_middlewares.py", line 107, in raise_unexpected
326
- # raise SomeUnhandledError(cls.raise_unexpected_REASON )
350
+ # File "osparc-simcore/packages/service-library/tests/aiohttp/test_rest_middlewares.py", line 107, in raise_exception
351
+ # raise SomeUnhandledError(cls.EXPECTED_RAISE_UNEXPECTED_REASON )
327
352
# tests.aiohttp.test_rest_middlewares.SomeUnhandledError: Unexpected error
328
353
329
354
assert response .method in caplog .text
330
355
assert response .url .path in caplog .text
331
356
assert "request.headers=" in caplog .text
332
357
assert "request.remote=" in caplog .text
333
358
assert SomeUnexpectedError .__name__ in caplog .text
334
- assert Handlers .RAISE_UNEXPECTED_REASON in caplog .text
359
+ assert Handlers .EXPECTED_RAISE_UNEXPECTED_REASON in caplog .text
335
360
# log OEC
336
361
assert "OEC:" in caplog .text
362
+
363
+
364
+ async def test_aiohttp_exceptions_construction_policies (client : TestClient ):
365
+
366
+ # using default constructor
367
+ err = web .HTTPOk ()
368
+ assert err .status == status .HTTP_200_OK
369
+ assert err .content_type == "text/plain"
370
+ # reason is an exception property and is default to
371
+ assert err .reason == HTTPStatus (status .HTTP_200_OK ).phrase
372
+ # default text if nothing set!
373
+ assert err .text == f"{ err .status } : { err .reason } "
374
+
375
+ # This is how it is transformed into a response
376
+ #
377
+ # NOTE: that the reqson is somehow transmitted in the header
378
+ #
379
+ # version = request.version
380
+ # status_line = "HTTP/{}.{} {} {}".format(
381
+ # version[0], version[1], self._status, self._reason
382
+ # )
383
+ # await writer.write_headers(status_line, self._headers)
384
+ #
385
+ #
386
+ assert client .app
387
+ assert (
388
+ client .app .router ["raise_exception_without_middleware" ].url_for ().path
389
+ == "/free/raise_exception"
390
+ )
391
+
392
+ response = await client .get (
393
+ "/free/raise_exception" , params = {"exc" : web .HTTPOk .__name__ }
394
+ )
395
+ assert response .status == status .HTTP_200_OK
396
+ assert response .reason == err .reason # I wonder how this is passed
397
+ assert response .content_type == err .content_type
398
+
399
+ text = await response .text ()
400
+ assert err .text == f"{ err .status } : { err .reason } "
401
+ print (text )
0 commit comments