async def test_responders(method): class Resource: async def on_get(self, req, resp): resp.set_header('method', 'get') async def on_head(self, req, resp): resp.set_header('method', 'head') async def on_post(self, req, resp): resp.set_header('method', 'post') async def on_put(self, req, resp): resp.set_header('method', 'put') async def on_options(self, req, resp): resp.set_header('method', 'options') async def on_patch(self, req, resp): resp.set_header('method', 'patch') async def on_delete(self, req, resp): resp.set_header('method', 'delete') resource = Resource() app = App() app.add_route('/', resource) async with testing.ASGIConductor(app) as conductor: simulate = getattr(conductor, 'simulate_' + method) result = await simulate('/') assert result.headers['method'] == method
def test_no_events(): class Emitter: def __aiter__(self): return self async def __anext__(self): raise StopAsyncIteration class SomeResource: async def on_get(self, req, resp): self._called = True resp.sse = Emitter() # NOTE(vytas): Test explicitly referencing property since it is # normally optimized away by operating directly on the private # attribute in hot App.__call__ code paths. assert resp.sse is not None resource = SomeResource() app = App() app.add_route('/', resource) client = testing.TestClient(app) client.simulate_get() assert resource._called
def test_single_event(): class SomeResource: async def on_get(self, req, resp): async def emitter(): yield resp.sse = emitter() async def on_post(self, req, resp): async def emitter(): yield SSEvent() resp.sse = emitter() resource = SomeResource() app = App() app.add_route('/', resource) client = testing.TestClient(app) result = client.simulate_get() assert result.text == ': ping\n\n' result = client.simulate_post() assert result.text == ': ping\n\n'
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.invoke_coroutine_sync(t) assert not resource.called
async def test_default_headers(): class Resource: async def on_get(self, req, resp): resp.set_header('the-answer', req.get_header('the-answer')) app = App() app.add_route('/', Resource()) client = testing.TestClient(app, headers={'the-answer': '42'}) async with client as conductor: result = await conductor.simulate_get() assert result.headers['the-answer'] == '42'
async def test_generic_request(simulate_method): class Resource: async def on_lock(self, req, resp): raise falcon.HTTPUnprocessableEntity app = App() app.add_route('/file', Resource()) client = testing.TestClient(app) async with client as conductor: simulate_request = getattr(conductor, simulate_method) result = await simulate_request('LOCK', '/file') assert result.status_code == 422
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.invoke_coroutine_sync(app.__call__, scope, req_event_emitter, resp_event_collector) assert resource.called return resource
def client(self): class SomeResource: async def on_get(self, req, resp): async def emitter(): yield SSEvent(json={'foo': 'bar'}) yield SSEvent(json={'bar': 'baz'}) resp.sse = emitter() resource = SomeResource() app = App() app.add_route('/', resource) client = testing.TestClient(app) return client
def test_multiple_events(): class SomeResource: async def on_get(self, req, resp): async def emitter(): yield SSEvent(data=b'ketchup') yield SSEvent(data=b'mustard', event='condiment') yield SSEvent(data=b'mayo', event='condiment', event_id='1234') yield SSEvent(data=b'onions', event='topping', event_id='5678', retry=100) yield SSEvent(text='guacamole \u1F951', retry=100, comment='Serve with chips.') yield SSEvent(json={'condiment': 'salsa'}, retry=100) resp.sse = emitter() resource = SomeResource() app = App() app.add_route('/', resource) client = testing.TestClient(app) result = client.simulate_get() assert 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')
def test_non_iterable(): class SomeResource: async def on_get(self, req, resp): async def emitter(): yield resp.sse = emitter resource = SomeResource() app = App() app.add_route('/', resource) client = testing.TestClient(app) with pytest.raises(TypeError): client.simulate_get()
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 callback_app(simple_resource): app = App() app.add_route('/', simple_resource) app.add_route('/sse', simple_resource, suffix='sse') app.add_route('/stream', simple_resource, suffix='stream') return app
def test_no_events(): class Emitter: def __aiter__(self): return self async def __anext__(self): raise StopAsyncIteration class SomeResource: async def on_get(self, req, resp): self._called = True resp.sse = Emitter() resource = SomeResource() app = App() app.add_route('/', resource) client = testing.TestClient(app) client.simulate_get() assert resource._called
"""Configures the main Falcon app and routes `app` is hoisted to the main application; e.g.: from application import app """ from falcon.asgi import App from . import views from .environment import settings from .middleware.sqlalchemy import SQLAlchemySessionManager from .middleware.firebase import FirebaseSessionManager # Create our main application app = App(middleware=[ SQLAlchemySessionManager(), FirebaseSessionManager(), ]) # Allow HTTP cookies for local development app.resp_options.secure_cookies_by_default = settings.debug # Define our application routes app.add_route('/health-check', views.HealthCheck()) app.add_route('/session', views.FirebaseAuth())
def test_multiple(): class SomeResource: def __init__(self): self.counter = Counter() async def on_get(self, req, resp): async def background_job_async(): self.counter['backround:on_get:async'] += 1 def background_job_sync(): self.counter['backround:on_get:sync'] += 20 with pytest.raises(TypeError): resp.schedule(background_job_sync) resp.schedule_sync(background_job_sync) resp.schedule(background_job_async) resp.schedule_sync(background_job_sync) resp.schedule(background_job_async) async def on_post(self, req, resp): async def background_job_async(): self.counter['backround:on_get:async'] += 1000 def background_job_sync(): self.counter['backround:on_get:sync'] += 2000 resp.schedule(background_job_async) resp.schedule(background_job_async) resp.schedule_sync(background_job_sync) resp.schedule_sync(background_job_sync) async def on_put(self, req, resp): async def background_job_async(): self.counter['backround:on_get:async'] += 1000 c = background_job_async() try: resp.schedule(c) finally: await c resource = SomeResource() app = App() app.add_route('/', resource) client = testing.TestClient(app) client.simulate_get() client.simulate_post() time.sleep(0.5) assert resource.counter['backround:on_get:async'] == 2002 assert resource.counter['backround:on_get:sync'] == 4040 result = client.simulate_put() assert result.status_code == 500 # NOTE(kgriffs): Remove default handlers so that we can check the raised # exception is what we expecte. app._error_handlers.clear() with pytest.raises(TypeError) as exinfo: client.simulate_put() assert 'coroutine' in str(exinfo.value)
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)
'params': { 'user': user, 'record': record }, 'query': request.params, 'data': await request.get_media(), }, ensure_ascii=False, separators=(',', ':')) app = App() # first add ten more routes to load routing system # ------------------------------------------------ class req_ok: async def on_get(self, *args, **kwargs): pass for n in range(5): app.add_route(f"/route-{n}", req_ok()) app.add_route(f"/route-dyn-{n}/{{part}}", req_ok()) # then prepare endpoints for the benchmark # ---------------------------------------- app.add_route('/html', html()) app.add_route('/upload', upload()) app.add_route('/api/users/{user:int}/records/{record:int}', api())
async def test_echo(): # noqa: C901 consumer_sleep = 0.01 producer_loop = 10 producer_sleep_factor = consumer_sleep / (producer_loop / 2) class Resource: def __init__(self): self.caught_operation_not_allowed = False async def on_websocket(self, req, ws, p1, p2, injected): # NOTE(kgriffs): Normally the receiver task is not started # until the websocket is started. But here we start it # first in order to simulate a potential race condition # that ASGIWebSocketSimulator._emit() guards against, # in case it ever arises due to the way the target ASGI # app may be implemented. ws._buffered_receiver.start() await asyncio.sleep(0) if ws.unaccepted: await ws.accept() try: await ws.accept() except falcon.OperationNotAllowed: self.caught_operation_not_allowed = True if ws.ready: await ws.send_text( f'{p1}:{p2}:{req.context.message}:{injected}') messages = deque() sink_task = falcon.create_task(self._sink(ws, messages)) while not sink_task.done(): if not messages: await asyncio.sleep(0) continue try: await ws.send_text(messages.popleft()) except falcon.WebSocketDisconnected: break sink_task.cancel() try: await sink_task except asyncio.CancelledError: pass async def _sink(self, ws, messages): while True: # NOTE(kgriffs): Throttle slightly to allow messages to # fill up the buffer. await asyncio.sleep(consumer_sleep) try: message = await ws.receive_text() except falcon.WebSocketDisconnected: break if message != 'ignore': messages.append(message) class MiddlewareA: async def process_resource_ws(self, req, ws, resource, params): assert isinstance(resource, Resource) assert isinstance(ws, falcon.asgi.WebSocket) params['injected'] = '42' class MiddlewareB: async def process_request_ws(self, req, ws): assert isinstance(ws, falcon.asgi.WebSocket) req.context.message = 'hello' resource = Resource() # NOTE(kgriffs): The two methods are split across different middleware # classes so that we can test code paths that check for the existence # of one WebSocket middleware method vs the other, and also so that # we can make sure both the kwarg and the add_middlware() paths # succeed. app = App(middleware=MiddlewareA()) app.add_middleware(MiddlewareB()) app.add_route('/{p1}/{p2}', resource) async with testing.ASGIConductor(app) as c: async with c.simulate_ws('/v1/v2', headers={}) as ws: assert (await ws.receive_text()) == 'v1:v2:hello:42' for i in range(producer_loop): message = str( i ) if i else '' # Test round-tripping the empty string as well for i in range(100): await ws.send_text('ignore') # NOTE(kgriffs): For part of the time, cause the buffer on the # server side to fill up, and for the remainder of the time # for the buffer to be empty and wait on the client for # additional messages. await asyncio.sleep(i * producer_sleep_factor) await ws.send_text(message) assert (await ws.receive_text()) == message await ws.close() assert ws.closed assert ws.close_code == CloseCode.NORMAL assert resource.caught_operation_not_allowed
def test_sync_helpers(): safely_values = [] unsafely_values = [] shirley_values = [] class SomeResource: async def on_get(self, req, resp): safely_coroutine_objects = [] unsafely_coroutine_objects = [] shirley_coroutine_objects = [] def callme_safely(a, b, c=None): # NOTE(kgriffs): Sleep to prove that there isn't another # instance running in parallel that is able to race ahead. time.sleep(0.001) safely_values.append((a, b, c)) def callme_unsafely(a, b, c=None): time.sleep(0.01) # NOTE(vytas): Deliberately exaggerate a race condition here # in order to ensure a more deterministic test outcome. if a == 137: for _ in range(1000): if len(unsafely_values) > 137: break time.sleep(0.01) unsafely_values.append((a, b, c)) def callme_shirley(a=42, b=None): time.sleep(0.01) v = (a, b) shirley_values.append(v) # NOTE(kgriffs): Test that returning values works as expected return v # NOTE(kgriffs): Test setting threadsafe=True explicitly cmus = falcon.util.wrap_sync_to_async(callme_unsafely, threadsafe=True) cms = falcon.util.wrap_sync_to_async(callme_safely, threadsafe=False) loop = falcon.util.get_running_loop() # NOTE(kgriffs): create_task() is used here, so that the coroutines # are scheduled immediately in the order created; under Python # 3.6, asyncio.gather() does not seem to always schedule # them in order, so we do it this way to make it predictable. for i in range(1000): safely_coroutine_objects.append( loop.create_task(cms(i, i + 1, c=i + 2)) ) unsafely_coroutine_objects.append( loop.create_task(cmus(i, i + 1, c=i + 2)) ) shirley_coroutine_objects.append( loop.create_task(falcon.util.sync_to_async(callme_shirley, 24, b=i)) ) await asyncio.gather( *( safely_coroutine_objects + unsafely_coroutine_objects + shirley_coroutine_objects ) ) assert (42, None) == await falcon.util.sync_to_async(callme_shirley) assert (1, 2) == await falcon.util.sync_to_async(callme_shirley, 1, 2) assert (3, 4) == await falcon.util.sync_to_async(callme_shirley, 3, b=4) assert (5, None) == await falcon.util.wrap_sync_to_async(callme_shirley)(5) assert (42, 6) == await falcon.util.wrap_sync_to_async( callme_shirley, threadsafe=True)(b=6) with pytest.raises(TypeError): await falcon.util.sync_to_async(callme_shirley, -1, bogus=-1) resource = SomeResource() app = App() app.add_route('/', resource) client = testing.TestClient(app) result = client.simulate_get() assert result.status_code == 200 assert len(safely_values) == 1000 for i, val in enumerate(safely_values): assert val == (i, i + 1, i + 2) assert len(unsafely_values) == 1000 assert any( val != (i, i + 1, i + 2) for i, val in enumerate(unsafely_values) ) for i, val in enumerate(shirley_values): assert val[0] in {24, 42, 1, 5, 3} assert val[1] is None or (0 <= val[1] < 1000)