def test_asgi_conductor_raised_error_skips_shutdown(): class SomeException(Exception): pass class Foo: def __init__(self): self.called_startup = False self.called_shutdown = False async def process_startup(self, scope, event): self.called_startup = True async def process_shutdown(self, scope, event): self.called_shutdown = True foo = Foo() app = App() app.add_middleware(foo) async def t(): with pytest.raises(SomeException): async with testing.ASGIConductor(app): raise SomeException() falcon.async_to_sync(t) assert foo.called_startup assert not foo.called_shutdown
def test_lifespan_scope_version(spec_version, supported): app = App() shutting_down = asyncio.Condition() req_event_emitter = testing.ASGILifespanEventEmitter(shutting_down) resp_event_collector = testing.ASGIResponseEventCollector() scope = { 'type': 'lifespan', 'asgi': {'spec_version': spec_version, 'version': '3.0'} } if not supported: with pytest.raises(UnsupportedScopeError): falcon.async_to_sync( app.__call__, scope, req_event_emitter, resp_event_collector ) return async def t(): t = asyncio.get_event_loop().create_task( app(scope, req_event_emitter, resp_event_collector) ) # NOTE(kgriffs): Yield to the lifespan task above await asyncio.sleep(0.001) async with shutting_down: shutting_down.notify() await t falcon.async_to_sync(t)
def test_lifespan_scope_default_version(): app = App() resource = testing.SimpleTestResourceAsync() app.add_route('/', resource) shutting_down = asyncio.Condition() req_event_emitter = testing.ASGILifespanEventEmitter(shutting_down) resp_event_collector = testing.ASGIResponseEventCollector() scope = {'type': 'lifespan'} async def t(): t = asyncio.get_event_loop().create_task( app(scope, req_event_emitter, resp_event_collector) ) # NOTE(kgriffs): Yield to the lifespan task above await asyncio.sleep(0.001) async with shutting_down: shutting_down.notify() await t falcon.async_to_sync(t) assert not resource.called
def test_filelike(): s = asgi.BoundedStream(testing.ASGIRequestEventEmitter()) for __ in range(2): with pytest.raises(OSError): s.fileno() assert not s.isatty() assert s.readable() assert not s.seekable() assert not s.writable() s.close() assert s.closed # NOTE(kgriffs): Closing an already-closed stream is a noop. s.close() assert s.closed async def test_iteration(): with pytest.raises(ValueError): await s.read() with pytest.raises(ValueError): await s.readall() with pytest.raises(ValueError): await s.exhaust() with pytest.raises(ValueError): async for chunk in s: pass falcon.async_to_sync(test_iteration)
def test_parser_async(body, doc): with disable_asgi_non_coroutine_wrapping(): class WrappedRespondersBodyParserAsyncResource: @falcon.before(validate_param_async, 'limit', 100, is_async=True) @falcon.before(parse_body_async) async def on_get(self, req, resp, doc=None): self.doc = doc @falcon.before(parse_body_async, is_async=False) async def on_put(self, req, resp, doc=None): self.doc = doc app = create_app(asgi=True) resource = WrappedRespondersBodyParserAsyncResource() app.add_route('/', resource) testing.simulate_get(app, '/', body=body) assert resource.doc == doc testing.simulate_put(app, '/', body=body) assert resource.doc == doc async def test_direct(): resource = WrappedRespondersBodyParserAsyncResource() req = testing.create_asgi_req() resp = create_resp(True) await resource.on_get(req, resp, doc) assert resource.doc == doc falcon.async_to_sync(test_direct)
def test_read_chunks(body, chunk_size): def stream(): return _stream(body) async def test_nonmixed(): s = stream() assert await s.read(0) == b'' chunks = [] while not s.eof: chunks.append(await s.read(chunk_size)) assert b''.join(chunks) == body async def test_mixed_a(): s = stream() chunks = [] chunks.append(await s.read(chunk_size)) chunks.append(await s.read(chunk_size)) chunks.append(await s.readall()) chunks.append(await s.read(chunk_size)) assert b''.join(chunks) == body async def test_mixed_b(): s = stream() chunks = [] chunks.append(await s.read(chunk_size)) chunks.append(await s.read(-1)) assert b''.join(chunks) == body async def test_mixed_iter(): s = stream() chunks = [await s.read(chunk_size)] chunks += [data async for data in s] assert b''.join(chunks) == body for t in (test_nonmixed, test_mixed_a, test_mixed_b, test_mixed_iter): falcon.async_to_sync(t) falcon.async_to_sync(t)
def _call_with_scope(scope): app = App() resource = testing.SimpleTestResourceAsync() app.add_route('/', resource) req_event_emitter = testing.ASGIRequestEventEmitter() resp_event_collector = testing.ASGIResponseEventCollector() falcon.async_to_sync(app.__call__, scope, req_event_emitter, resp_event_collector) assert resource.called return resource
def test_bad_path(asgi, uri, patch_open): patch_open(b'') sr_type = StaticRouteAsync if asgi else StaticRoute sr = sr_type('/static', '/var/www/statics') req = _util.create_req(asgi, host='test.com', path=uri, root_path='statics') resp = _util.create_resp(asgi) with pytest.raises(falcon.HTTPNotFound): if asgi: falcon.async_to_sync(sr, req, resp) else: sr(req, resp)
def test_pathlib_path(asgi, patch_open): patch_open() sr = create_sr(asgi, '/static/', pathlib.Path('/var/www/statics')) req_path = '/static/css/test.css' req = _util.create_req(asgi, host='test.com', path=req_path, root_path='statics') resp = _util.create_resp(asgi) if asgi: async def run(): await sr(req, resp) return await resp.stream.read() body = falcon.async_to_sync(run) else: sr(req, resp) body = resp.stream.read() assert body.decode() == os.path.normpath('/var/www/statics/css/test.css')
def test_urlencoded_form_handler_serialize(data, expected): handler = media.URLEncodedFormHandler() assert handler.serialize(data, falcon.MEDIA_URLENCODED) == expected value = falcon.async_to_sync(handler.serialize_async, data, falcon.MEDIA_URLENCODED) assert value == expected
def test_good_path(asgi, uri_prefix, uri_path, expected_path, mtype, monkeypatch): monkeypatch.setattr(io, 'open', lambda path, mode: io.BytesIO(path.encode())) sr = create_sr(asgi, uri_prefix, '/var/www/statics') req_path = uri_prefix[:-1] if uri_prefix.endswith('/') else uri_prefix req_path += uri_path req = _util.create_req(asgi, host='test.com', path=req_path, root_path='statics') resp = _util.create_resp(asgi) if asgi: async def run(): await sr(req, resp) return await resp.stream.read() body = falcon.async_to_sync(run) else: sr(req, resp) body = resp.stream.read() assert resp.content_type in _MIME_ALTERNATIVE.get(mtype, (mtype, )) assert body.decode() == os.path.normpath('/var/www/statics' + expected_path)
def test_good_path(asgi, uri_prefix, uri_path, expected_path, mtype, patch_open): patch_open() sr = create_sr(asgi, uri_prefix, '/var/www/statics') req_path = uri_prefix[:-1] if uri_prefix.endswith('/') else uri_prefix req_path += uri_path req = _util.create_req(asgi, host='test.com', path=req_path, root_path='statics') resp = _util.create_resp(asgi) if asgi: async def run(): await sr(req, resp) return await resp.stream.read() body = falcon.async_to_sync(run) else: sr(req, resp) body = resp.stream.read() assert resp.content_type in _MIME_ALTERNATIVE.get(mtype, (mtype, )) assert body.decode() == os.path.normpath('/var/www/statics' + expected_path) assert resp.headers.get('accept-ranges') == 'bytes'
def call_method(asgi, method_name, *args): resource = ResourceAsync() if asgi else Resource() if asgi: return falcon.async_to_sync(getattr(resource, method_name), *args) return getattr(resource, method_name)(*args)
def test_bad_path(asgi, uri, monkeypatch): monkeypatch.setattr(io, 'open', lambda path, mode: io.BytesIO()) sr_type = StaticRouteAsync if asgi else StaticRoute sr = sr_type('/static', '/var/www/statics') req = _util.create_req(asgi, host='test.com', path=uri, root_path='statics') resp = _util.create_resp(asgi) with pytest.raises(falcon.HTTPNotFound): if asgi: falcon.async_to_sync(sr, req, resp) else: sr(req, resp)
def test_exhaust_with_disconnect(): async def t(): emitter = testing.ASGIRequestEventEmitter( b'123456789' * 2, # NOTE(kgriffs): This must be small enough to create several events chunk_size=3, ) s = asgi.BoundedStream(emitter) assert await s.read(1) == b'1' assert await s.read(2) == b'23' emitter.disconnect(exhaust_body=False) await s.exhaust() assert await s.read(1) == b'' assert await s.read(100) == b'' assert s.eof falcon.async_to_sync(t)
def async_take(source, count=None): async def collect(): result = [] async for item in source: result.append(item) if count is not None and count <= len(result): return result return result return falcon.async_to_sync(collect)
def test_supported_asgi_version(version, supported): scope = { 'type': 'lifespan', 'asgi': { 'spec_version': '2.0', 'version': version }, } if version is None: del scope['asgi']['version'] app = App() resource = testing.SimpleTestResourceAsync() app.add_route('/', resource) shutting_down = asyncio.Condition() req_event_emitter = testing.ASGILifespanEventEmitter(shutting_down) resp_event_collector = testing.ASGIResponseEventCollector() async def task(): coro = asyncio.get_event_loop().create_task( app(scope, req_event_emitter, resp_event_collector)) # NOTE(vytas): Yield to the lifespan task above. await asyncio.sleep(0) assert len(resp_event_collector.events) == 1 event = resp_event_collector.events[0] if supported: assert event['type'] == 'lifespan.startup.complete' else: assert event['type'] == 'lifespan.startup.failed' assert event['message'].startswith( 'Falcon requires ASGI version 3.x') async with shutting_down: shutting_down.notify() await coro falcon.async_to_sync(task)
def test_multiple_events_early_disconnect(): class SomeResource: async def on_get(self, req, resp): async def emitter(): while True: yield SSEvent(data=b'whassup') await asyncio.sleep(0.01) resp.sse = emitter() resource = SomeResource() app = App() app.add_route('/', resource) async def _test(): conductor = testing.ASGIConductor(app) result = await conductor.simulate_get() assert 'data: whassup' in result.text async with testing.ASGIConductor(app) as conductor: # NOTE(vytas): Using the get_stream() alias. async with conductor.get_stream() as sr: event_count = 0 result_text = '' while event_count < 5: chunk = (await sr.stream.read()).decode() if not chunk: continue result_text += chunk event_count += len(chunk.strip().split('\n\n')) assert result_text.startswith('data: whassup\n\n' * 5) assert event_count == 5 falcon.async_to_sync(_test)
def test_resource_with_uri_fields_async(): app = create_app(asgi=True) resource = ClassResourceWithURIFieldsAsync() app.add_route('/{field1}/{field2}', resource) result = testing.simulate_get(app, '/a/b') assert result.status_code == 200 assert result.headers['X-Fluffiness'] == 'fluffy' assert resource.fields == ('a', 'b') async def test_direct(): resource = ClassResourceWithURIFieldsAsync() req = testing.create_asgi_req() resp = create_resp(True) await resource.on_get(req, resp, '1', '2') assert resource.fields == ('1', '2') falcon.async_to_sync(test_direct)
def test_iteration_already_started(): body = testing.rand_string(1, 2048).encode() s = _stream(body) async def t(): stream_iter = s.__aiter__() chunks = [await stream_iter.__anext__()] with pytest.raises(ValueError): stream_iter2 = s.__aiter__() await stream_iter2.__anext__() while True: try: chunks.append(await stream_iter.__anext__()) except StopAsyncIteration: break assert b''.join(chunks) == body falcon.async_to_sync(t)
def test_serialization(asgi, func, body, expected): handler = media.JSONHandler(dumps=func) args = (body, b'application/javacript') if asgi: result = falcon.async_to_sync(handler.serialize_async, *args) else: result = handler.serialize(*args) # NOTE(nZac) PyPy and CPython render the final string differently. One # includes spaces and the other doesn't. This replace will normalize that. assert result.replace(b' ', b'') == expected # noqa
def test_fallback_filename(asgi, uri, default, expected, content_type, downloadable, patch_open, monkeypatch): def validate(path): if os.path.normpath(default) not in path: raise IOError() patch_open(validate=validate) monkeypatch.setattr('os.path.isfile', lambda file: os.path.normpath(default) in file) sr = create_sr( asgi, '/static', '/var/www/statics', downloadable=downloadable, fallback_filename=default, ) req_path = '/static/' + uri req = _util.create_req(asgi, host='test.com', path=req_path, root_path='statics') resp = _util.create_resp(asgi) if asgi: async def run(): await sr(req, resp) return await resp.stream.read() body = falcon.async_to_sync(run) else: sr(req, resp) body = resp.stream.read() assert sr.match(req.path) expected_content = os.path.normpath( os.path.join('/var/www/statics', expected)) assert body.decode() == expected_content assert resp.content_type in _MIME_ALTERNATIVE.get(content_type, (content_type, )) assert resp.headers.get('accept-ranges') == 'bytes' if downloadable: assert os.path.basename(expected) in resp.downloadable_as else: assert resp.downloadable_as is None
def test_fallback_filename(asgi, uri, default, expected, content_type, downloadable, monkeypatch): def mock_open(path, mode): if os.path.normpath(default) in path: return io.BytesIO(path.encode()) raise IOError() monkeypatch.setattr(io, 'open', mock_open) monkeypatch.setattr('os.path.isfile', lambda file: os.path.normpath(default) in file) sr = create_sr(asgi, '/static', '/var/www/statics', downloadable=downloadable, fallback_filename=default) req_path = '/static/' + uri req = _util.create_req(asgi, host='test.com', path=req_path, root_path='statics') resp = _util.create_resp(asgi) if asgi: async def run(): await sr(req, resp) return await resp.stream.read() body = falcon.async_to_sync(run) else: sr(req, resp) body = resp.stream.read() assert sr.match(req.path) assert body.decode() == os.path.normpath( os.path.join('/var/www/statics', expected)) assert resp.content_type in _MIME_ALTERNATIVE.get(content_type, (content_type, )) if downloadable: assert os.path.basename(expected) in resp.downloadable_as else: assert resp.downloadable_as is None
def test_deserialization(asgi, func, body, expected): handler = media.JSONHandler(loads=func) args = ['application/javacript', len(body)] if asgi: if not ASGI_SUPPORTED: pytest.skip('ASGI requires Python 3.6+') from falcon.asgi.stream import BoundedStream s = BoundedStream(testing.ASGIRequestEventEmitter(body)) args.insert(0, s) result = falcon.async_to_sync(handler.deserialize_async, *args) else: args.insert(0, io.BytesIO(body)) result = handler.deserialize(*args) assert result == expected
def test_read_all(body, extra_body, set_content_length): if extra_body and not set_content_length: pytest.skip( 'extra_body ignores set_content_length so we only need to test ' 'one of the parameter permutations' ) expected_body = body if isinstance(body, bytes) else body.encode() def stream(): stream_body = body content_length = None if extra_body: # NOTE(kgriffs): Test emitting more data than expected to the app content_length = len(expected_body) stream_body += b'\x00' if isinstance(stream_body, bytes) else '~' elif set_content_length: content_length = len(expected_body) return _stream(stream_body, content_length=content_length) async def test_iteration(): s = stream() chunks = [chunk async for chunk in s] if not (expected_body or extra_body): assert not chunks assert b''.join(chunks) == expected_body assert await s.read() == b'' assert await s.readall() == b'' assert not [chunk async for chunk in s] assert s.tell() == len(expected_body) assert s.eof async def test_readall_a(): s = stream() assert await s.readall() == expected_body assert await s.read() == b'' assert await s.readall() == b'' assert not [chunk async for chunk in s] assert s.tell() == len(expected_body) assert s.eof async def test_readall_b(): s = stream() assert await s.read() == expected_body assert await s.readall() == b'' assert await s.read() == b'' assert not [chunk async for chunk in s] assert s.tell() == len(expected_body) assert s.eof async def test_readall_c(): s = stream() body = await s.read(1) body += await s.read(None) assert body == expected_body assert s.tell() == len(expected_body) assert s.eof async def test_readall_d(): s = stream() assert not s.closed if expected_body: assert not s.eof elif set_content_length: assert s.eof else: # NOTE(kgriffs): Stream doesn't know if there is more data # coming or not until the first read. assert not s.eof assert s.tell() == 0 assert await s.read(-2) == b'' assert await s.read(-3) == b'' assert await s.read(-100) == b'' assert await s.read(-1) == expected_body assert await s.read(-1) == b'' assert await s.readall() == b'' assert await s.read() == b'' assert not [chunk async for chunk in s] assert await s.read(-2) == b'' assert s.tell() == len(expected_body) assert s.eof assert not s.closed s.close() assert s.closed for t in (test_iteration, test_readall_a, test_readall_b, test_readall_c, test_readall_d): falcon.async_to_sync(t)
def test_multiple_events(): expected_result_text = ('data: ketchup\n' '\n' 'event: condiment\n' 'data: mustard\n' '\n' 'event: condiment\n' 'id: 1234\n' 'data: mayo\n' '\n' 'event: topping\n' 'id: 5678\n' 'retry: 100\n' 'data: onions\n' '\n' ': Serve with chips.\n' 'retry: 100\n' 'data: guacamole \u1F951\n' '\n' 'retry: 100\n' 'data: {"condiment": "salsa"}\n' '\n') class SomeResource: async def on_get(self, req, resp): async def emitter(): for event in [ SSEvent(data=b'ketchup'), SSEvent(data=b'mustard', event='condiment'), SSEvent(data=b'mayo', event='condiment', event_id='1234'), SSEvent(data=b'onions', event='topping', event_id='5678', retry=100), SSEvent(text='guacamole \u1F951', retry=100, comment='Serve with chips.'), SSEvent(json={'condiment': 'salsa'}, retry=100), ]: yield event await asyncio.sleep(0.001) resp.sse = emitter() resource = SomeResource() app = App() app.add_route('/', resource) client = testing.TestClient(app) async def _test(): async with client as conductor: # NOTE(kgriffs): Single-shot test will only allow the first # one or two events since a client disconnect will be emitted # into the app immediately. result = await conductor.simulate_get() assert expected_result_text.startswith(result.text) async with conductor.simulate_get_stream() as sr: event_count = 0 result_text = '' while True: chunk = (await sr.stream.read()).decode() if not chunk: continue result_text += chunk event_count += len(chunk.strip().split('\n\n')) if 'salsa' in chunk: break assert not (await sr.stream.read()) assert event_count == 6 assert result_text == expected_result_text falcon.async_to_sync(_test)