starlette/tests/test_websockets.py

635 lines
24 KiB
Python
Raw Normal View History

import sys
from typing import Any, Callable, MutableMapping
anyio integration (#1157) * First whack at anyio integration * Fix formatting * Remove debug messages * mypy fixes * Update README.md Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com> * Fix install_requires typo * move_on_after blocks if deadline is too small * Linter fixes * Improve WSGI structured concurrency * Tests use anyio * Checkin progress on testclient * Prep for anyio 3 * Remove debug backend option * Use anyio 3.0.0rc1 * Remove old style executor from GraphQLApp * Fix extra import * Don't cancel task scope early * Wait for wsgi sender to finish before exiting * Use memory object streams in websocket tests * Test on asyncio, asyncio+uvloop, and trio * Formatting fixes * run_until_first_complete doesn't need a return * Fix middleware app call * Simplify middleware exceptions * Use anyio for websocket test * Set STARLETTE_TESTCLIENT_ASYNC_BACKEND in tests * Pass async backend to portal * Formatting fixes * Bump anyio * Cleanup portals and add TestClient.async_backend * Use anyio.run_async_from_thread to send from worker thread * Use websocket_connect as context manager * Document changes in TestClient * Formatting fix * Fix websocket raises coverage * Update to anyio 3.0.0rc3 and replace aiofiles * Apply suggestions from code review Co-authored-by: Alex Grönholm <alex.gronholm@nextday.fi> * Bump to require anyio 3.0.0 final * Remove mention of aiofiles in README.md * Pin jinja2 to releases before 3 due to DeprecationWarnings * Add task_group as application attribute * Remove run_until_first_complete * Undo jinja pin * Refactor anyio.sleep into an event * Use one less task in test_websocket_concurrency_pattern * Apply review suggestions * Rename argument * fix start_task_soon type * fix BaseHTTPMiddleware when used without Starlette * Testclient receive() is a non-trapping function if the response is already complete This allows for a zero deadline when waiting for a disconnect message * Use variable annotation for async_backend * Update docs regarding dependency on anyio * Use CancelScope instead of move_on_after in request.is_disconnected * Cancel task group after returning middleware response Add test for https://github.com/encode/starlette/issues/1022 * Add link to anyio backend options in testclient docs * Add types-dataclasses * Re-implement starlette.concurrency.run_until_first_complete and add a test * Fix type on handler callable * Apply review comments to clarify run_until_first_complete scope Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com> Co-authored-by: Alex Grönholm <alex.gronholm@nextday.fi> Co-authored-by: Thomas Grainger <tagrain@gmail.com>
2021-06-18 14:48:43 +00:00
import anyio
import pytest
from anyio.abc import ObjectReceiveStream, ObjectSendStream
from starlette import status
Support the WebSocket Denial Response ASGI extension (#2041) * supply asgi_extensions to TestClient * Add WebSocket.send_response() * Add response support for WebSocket testclient * fix test for filesystem line-endings * lintint * support websocket.http.response extension by default * Improve coverate * Apply suggestions from code review Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com> * Undo unrelated change * fix incorrect error message * Update starlette/websockets.py Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com> * formatting * Re-introduce close-code and close-reason to WebSocketReject * Make sure the "websocket.connect" message is received in tests * Deliver a websocket.disconnect message to the app even if it closes/rejects itself. * Add test for filling out missing `websocket.disconnect` code * Add rejection headers. Expand tests. * Fix types, headers in message are `bytes` tuples. * Minimal WebSocket Denial Response implementation * Revert "Minimal WebSocket Denial Response implementation" This reverts commit 7af10ddcfa5423c18953cf5d1317cb5aa30a014c. * Rename to send_denial_response and update documentation * Remove the app_disconnect_msg. This can be added later in a separate PR * Remove status code 1005 from this PR * Assume that the application has tested for the extension before sending websocket.http.response.start * Rename WebSocketReject to WebSocketDenialResponse * Remove code and status from WebSocketDenialResponse. Just send a regular WebSocketDisconnect even when connection is rejected with close() * Raise an exception if attempting to send a http response and server does not support it. * WebSocketDenialClose and WebSocketDenialResponse These are both instances of WebSocketDenial. * Update starlette/testclient.py Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com> * Revert "WebSocketDenialClose and WebSocketDenialResponse" This reverts commit 71b76e3f1c87064fe8458ff9d4ad0b242cbf15e7. * Rename parameters, member variables * Use httpx.Response as the base for WebSocketDenialResponse. * Apply suggestions from code review Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com> * Update sanity check message * Remove un-needed function * Expand error message test regex * Add type hings to test methods * Add doc string to test. * Fix mypy complaining about mismatching parent methods. * nitpick & remove test * Simplify the documentation * Update starlette/testclient.py * Update starlette/testclient.py * Remove an unnecessary test * there is no special "close because of rejection" in the testclient anymore. --------- Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
2024-02-04 20:16:10 +00:00
from starlette.responses import Response
from starlette.testclient import TestClient, WebSocketDenialResponse
from starlette.types import Message, Receive, Scope, Send
from starlette.websockets import WebSocket, WebSocketDisconnect, WebSocketState
TestClientFactory = Callable[..., TestClient]
def test_websocket_url(test_client_factory: TestClientFactory) -> None:
async def app(scope: Scope, receive: Receive, send: Send) -> None:
websocket = WebSocket(scope, receive=receive, send=send)
await websocket.accept()
await websocket.send_json({"url": str(websocket.url)})
await websocket.close()
client = test_client_factory(app)
with client.websocket_connect("/123?a=abc") as websocket:
data = websocket.receive_json()
assert data == {"url": "ws://testserver/123?a=abc"}
def test_websocket_binary_json(test_client_factory: TestClientFactory) -> None:
async def app(scope: Scope, receive: Receive, send: Send) -> None:
websocket = WebSocket(scope, receive=receive, send=send)
await websocket.accept()
message = await websocket.receive_json(mode="binary")
await websocket.send_json(message, mode="binary")
await websocket.close()
client = test_client_factory(app)
with client.websocket_connect("/123?a=abc") as websocket:
websocket.send_json({"test": "data"}, mode="binary")
data = websocket.receive_json(mode="binary")
assert data == {"test": "data"}
def test_websocket_ensure_unicode_on_send_json(
test_client_factory: TestClientFactory,
) -> None:
async def app(scope: Scope, receive: Receive, send: Send) -> None:
websocket = WebSocket(scope, receive=receive, send=send)
await websocket.accept()
message = await websocket.receive_json(mode="text")
await websocket.send_json(message, mode="text")
await websocket.close()
client = test_client_factory(app)
with client.websocket_connect("/123?a=abc") as websocket:
websocket.send_json({"test": "数据"}, mode="text")
data = websocket.receive_text()
assert data == '{"test":"数据"}'
def test_websocket_query_params(test_client_factory: TestClientFactory) -> None:
async def app(scope: Scope, receive: Receive, send: Send) -> None:
websocket = WebSocket(scope, receive=receive, send=send)
query_params = dict(websocket.query_params)
await websocket.accept()
await websocket.send_json({"params": query_params})
await websocket.close()
client = test_client_factory(app)
with client.websocket_connect("/?a=abc&b=456") as websocket:
data = websocket.receive_json()
assert data == {"params": {"a": "abc", "b": "456"}}
@pytest.mark.skipif(
any(module in sys.modules for module in ("brotli", "brotlicffi")),
reason='urllib3 includes "br" to the "accept-encoding" headers.',
)
def test_websocket_headers(test_client_factory: TestClientFactory) -> None:
async def app(scope: Scope, receive: Receive, send: Send) -> None:
websocket = WebSocket(scope, receive=receive, send=send)
headers = dict(websocket.headers)
await websocket.accept()
await websocket.send_json({"headers": headers})
await websocket.close()
client = test_client_factory(app)
with client.websocket_connect("/") as websocket:
expected_headers = {
"accept": "*/*",
"accept-encoding": "gzip, deflate",
"connection": "upgrade",
"host": "testserver",
"user-agent": "testclient",
"sec-websocket-key": "testserver==",
"sec-websocket-version": "13",
}
data = websocket.receive_json()
assert data == {"headers": expected_headers}
def test_websocket_port(test_client_factory: TestClientFactory) -> None:
async def app(scope: Scope, receive: Receive, send: Send) -> None:
websocket = WebSocket(scope, receive=receive, send=send)
await websocket.accept()
await websocket.send_json({"port": websocket.url.port})
await websocket.close()
client = test_client_factory(app)
with client.websocket_connect("ws://example.com:123/123?a=abc") as websocket:
data = websocket.receive_json()
assert data == {"port": 123}
def test_websocket_send_and_receive_text(
test_client_factory: TestClientFactory,
) -> None:
async def app(scope: Scope, receive: Receive, send: Send) -> None:
websocket = WebSocket(scope, receive=receive, send=send)
await websocket.accept()
data = await websocket.receive_text()
await websocket.send_text("Message was: " + data)
await websocket.close()
client = test_client_factory(app)
with client.websocket_connect("/") as websocket:
websocket.send_text("Hello, world!")
data = websocket.receive_text()
assert data == "Message was: Hello, world!"
def test_websocket_send_and_receive_bytes(
test_client_factory: TestClientFactory,
) -> None:
async def app(scope: Scope, receive: Receive, send: Send) -> None:
websocket = WebSocket(scope, receive=receive, send=send)
await websocket.accept()
data = await websocket.receive_bytes()
await websocket.send_bytes(b"Message was: " + data)
await websocket.close()
client = test_client_factory(app)
with client.websocket_connect("/") as websocket:
websocket.send_bytes(b"Hello, world!")
data = websocket.receive_bytes()
assert data == b"Message was: Hello, world!"
def test_websocket_send_and_receive_json(
test_client_factory: TestClientFactory,
) -> None:
async def app(scope: Scope, receive: Receive, send: Send) -> None:
websocket = WebSocket(scope, receive=receive, send=send)
await websocket.accept()
data = await websocket.receive_json()
await websocket.send_json({"message": data})
await websocket.close()
client = test_client_factory(app)
with client.websocket_connect("/") as websocket:
websocket.send_json({"hello": "world"})
data = websocket.receive_json()
assert data == {"message": {"hello": "world"}}
def test_websocket_iter_text(test_client_factory: TestClientFactory) -> None:
async def app(scope: Scope, receive: Receive, send: Send) -> None:
websocket = WebSocket(scope, receive=receive, send=send)
await websocket.accept()
async for data in websocket.iter_text():
await websocket.send_text("Message was: " + data)
client = test_client_factory(app)
with client.websocket_connect("/") as websocket:
websocket.send_text("Hello, world!")
data = websocket.receive_text()
assert data == "Message was: Hello, world!"
def test_websocket_iter_bytes(test_client_factory: TestClientFactory) -> None:
async def app(scope: Scope, receive: Receive, send: Send) -> None:
websocket = WebSocket(scope, receive=receive, send=send)
await websocket.accept()
async for data in websocket.iter_bytes():
await websocket.send_bytes(b"Message was: " + data)
client = test_client_factory(app)
with client.websocket_connect("/") as websocket:
websocket.send_bytes(b"Hello, world!")
data = websocket.receive_bytes()
assert data == b"Message was: Hello, world!"
def test_websocket_iter_json(test_client_factory: TestClientFactory) -> None:
async def app(scope: Scope, receive: Receive, send: Send) -> None:
websocket = WebSocket(scope, receive=receive, send=send)
await websocket.accept()
async for data in websocket.iter_json():
await websocket.send_json({"message": data})
client = test_client_factory(app)
with client.websocket_connect("/") as websocket:
websocket.send_json({"hello": "world"})
data = websocket.receive_json()
assert data == {"message": {"hello": "world"}}
def test_websocket_concurrency_pattern(test_client_factory: TestClientFactory) -> None:
stream_send: ObjectSendStream[MutableMapping[str, Any]]
stream_receive: ObjectReceiveStream[MutableMapping[str, Any]]
stream_send, stream_receive = anyio.create_memory_object_stream()
anyio integration (#1157) * First whack at anyio integration * Fix formatting * Remove debug messages * mypy fixes * Update README.md Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com> * Fix install_requires typo * move_on_after blocks if deadline is too small * Linter fixes * Improve WSGI structured concurrency * Tests use anyio * Checkin progress on testclient * Prep for anyio 3 * Remove debug backend option * Use anyio 3.0.0rc1 * Remove old style executor from GraphQLApp * Fix extra import * Don't cancel task scope early * Wait for wsgi sender to finish before exiting * Use memory object streams in websocket tests * Test on asyncio, asyncio+uvloop, and trio * Formatting fixes * run_until_first_complete doesn't need a return * Fix middleware app call * Simplify middleware exceptions * Use anyio for websocket test * Set STARLETTE_TESTCLIENT_ASYNC_BACKEND in tests * Pass async backend to portal * Formatting fixes * Bump anyio * Cleanup portals and add TestClient.async_backend * Use anyio.run_async_from_thread to send from worker thread * Use websocket_connect as context manager * Document changes in TestClient * Formatting fix * Fix websocket raises coverage * Update to anyio 3.0.0rc3 and replace aiofiles * Apply suggestions from code review Co-authored-by: Alex Grönholm <alex.gronholm@nextday.fi> * Bump to require anyio 3.0.0 final * Remove mention of aiofiles in README.md * Pin jinja2 to releases before 3 due to DeprecationWarnings * Add task_group as application attribute * Remove run_until_first_complete * Undo jinja pin * Refactor anyio.sleep into an event * Use one less task in test_websocket_concurrency_pattern * Apply review suggestions * Rename argument * fix start_task_soon type * fix BaseHTTPMiddleware when used without Starlette * Testclient receive() is a non-trapping function if the response is already complete This allows for a zero deadline when waiting for a disconnect message * Use variable annotation for async_backend * Update docs regarding dependency on anyio * Use CancelScope instead of move_on_after in request.is_disconnected * Cancel task group after returning middleware response Add test for https://github.com/encode/starlette/issues/1022 * Add link to anyio backend options in testclient docs * Add types-dataclasses * Re-implement starlette.concurrency.run_until_first_complete and add a test * Fix type on handler callable * Apply review comments to clarify run_until_first_complete scope Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com> Co-authored-by: Alex Grönholm <alex.gronholm@nextday.fi> Co-authored-by: Thomas Grainger <tagrain@gmail.com>
2021-06-18 14:48:43 +00:00
async def reader(websocket: WebSocket) -> None:
async with stream_send:
async for data in websocket.iter_json():
await stream_send.send(data)
async def writer(websocket: WebSocket) -> None:
async with stream_receive:
async for message in stream_receive:
await websocket.send_json(message)
async def app(scope: Scope, receive: Receive, send: Send) -> None:
websocket = WebSocket(scope, receive=receive, send=send)
await websocket.accept()
async with anyio.create_task_group() as task_group:
task_group.start_soon(reader, websocket)
await writer(websocket)
await websocket.close()
client = test_client_factory(app)
with client.websocket_connect("/") as websocket:
websocket.send_json({"hello": "world"})
data = websocket.receive_json()
assert data == {"hello": "world"}
def test_client_close(test_client_factory: TestClientFactory) -> None:
close_code = None
close_reason = None
async def app(scope: Scope, receive: Receive, send: Send) -> None:
nonlocal close_code, close_reason
websocket = WebSocket(scope, receive=receive, send=send)
await websocket.accept()
try:
await websocket.receive_text()
except WebSocketDisconnect as exc:
close_code = exc.code
close_reason = exc.reason
client = test_client_factory(app)
with client.websocket_connect("/") as websocket:
websocket.close(code=status.WS_1001_GOING_AWAY, reason="Going Away")
2018-10-18 07:47:04 +00:00
assert close_code == status.WS_1001_GOING_AWAY
assert close_reason == "Going Away"
@pytest.mark.anyio
async def test_client_disconnect_on_send() -> None:
async def app(scope: Scope, receive: Receive, send: Send) -> None:
websocket = WebSocket(scope, receive=receive, send=send)
await websocket.accept()
await websocket.send_text("Hello, world!")
async def receive() -> Message:
return {"type": "websocket.connect"}
async def send(message: Message) -> None:
if message["type"] == "websocket.accept":
return
# Simulate the exception the server would send to the application when the
# client disconnects.
raise OSError
with pytest.raises(WebSocketDisconnect) as ctx:
await app({"type": "websocket", "path": "/"}, receive, send)
assert ctx.value.code == status.WS_1006_ABNORMAL_CLOSURE
def test_application_close(test_client_factory: TestClientFactory) -> None:
async def app(scope: Scope, receive: Receive, send: Send) -> None:
websocket = WebSocket(scope, receive=receive, send=send)
await websocket.accept()
await websocket.close(status.WS_1001_GOING_AWAY)
client = test_client_factory(app)
with client.websocket_connect("/") as websocket:
with pytest.raises(WebSocketDisconnect) as exc:
websocket.receive_text()
2018-10-18 07:47:04 +00:00
assert exc.value.code == status.WS_1001_GOING_AWAY
def test_rejected_connection(test_client_factory: TestClientFactory) -> None:
async def app(scope: Scope, receive: Receive, send: Send) -> None:
websocket = WebSocket(scope, receive=receive, send=send)
Support the WebSocket Denial Response ASGI extension (#2041) * supply asgi_extensions to TestClient * Add WebSocket.send_response() * Add response support for WebSocket testclient * fix test for filesystem line-endings * lintint * support websocket.http.response extension by default * Improve coverate * Apply suggestions from code review Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com> * Undo unrelated change * fix incorrect error message * Update starlette/websockets.py Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com> * formatting * Re-introduce close-code and close-reason to WebSocketReject * Make sure the "websocket.connect" message is received in tests * Deliver a websocket.disconnect message to the app even if it closes/rejects itself. * Add test for filling out missing `websocket.disconnect` code * Add rejection headers. Expand tests. * Fix types, headers in message are `bytes` tuples. * Minimal WebSocket Denial Response implementation * Revert "Minimal WebSocket Denial Response implementation" This reverts commit 7af10ddcfa5423c18953cf5d1317cb5aa30a014c. * Rename to send_denial_response and update documentation * Remove the app_disconnect_msg. This can be added later in a separate PR * Remove status code 1005 from this PR * Assume that the application has tested for the extension before sending websocket.http.response.start * Rename WebSocketReject to WebSocketDenialResponse * Remove code and status from WebSocketDenialResponse. Just send a regular WebSocketDisconnect even when connection is rejected with close() * Raise an exception if attempting to send a http response and server does not support it. * WebSocketDenialClose and WebSocketDenialResponse These are both instances of WebSocketDenial. * Update starlette/testclient.py Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com> * Revert "WebSocketDenialClose and WebSocketDenialResponse" This reverts commit 71b76e3f1c87064fe8458ff9d4ad0b242cbf15e7. * Rename parameters, member variables * Use httpx.Response as the base for WebSocketDenialResponse. * Apply suggestions from code review Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com> * Update sanity check message * Remove un-needed function * Expand error message test regex * Add type hings to test methods * Add doc string to test. * Fix mypy complaining about mismatching parent methods. * nitpick & remove test * Simplify the documentation * Update starlette/testclient.py * Update starlette/testclient.py * Remove an unnecessary test * there is no special "close because of rejection" in the testclient anymore. --------- Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
2024-02-04 20:16:10 +00:00
msg = await websocket.receive()
assert msg == {"type": "websocket.connect"}
await websocket.close(status.WS_1001_GOING_AWAY)
client = test_client_factory(app)
with pytest.raises(WebSocketDisconnect) as exc:
anyio integration (#1157) * First whack at anyio integration * Fix formatting * Remove debug messages * mypy fixes * Update README.md Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com> * Fix install_requires typo * move_on_after blocks if deadline is too small * Linter fixes * Improve WSGI structured concurrency * Tests use anyio * Checkin progress on testclient * Prep for anyio 3 * Remove debug backend option * Use anyio 3.0.0rc1 * Remove old style executor from GraphQLApp * Fix extra import * Don't cancel task scope early * Wait for wsgi sender to finish before exiting * Use memory object streams in websocket tests * Test on asyncio, asyncio+uvloop, and trio * Formatting fixes * run_until_first_complete doesn't need a return * Fix middleware app call * Simplify middleware exceptions * Use anyio for websocket test * Set STARLETTE_TESTCLIENT_ASYNC_BACKEND in tests * Pass async backend to portal * Formatting fixes * Bump anyio * Cleanup portals and add TestClient.async_backend * Use anyio.run_async_from_thread to send from worker thread * Use websocket_connect as context manager * Document changes in TestClient * Formatting fix * Fix websocket raises coverage * Update to anyio 3.0.0rc3 and replace aiofiles * Apply suggestions from code review Co-authored-by: Alex Grönholm <alex.gronholm@nextday.fi> * Bump to require anyio 3.0.0 final * Remove mention of aiofiles in README.md * Pin jinja2 to releases before 3 due to DeprecationWarnings * Add task_group as application attribute * Remove run_until_first_complete * Undo jinja pin * Refactor anyio.sleep into an event * Use one less task in test_websocket_concurrency_pattern * Apply review suggestions * Rename argument * fix start_task_soon type * fix BaseHTTPMiddleware when used without Starlette * Testclient receive() is a non-trapping function if the response is already complete This allows for a zero deadline when waiting for a disconnect message * Use variable annotation for async_backend * Update docs regarding dependency on anyio * Use CancelScope instead of move_on_after in request.is_disconnected * Cancel task group after returning middleware response Add test for https://github.com/encode/starlette/issues/1022 * Add link to anyio backend options in testclient docs * Add types-dataclasses * Re-implement starlette.concurrency.run_until_first_complete and add a test * Fix type on handler callable * Apply review comments to clarify run_until_first_complete scope Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com> Co-authored-by: Alex Grönholm <alex.gronholm@nextday.fi> Co-authored-by: Thomas Grainger <tagrain@gmail.com>
2021-06-18 14:48:43 +00:00
with client.websocket_connect("/"):
pass # pragma: no cover
2018-10-18 07:47:04 +00:00
assert exc.value.code == status.WS_1001_GOING_AWAY
def test_send_denial_response(test_client_factory: TestClientFactory) -> None:
Support the WebSocket Denial Response ASGI extension (#2041) * supply asgi_extensions to TestClient * Add WebSocket.send_response() * Add response support for WebSocket testclient * fix test for filesystem line-endings * lintint * support websocket.http.response extension by default * Improve coverate * Apply suggestions from code review Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com> * Undo unrelated change * fix incorrect error message * Update starlette/websockets.py Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com> * formatting * Re-introduce close-code and close-reason to WebSocketReject * Make sure the "websocket.connect" message is received in tests * Deliver a websocket.disconnect message to the app even if it closes/rejects itself. * Add test for filling out missing `websocket.disconnect` code * Add rejection headers. Expand tests. * Fix types, headers in message are `bytes` tuples. * Minimal WebSocket Denial Response implementation * Revert "Minimal WebSocket Denial Response implementation" This reverts commit 7af10ddcfa5423c18953cf5d1317cb5aa30a014c. * Rename to send_denial_response and update documentation * Remove the app_disconnect_msg. This can be added later in a separate PR * Remove status code 1005 from this PR * Assume that the application has tested for the extension before sending websocket.http.response.start * Rename WebSocketReject to WebSocketDenialResponse * Remove code and status from WebSocketDenialResponse. Just send a regular WebSocketDisconnect even when connection is rejected with close() * Raise an exception if attempting to send a http response and server does not support it. * WebSocketDenialClose and WebSocketDenialResponse These are both instances of WebSocketDenial. * Update starlette/testclient.py Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com> * Revert "WebSocketDenialClose and WebSocketDenialResponse" This reverts commit 71b76e3f1c87064fe8458ff9d4ad0b242cbf15e7. * Rename parameters, member variables * Use httpx.Response as the base for WebSocketDenialResponse. * Apply suggestions from code review Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com> * Update sanity check message * Remove un-needed function * Expand error message test regex * Add type hings to test methods * Add doc string to test. * Fix mypy complaining about mismatching parent methods. * nitpick & remove test * Simplify the documentation * Update starlette/testclient.py * Update starlette/testclient.py * Remove an unnecessary test * there is no special "close because of rejection" in the testclient anymore. --------- Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
2024-02-04 20:16:10 +00:00
async def app(scope: Scope, receive: Receive, send: Send) -> None:
websocket = WebSocket(scope, receive=receive, send=send)
msg = await websocket.receive()
assert msg == {"type": "websocket.connect"}
response = Response(status_code=404, content="foo")
await websocket.send_denial_response(response)
client = test_client_factory(app)
with pytest.raises(WebSocketDenialResponse) as exc:
with client.websocket_connect("/"):
pass # pragma: no cover
assert exc.value.status_code == 404
assert exc.value.content == b"foo"
def test_send_response_multi(test_client_factory: TestClientFactory) -> None:
Support the WebSocket Denial Response ASGI extension (#2041) * supply asgi_extensions to TestClient * Add WebSocket.send_response() * Add response support for WebSocket testclient * fix test for filesystem line-endings * lintint * support websocket.http.response extension by default * Improve coverate * Apply suggestions from code review Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com> * Undo unrelated change * fix incorrect error message * Update starlette/websockets.py Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com> * formatting * Re-introduce close-code and close-reason to WebSocketReject * Make sure the "websocket.connect" message is received in tests * Deliver a websocket.disconnect message to the app even if it closes/rejects itself. * Add test for filling out missing `websocket.disconnect` code * Add rejection headers. Expand tests. * Fix types, headers in message are `bytes` tuples. * Minimal WebSocket Denial Response implementation * Revert "Minimal WebSocket Denial Response implementation" This reverts commit 7af10ddcfa5423c18953cf5d1317cb5aa30a014c. * Rename to send_denial_response and update documentation * Remove the app_disconnect_msg. This can be added later in a separate PR * Remove status code 1005 from this PR * Assume that the application has tested for the extension before sending websocket.http.response.start * Rename WebSocketReject to WebSocketDenialResponse * Remove code and status from WebSocketDenialResponse. Just send a regular WebSocketDisconnect even when connection is rejected with close() * Raise an exception if attempting to send a http response and server does not support it. * WebSocketDenialClose and WebSocketDenialResponse These are both instances of WebSocketDenial. * Update starlette/testclient.py Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com> * Revert "WebSocketDenialClose and WebSocketDenialResponse" This reverts commit 71b76e3f1c87064fe8458ff9d4ad0b242cbf15e7. * Rename parameters, member variables * Use httpx.Response as the base for WebSocketDenialResponse. * Apply suggestions from code review Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com> * Update sanity check message * Remove un-needed function * Expand error message test regex * Add type hings to test methods * Add doc string to test. * Fix mypy complaining about mismatching parent methods. * nitpick & remove test * Simplify the documentation * Update starlette/testclient.py * Update starlette/testclient.py * Remove an unnecessary test * there is no special "close because of rejection" in the testclient anymore. --------- Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
2024-02-04 20:16:10 +00:00
async def app(scope: Scope, receive: Receive, send: Send) -> None:
websocket = WebSocket(scope, receive=receive, send=send)
msg = await websocket.receive()
assert msg == {"type": "websocket.connect"}
await websocket.send(
{
"type": "websocket.http.response.start",
"status": 404,
"headers": [(b"content-type", b"text/plain"), (b"foo", b"bar")],
}
)
await websocket.send(
{
"type": "websocket.http.response.body",
"body": b"hard",
"more_body": True,
}
)
await websocket.send(
{
"type": "websocket.http.response.body",
"body": b"body",
}
)
client = test_client_factory(app)
with pytest.raises(WebSocketDenialResponse) as exc:
with client.websocket_connect("/"):
pass # pragma: no cover
assert exc.value.status_code == 404
assert exc.value.content == b"hardbody"
assert exc.value.headers["foo"] == "bar"
def test_send_response_unsupported(test_client_factory: TestClientFactory) -> None:
Support the WebSocket Denial Response ASGI extension (#2041) * supply asgi_extensions to TestClient * Add WebSocket.send_response() * Add response support for WebSocket testclient * fix test for filesystem line-endings * lintint * support websocket.http.response extension by default * Improve coverate * Apply suggestions from code review Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com> * Undo unrelated change * fix incorrect error message * Update starlette/websockets.py Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com> * formatting * Re-introduce close-code and close-reason to WebSocketReject * Make sure the "websocket.connect" message is received in tests * Deliver a websocket.disconnect message to the app even if it closes/rejects itself. * Add test for filling out missing `websocket.disconnect` code * Add rejection headers. Expand tests. * Fix types, headers in message are `bytes` tuples. * Minimal WebSocket Denial Response implementation * Revert "Minimal WebSocket Denial Response implementation" This reverts commit 7af10ddcfa5423c18953cf5d1317cb5aa30a014c. * Rename to send_denial_response and update documentation * Remove the app_disconnect_msg. This can be added later in a separate PR * Remove status code 1005 from this PR * Assume that the application has tested for the extension before sending websocket.http.response.start * Rename WebSocketReject to WebSocketDenialResponse * Remove code and status from WebSocketDenialResponse. Just send a regular WebSocketDisconnect even when connection is rejected with close() * Raise an exception if attempting to send a http response and server does not support it. * WebSocketDenialClose and WebSocketDenialResponse These are both instances of WebSocketDenial. * Update starlette/testclient.py Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com> * Revert "WebSocketDenialClose and WebSocketDenialResponse" This reverts commit 71b76e3f1c87064fe8458ff9d4ad0b242cbf15e7. * Rename parameters, member variables * Use httpx.Response as the base for WebSocketDenialResponse. * Apply suggestions from code review Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com> * Update sanity check message * Remove un-needed function * Expand error message test regex * Add type hings to test methods * Add doc string to test. * Fix mypy complaining about mismatching parent methods. * nitpick & remove test * Simplify the documentation * Update starlette/testclient.py * Update starlette/testclient.py * Remove an unnecessary test * there is no special "close because of rejection" in the testclient anymore. --------- Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
2024-02-04 20:16:10 +00:00
async def app(scope: Scope, receive: Receive, send: Send) -> None:
del scope["extensions"]["websocket.http.response"]
websocket = WebSocket(scope, receive=receive, send=send)
msg = await websocket.receive()
assert msg == {"type": "websocket.connect"}
response = Response(status_code=404, content="foo")
with pytest.raises(
RuntimeError,
match="The server doesn't support the Websocket Denial Response extension.",
):
await websocket.send_denial_response(response)
await websocket.close()
client = test_client_factory(app)
with pytest.raises(WebSocketDisconnect) as exc:
with client.websocket_connect("/"):
pass # pragma: no cover
assert exc.value.code == status.WS_1000_NORMAL_CLOSURE
def test_send_response_duplicate_start(test_client_factory: TestClientFactory) -> None:
Support the WebSocket Denial Response ASGI extension (#2041) * supply asgi_extensions to TestClient * Add WebSocket.send_response() * Add response support for WebSocket testclient * fix test for filesystem line-endings * lintint * support websocket.http.response extension by default * Improve coverate * Apply suggestions from code review Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com> * Undo unrelated change * fix incorrect error message * Update starlette/websockets.py Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com> * formatting * Re-introduce close-code and close-reason to WebSocketReject * Make sure the "websocket.connect" message is received in tests * Deliver a websocket.disconnect message to the app even if it closes/rejects itself. * Add test for filling out missing `websocket.disconnect` code * Add rejection headers. Expand tests. * Fix types, headers in message are `bytes` tuples. * Minimal WebSocket Denial Response implementation * Revert "Minimal WebSocket Denial Response implementation" This reverts commit 7af10ddcfa5423c18953cf5d1317cb5aa30a014c. * Rename to send_denial_response and update documentation * Remove the app_disconnect_msg. This can be added later in a separate PR * Remove status code 1005 from this PR * Assume that the application has tested for the extension before sending websocket.http.response.start * Rename WebSocketReject to WebSocketDenialResponse * Remove code and status from WebSocketDenialResponse. Just send a regular WebSocketDisconnect even when connection is rejected with close() * Raise an exception if attempting to send a http response and server does not support it. * WebSocketDenialClose and WebSocketDenialResponse These are both instances of WebSocketDenial. * Update starlette/testclient.py Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com> * Revert "WebSocketDenialClose and WebSocketDenialResponse" This reverts commit 71b76e3f1c87064fe8458ff9d4ad0b242cbf15e7. * Rename parameters, member variables * Use httpx.Response as the base for WebSocketDenialResponse. * Apply suggestions from code review Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com> * Update sanity check message * Remove un-needed function * Expand error message test regex * Add type hings to test methods * Add doc string to test. * Fix mypy complaining about mismatching parent methods. * nitpick & remove test * Simplify the documentation * Update starlette/testclient.py * Update starlette/testclient.py * Remove an unnecessary test * there is no special "close because of rejection" in the testclient anymore. --------- Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
2024-02-04 20:16:10 +00:00
async def app(scope: Scope, receive: Receive, send: Send) -> None:
websocket = WebSocket(scope, receive=receive, send=send)
msg = await websocket.receive()
assert msg == {"type": "websocket.connect"}
response = Response(status_code=404, content="foo")
await websocket.send(
{
"type": "websocket.http.response.start",
"status": response.status_code,
"headers": response.raw_headers,
}
)
await websocket.send(
{
"type": "websocket.http.response.start",
"status": response.status_code,
"headers": response.raw_headers,
}
)
client = test_client_factory(app)
with pytest.raises(
RuntimeError,
match=(
'Expected ASGI message "websocket.http.response.body", but got '
"'websocket.http.response.start'"
),
):
with client.websocket_connect("/"):
pass # pragma: no cover
def test_subprotocol(test_client_factory: TestClientFactory) -> None:
async def app(scope: Scope, receive: Receive, send: Send) -> None:
websocket = WebSocket(scope, receive=receive, send=send)
assert websocket["subprotocols"] == ["soap", "wamp"]
await websocket.accept(subprotocol="wamp")
await websocket.close()
client = test_client_factory(app)
with client.websocket_connect("/", subprotocols=["soap", "wamp"]) as websocket:
assert websocket.accepted_subprotocol == "wamp"
def test_additional_headers(test_client_factory: TestClientFactory) -> None:
async def app(scope: Scope, receive: Receive, send: Send) -> None:
websocket = WebSocket(scope, receive=receive, send=send)
await websocket.accept(headers=[(b"additional", b"header")])
await websocket.close()
client = test_client_factory(app)
with client.websocket_connect("/") as websocket:
assert websocket.extra_headers == [(b"additional", b"header")]
def test_no_additional_headers(test_client_factory: TestClientFactory) -> None:
async def app(scope: Scope, receive: Receive, send: Send) -> None:
websocket = WebSocket(scope, receive=receive, send=send)
await websocket.accept()
await websocket.close()
client = test_client_factory(app)
with client.websocket_connect("/") as websocket:
assert websocket.extra_headers == []
def test_websocket_exception(test_client_factory: TestClientFactory) -> None:
async def app(scope: Scope, receive: Receive, send: Send) -> None:
assert False
client = test_client_factory(app)
with pytest.raises(AssertionError):
anyio integration (#1157) * First whack at anyio integration * Fix formatting * Remove debug messages * mypy fixes * Update README.md Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com> * Fix install_requires typo * move_on_after blocks if deadline is too small * Linter fixes * Improve WSGI structured concurrency * Tests use anyio * Checkin progress on testclient * Prep for anyio 3 * Remove debug backend option * Use anyio 3.0.0rc1 * Remove old style executor from GraphQLApp * Fix extra import * Don't cancel task scope early * Wait for wsgi sender to finish before exiting * Use memory object streams in websocket tests * Test on asyncio, asyncio+uvloop, and trio * Formatting fixes * run_until_first_complete doesn't need a return * Fix middleware app call * Simplify middleware exceptions * Use anyio for websocket test * Set STARLETTE_TESTCLIENT_ASYNC_BACKEND in tests * Pass async backend to portal * Formatting fixes * Bump anyio * Cleanup portals and add TestClient.async_backend * Use anyio.run_async_from_thread to send from worker thread * Use websocket_connect as context manager * Document changes in TestClient * Formatting fix * Fix websocket raises coverage * Update to anyio 3.0.0rc3 and replace aiofiles * Apply suggestions from code review Co-authored-by: Alex Grönholm <alex.gronholm@nextday.fi> * Bump to require anyio 3.0.0 final * Remove mention of aiofiles in README.md * Pin jinja2 to releases before 3 due to DeprecationWarnings * Add task_group as application attribute * Remove run_until_first_complete * Undo jinja pin * Refactor anyio.sleep into an event * Use one less task in test_websocket_concurrency_pattern * Apply review suggestions * Rename argument * fix start_task_soon type * fix BaseHTTPMiddleware when used without Starlette * Testclient receive() is a non-trapping function if the response is already complete This allows for a zero deadline when waiting for a disconnect message * Use variable annotation for async_backend * Update docs regarding dependency on anyio * Use CancelScope instead of move_on_after in request.is_disconnected * Cancel task group after returning middleware response Add test for https://github.com/encode/starlette/issues/1022 * Add link to anyio backend options in testclient docs * Add types-dataclasses * Re-implement starlette.concurrency.run_until_first_complete and add a test * Fix type on handler callable * Apply review comments to clarify run_until_first_complete scope Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com> Co-authored-by: Alex Grönholm <alex.gronholm@nextday.fi> Co-authored-by: Thomas Grainger <tagrain@gmail.com>
2021-06-18 14:48:43 +00:00
with client.websocket_connect("/123?a=abc"):
pass # pragma: no cover
def test_duplicate_close(test_client_factory: TestClientFactory) -> None:
async def app(scope: Scope, receive: Receive, send: Send) -> None:
websocket = WebSocket(scope, receive=receive, send=send)
await websocket.accept()
await websocket.close()
await websocket.close()
client = test_client_factory(app)
with pytest.raises(RuntimeError):
with client.websocket_connect("/"):
pass # pragma: no cover
def test_duplicate_disconnect(test_client_factory: TestClientFactory) -> None:
async def app(scope: Scope, receive: Receive, send: Send) -> None:
websocket = WebSocket(scope, receive=receive, send=send)
await websocket.accept()
message = await websocket.receive()
assert message["type"] == "websocket.disconnect"
message = await websocket.receive()
client = test_client_factory(app)
with pytest.raises(RuntimeError):
with client.websocket_connect("/") as websocket:
websocket.close()
def test_websocket_scope_interface() -> None:
"""
A WebSocket can be instantiated with a scope, and presents a `Mapping`
interface.
"""
async def mock_receive() -> Message: # type: ignore
... # pragma: no cover
2018-10-02 12:24:02 +00:00
async def mock_send(message: Message) -> None:
... # pragma: no cover
2018-10-02 12:24:02 +00:00
websocket = WebSocket(
{"type": "websocket", "path": "/abc/", "headers": []},
2018-10-02 12:24:02 +00:00
receive=mock_receive,
send=mock_send,
)
assert websocket["type"] == "websocket"
assert dict(websocket) == {"type": "websocket", "path": "/abc/", "headers": []}
assert len(websocket) == 3
# check __eq__ and __hash__
assert websocket != WebSocket(
{"type": "websocket", "path": "/abc/", "headers": []},
receive=mock_receive,
send=mock_send,
)
assert websocket == websocket
assert websocket in {websocket}
assert {websocket} == {websocket}
def test_websocket_close_reason(test_client_factory: TestClientFactory) -> None:
async def app(scope: Scope, receive: Receive, send: Send) -> None:
websocket = WebSocket(scope, receive=receive, send=send)
await websocket.accept()
await websocket.close(code=status.WS_1001_GOING_AWAY, reason="Going Away")
client = test_client_factory(app)
with client.websocket_connect("/") as websocket:
with pytest.raises(WebSocketDisconnect) as exc:
websocket.receive_text()
assert exc.value.code == status.WS_1001_GOING_AWAY
assert exc.value.reason == "Going Away"
def test_send_json_invalid_mode(test_client_factory: TestClientFactory) -> None:
async def app(scope: Scope, receive: Receive, send: Send) -> None:
websocket = WebSocket(scope, receive=receive, send=send)
await websocket.accept()
await websocket.send_json({}, mode="invalid")
client = test_client_factory(app)
with pytest.raises(RuntimeError):
with client.websocket_connect("/"):
pass # pragma: no cover
def test_receive_json_invalid_mode(test_client_factory: TestClientFactory) -> None:
async def app(scope: Scope, receive: Receive, send: Send) -> None:
websocket = WebSocket(scope, receive=receive, send=send)
await websocket.accept()
await websocket.receive_json(mode="invalid")
client = test_client_factory(app)
with pytest.raises(RuntimeError):
with client.websocket_connect("/"):
pass # pragma: nocover
def test_receive_text_before_accept(test_client_factory: TestClientFactory) -> None:
async def app(scope: Scope, receive: Receive, send: Send) -> None:
websocket = WebSocket(scope, receive=receive, send=send)
await websocket.receive_text()
client = test_client_factory(app)
with pytest.raises(RuntimeError):
with client.websocket_connect("/"):
pass # pragma: nocover
def test_receive_bytes_before_accept(test_client_factory: TestClientFactory) -> None:
async def app(scope: Scope, receive: Receive, send: Send) -> None:
websocket = WebSocket(scope, receive=receive, send=send)
await websocket.receive_bytes()
client = test_client_factory(app)
with pytest.raises(RuntimeError):
with client.websocket_connect("/"):
pass # pragma: nocover
def test_receive_json_before_accept(test_client_factory: TestClientFactory) -> None:
async def app(scope: Scope, receive: Receive, send: Send) -> None:
websocket = WebSocket(scope, receive=receive, send=send)
await websocket.receive_json()
client = test_client_factory(app)
with pytest.raises(RuntimeError):
with client.websocket_connect("/"):
pass # pragma: no cover
def test_send_before_accept(test_client_factory: TestClientFactory) -> None:
async def app(scope: Scope, receive: Receive, send: Send) -> None:
websocket = WebSocket(scope, receive=receive, send=send)
await websocket.send({"type": "websocket.send"})
client = test_client_factory(app)
with pytest.raises(RuntimeError):
with client.websocket_connect("/"):
pass # pragma: nocover
def test_send_wrong_message_type(test_client_factory: TestClientFactory) -> None:
async def app(scope: Scope, receive: Receive, send: Send) -> None:
websocket = WebSocket(scope, receive=receive, send=send)
await websocket.send({"type": "websocket.accept"})
await websocket.send({"type": "websocket.accept"})
client = test_client_factory(app)
with pytest.raises(RuntimeError):
with client.websocket_connect("/"):
pass # pragma: no cover
def test_receive_before_accept(test_client_factory: TestClientFactory) -> None:
async def app(scope: Scope, receive: Receive, send: Send) -> None:
websocket = WebSocket(scope, receive=receive, send=send)
await websocket.accept()
websocket.client_state = WebSocketState.CONNECTING
await websocket.receive()
client = test_client_factory(app)
with pytest.raises(RuntimeError):
with client.websocket_connect("/") as websocket:
websocket.send({"type": "websocket.send"})
def test_receive_wrong_message_type(test_client_factory: TestClientFactory) -> None:
async def app(scope: Scope, receive: Receive, send: Send) -> None:
websocket = WebSocket(scope, receive=receive, send=send)
await websocket.accept()
await websocket.receive()
client = test_client_factory(app)
with pytest.raises(RuntimeError):
with client.websocket_connect("/") as websocket:
websocket.send({"type": "websocket.connect"})