Skip to content

Commit a9b72c4

Browse files
authored
test(testing): add POST request tests for the 'things_advanced' example (#2581)
* update to examples and FAQ * update to examples and FAQ with corrections
1 parent 96b1fba commit a9b72c4

File tree

4 files changed

+120
-49
lines changed

4 files changed

+120
-49
lines changed

README.rst

Lines changed: 60 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -444,12 +444,12 @@ Note that this example assumes that the
444444
import uuid
445445
from wsgiref import simple_server
446446
447-
import falcon
448447
import requests
449448
449+
import falcon
450+
450451
451452
class StorageEngine:
452-
453453
def get_things(self, marker, limit):
454454
return [{'id': str(uuid.uuid4()), 'color': 'green'}]
455455
@@ -459,15 +459,13 @@ Note that this example assumes that the
459459
460460
461461
class StorageError(Exception):
462-
463462
@staticmethod
464-
def handle(ex, req, resp, params):
463+
def handle(req, resp, ex, params):
465464
# TODO: Log the error, clean up, etc. before raising
466465
raise falcon.HTTPInternalServerError()
467466
468467
469468
class SinkAdapter:
470-
471469
engines = {
472470
'ddg': 'https://duckduckgo.com',
473471
'y': 'https://search.yahoo.com/search',
@@ -478,54 +476,59 @@ Note that this example assumes that the
478476
params = {'q': req.get_param('q', True)}
479477
result = requests.get(url, params=params)
480478
481-
resp.status = str(result.status_code) + ' ' + result.reason
479+
resp.status = falcon.code_to_http_status(result.status_code)
482480
resp.content_type = result.headers['content-type']
483481
resp.text = result.text
484482
485483
486484
class AuthMiddleware:
487-
488485
def process_request(self, req, resp):
489486
token = req.get_header('Authorization')
490487
account_id = req.get_header('Account-ID')
491488
492489
challenges = ['Token type="Fernet"']
493490
494491
if token is None:
495-
description = ('Please provide an auth token '
496-
'as part of the request.')
492+
description = 'Please provide an auth token as part of the request.'
497493
498-
raise falcon.HTTPUnauthorized(title='Auth token required',
499-
description=description,
500-
challenges=challenges,
501-
href='http://docs.example.com/auth')
494+
raise falcon.HTTPUnauthorized(
495+
title='Auth token required',
496+
description=description,
497+
challenges=challenges,
498+
href='http://docs.example.com/auth',
499+
)
502500
503501
if not self._token_is_valid(token, account_id):
504-
description = ('The provided auth token is not valid. '
505-
'Please request a new token and try again.')
502+
description = (
503+
'The provided auth token is not valid. '
504+
'Please request a new token and try again.'
505+
)
506506
507-
raise falcon.HTTPUnauthorized(title='Authentication required',
508-
description=description,
509-
challenges=challenges,
510-
href='http://docs.example.com/auth')
507+
raise falcon.HTTPUnauthorized(
508+
title='Authentication required',
509+
description=description,
510+
challenges=challenges,
511+
href='http://docs.example.com/auth',
512+
)
511513
512514
def _token_is_valid(self, token, account_id):
513515
return True # Suuuuuure it's valid...
514516
515517
516518
class RequireJSON:
517-
518519
def process_request(self, req, resp):
519520
if not req.client_accepts_json:
520521
raise falcon.HTTPNotAcceptable(
521522
description='This API only supports responses encoded as JSON.',
522-
href='http://docs.examples.com/api/json')
523+
href='http://docs.examples.com/api/json',
524+
)
523525
524526
if req.method in ('POST', 'PUT'):
525527
if 'application/json' not in req.content_type:
526528
raise falcon.HTTPUnsupportedMediaType(
527529
title='This API only supports requests encoded as JSON.',
528-
href='http://docs.examples.com/api/json')
530+
href='http://docs.examples.com/api/json',
531+
)
529532
530533
531534
class JSONTranslator:
@@ -542,21 +545,24 @@ Note that this example assumes that the
542545
# Nothing to do
543546
return
544547
545-
body = req.stream.read()
548+
body = req.bounded_stream.read()
546549
if not body:
547-
raise falcon.HTTPBadRequest(title='Empty request body',
548-
description='A valid JSON document is required.')
550+
raise falcon.HTTPBadRequest(
551+
title='Empty request body',
552+
description='A valid JSON document is required.',
553+
)
549554
550555
try:
551556
req.context.doc = json.loads(body.decode('utf-8'))
552557
553558
except (ValueError, UnicodeDecodeError):
554-
description = ('Could not decode the request body. The '
555-
'JSON was incorrect or not encoded as '
556-
'UTF-8.')
559+
description = (
560+
'Could not decode the request body. The '
561+
'JSON was incorrect or not encoded as '
562+
'UTF-8.'
563+
)
557564
558-
raise falcon.HTTPBadRequest(title='Malformed JSON',
559-
description=description)
565+
raise falcon.HTTPBadRequest(title='Malformed JSON', description=description)
560566
561567
def process_response(self, req, resp, resource, req_succeeded):
562568
if not hasattr(resp.context, 'result'):
@@ -566,21 +572,22 @@ Note that this example assumes that the
566572
567573
568574
def max_body(limit):
569-
570575
def hook(req, resp, resource, params):
571576
length = req.content_length
572577
if length is not None and length > limit:
573-
msg = ('The size of the request is too large. The body must not '
574-
'exceed ' + str(limit) + ' bytes in length.')
578+
msg = (
579+
'The size of the request is too large. The body must not '
580+
'exceed ' + str(limit) + ' bytes in length.'
581+
)
575582
576583
raise falcon.HTTPContentTooLarge(
577-
title='Request body is too large', description=msg)
584+
title='Request body is too large', description=msg
585+
)
578586
579587
return hook
580588
581589
582590
class ThingsResource:
583-
584591
def __init__(self, db):
585592
self.db = db
586593
self.logger = logging.getLogger('thingsapp.' + __name__)
@@ -594,14 +601,15 @@ Note that this example assumes that the
594601
except Exception as ex:
595602
self.logger.error(ex)
596603
597-
description = ('Aliens have attacked our base! We will '
598-
'be back as soon as we fight them off. '
599-
'We appreciate your patience.')
604+
description = (
605+
'Aliens have attacked our base! We will '
606+
'be back as soon as we fight them off. '
607+
'We appreciate your patience.'
608+
)
600609
601610
raise falcon.HTTPServiceUnavailable(
602-
title='Service Outage',
603-
description=description,
604-
retry_after=30)
611+
title='Service Outage', description=description, retry_after=30
612+
)
605613
606614
# NOTE: Normally you would use resp.media for this sort of thing;
607615
# this example serves only to demonstrate how the context can be
@@ -619,19 +627,23 @@ Note that this example assumes that the
619627
except AttributeError:
620628
raise falcon.HTTPBadRequest(
621629
title='Missing thing',
622-
description='A thing must be submitted in the request body.')
630+
description='A thing must be submitted in the request body.',
631+
)
623632
624633
proper_thing = self.db.add_thing(doc)
625634
626635
resp.status = falcon.HTTP_201
627-
resp.location = '/%s/things/%s' % (user_id, proper_thing['id'])
636+
resp.location = '/{}/things/{}'.format(user_id, proper_thing['id'])
637+
628638
629639
# Configure your WSGI server to load "things.app" (app is a WSGI callable)
630-
app = falcon.App(middleware=[
631-
AuthMiddleware(),
632-
RequireJSON(),
633-
JSONTranslator(),
634-
])
640+
app = falcon.App(
641+
middleware=[
642+
AuthMiddleware(),
643+
RequireJSON(),
644+
JSONTranslator(),
645+
]
646+
)
635647
636648
db = StorageEngine()
637649
things = ThingsResource(db)

docs/user/faq.rst

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1372,6 +1372,45 @@ To include multiple values, simply use ``"; "`` to separate each name-value
13721372
pair. For example, if you were to pass ``{'Cookie': 'xxx=yyy; hello=world'}``,
13731373
you would get ``{'cookies': {'xxx': 'yyy', 'hello': 'world'}}``.
13741374

1375+
How can I set header fields when simulating requests?
1376+
-----------------------------------------------------
1377+
1378+
Default header fields can be overwritten to simulate unexpected
1379+
behavior. For instance, to test the condition where a ``POST``
1380+
request has an empty body but the value of ``Content-Length``
1381+
is non-zero, we can overwrite that value in the header.
1382+
1383+
.. code:: python
1384+
1385+
import falcon
1386+
import falcon.testing
1387+
import pytest
1388+
1389+
class PostTest:
1390+
1391+
def on_post(self, req, resp):
1392+
if req.content_length in (None, 0):
1393+
resp.status = falcon.HTTP_200
1394+
else:
1395+
if req.stream.read(req.content_length or 0):
1396+
resp.status = falcon.HTTP_201
1397+
else:
1398+
resp.status = falcon.HTTP_400
1399+
1400+
@pytest.fixture
1401+
def client():
1402+
app = falcon.App()
1403+
app.add_route('/resource', PostTest())
1404+
1405+
return falcon.testing.TestClient(app)
1406+
1407+
1408+
def test_post_empty_body_with_length(client):
1409+
headers = [('Content-Length', '1'),]
1410+
body = ''
1411+
result = client.simulate_post(path='/resource', body=body, headers=headers)
1412+
assert(result.status == falcon.HTTP_400)
1413+
13751414
Why do I see no error tracebacks in my ASGI application?
13761415
--------------------------------------------------------
13771416

examples/things_advanced.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ def process_request(self, req, resp):
106106
# Nothing to do
107107
return
108108

109-
body = req.stream.read()
109+
body = req.bounded_stream.read()
110110
if not body:
111111
raise falcon.HTTPBadRequest(
112112
title='Empty request body',

tests/test_examples.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,23 @@ def test_things_advanced(asgi, util, httpx, requests):
5858
assert resp2.status_code == 200
5959
assert len(resp2.json) == 1
6060
assert resp2.json[0]['color'] == 'green'
61+
62+
resp3 = testing.simulate_post(
63+
advanced.app,
64+
'/1337/things',
65+
headers={'Authorization': 'custom-token', 'Content-Type': 'application/json'},
66+
body='{"key": "value"}',
67+
)
68+
assert resp3.status_code == 201
69+
70+
resp4 = testing.simulate_post(
71+
advanced.app,
72+
'/1337/things',
73+
headers={
74+
'Authorization': 'custom-token',
75+
'Content-Type': 'application/json',
76+
'Content-Length': '1',
77+
},
78+
body='',
79+
)
80+
assert resp4.status_code == 400

0 commit comments

Comments
 (0)