import pytest from starlette.responses import JSONResponse, PlainTextResponse, Response from starlette.routing import Host, Mount, NoMatchFound, Route, Router, WebSocketRoute from starlette.testclient import TestClient from starlette.websockets import WebSocket, WebSocketDisconnect def homepage(request): return Response("Hello, world", media_type="text/plain") def users(request): return Response("All users", media_type="text/plain") def user(request): content = "User " + request.path_params["username"] return Response(content, media_type="text/plain") def user_me(request): content = "User fixed me" return Response(content, media_type="text/plain") def user_no_match(request): # pragma: no cover content = "User fixed no match" return Response(content, media_type="text/plain") app = Router( [ Route("/", endpoint=homepage, methods=["GET"]), Mount( "/users", routes=[ Route("/", endpoint=users), Route("/me", endpoint=user_me), Route("/{username}", endpoint=user), Route("/nomatch", endpoint=user_no_match), ], ), Mount("/static", app=Response("xxxxx", media_type="image/png")), ] ) @app.route("/func") def func_homepage(request): return Response("Hello, world!", media_type="text/plain") @app.route("/func", methods=["POST"]) def contact(request): return Response("Hello, POST!", media_type="text/plain") @app.route("/int/{param:int}", name="int-convertor") def int_convertor(request): number = request.path_params["param"] return JSONResponse({"int": number}) @app.route("/float/{param:float}", name="float-convertor") def float_convertor(request): num = request.path_params["param"] return JSONResponse({"float": num}) @app.route("/path/{param:path}", name="path-convertor") def path_convertor(request): path = request.path_params["param"] return JSONResponse({"path": path}) @app.websocket_route("/ws") async def websocket_endpoint(session): await session.accept() await session.send_text("Hello, world!") await session.close() @app.websocket_route("/ws/{room}") async def websocket_params(session): await session.accept() await session.send_text(f"Hello, {session.path_params['room']}!") await session.close() client = TestClient(app) def test_router(): response = client.get("/") assert response.status_code == 200 assert response.text == "Hello, world" response = client.post("/") assert response.status_code == 405 assert response.text == "Method Not Allowed" response = client.get("/foo") assert response.status_code == 404 assert response.text == "Not Found" response = client.get("/users") assert response.status_code == 200 assert response.text == "All users" response = client.get("/users/tomchristie") assert response.status_code == 200 assert response.text == "User tomchristie" response = client.get("/users/me") assert response.status_code == 200 assert response.text == "User fixed me" response = client.get("/users/nomatch") assert response.status_code == 200 assert response.text == "User nomatch" response = client.get("/static/123") assert response.status_code == 200 assert response.text == "xxxxx" def test_route_converters(): # Test integer conversion response = client.get("/int/5") assert response.status_code == 200 assert response.json() == {"int": 5} assert app.url_path_for("int-convertor", param=5) == "/int/5" # Test float conversion response = client.get("/float/25.5") assert response.status_code == 200 assert response.json() == {"float": 25.5} assert app.url_path_for("float-convertor", param=25.5) == "/float/25.5" # Test path conversion response = client.get("/path/some/example") assert response.status_code == 200 assert response.json() == {"path": "some/example"} assert ( app.url_path_for("path-convertor", param="some/example") == "/path/some/example" ) def test_url_path_for(): assert app.url_path_for("homepage") == "/" assert app.url_path_for("user", username="tomchristie") == "/users/tomchristie" assert app.url_path_for("websocket_endpoint") == "/ws" with pytest.raises(NoMatchFound): assert app.url_path_for("broken") with pytest.raises(AssertionError): app.url_path_for("user", username="tom/christie") with pytest.raises(AssertionError): app.url_path_for("user", username="") def test_url_for(): assert ( app.url_path_for("homepage").make_absolute_url(base_url="https://example.org") == "https://example.org/" ) assert ( app.url_path_for("user", username="tomchristie").make_absolute_url( base_url="https://example.org" ) == "https://example.org/users/tomchristie" ) assert ( app.url_path_for("websocket_endpoint").make_absolute_url( base_url="https://example.org" ) == "wss://example.org/ws" ) def test_router_add_route(): response = client.get("/func") assert response.status_code == 200 assert response.text == "Hello, world!" def test_router_duplicate_path(): response = client.post("/func") assert response.status_code == 200 assert response.text == "Hello, POST!" def test_router_add_websocket_route(): with client.websocket_connect("/ws") as session: text = session.receive_text() assert text == "Hello, world!" with client.websocket_connect("/ws/test") as session: text = session.receive_text() assert text == "Hello, test!" def http_endpoint(request): url = request.url_for("http_endpoint") return Response(f"URL: {url}", media_type="text/plain") class WebSocketEndpoint: async def __call__(self, scope, receive, send): websocket = WebSocket(scope=scope, receive=receive, send=send) await websocket.accept() await websocket.send_json({"URL": str(websocket.url_for("websocket_endpoint"))}) await websocket.close() mixed_protocol_app = Router( routes=[ Route("/", endpoint=http_endpoint), WebSocketRoute("/", endpoint=WebSocketEndpoint(), name="websocket_endpoint"), ] ) def test_protocol_switch(): client = TestClient(mixed_protocol_app) response = client.get("/") assert response.status_code == 200 assert response.text == "URL: http://testserver/" with client.websocket_connect("/") as session: assert session.receive_json() == {"URL": "ws://testserver/"} with pytest.raises(WebSocketDisconnect): client.websocket_connect("/404") ok = PlainTextResponse("OK") def test_mount_urls(): mounted = Router([Mount("/users", ok, name="users")]) client = TestClient(mounted) assert client.get("/users").status_code == 200 assert client.get("/users").url == "http://testserver/users/" assert client.get("/users/").status_code == 200 assert client.get("/users/a").status_code == 200 assert client.get("/usersa").status_code == 404 def test_reverse_mount_urls(): mounted = Router([Mount("/users", ok, name="users")]) assert mounted.url_path_for("users", path="/a") == "/users/a" users = Router([Route("/{username}", ok, name="user")]) mounted = Router([Mount("/{subpath}/users", users, name="users")]) assert ( mounted.url_path_for("users:user", subpath="test", username="tom") == "/test/users/tom" ) assert ( mounted.url_path_for("users", subpath="test", path="/tom") == "/test/users/tom" ) def test_mount_at_root(): mounted = Router([Mount("/", ok, name="users")]) client = TestClient(mounted) assert client.get("/").status_code == 200 def users_api(request): return JSONResponse({"users": [{"username": "tom"}]}) mixed_hosts_app = Router( routes=[ Host( "www.example.org", app=Router( [ Route("/", homepage, name="homepage"), Route("/users", users, name="users"), ] ), ), Host( "api.example.org", name="api", app=Router([Route("/users", users_api, name="users")]), ), ] ) def test_host_routing(): client = TestClient(mixed_hosts_app, base_url="https://api.example.org/") response = client.get("/users") assert response.status_code == 200 assert response.json() == {"users": [{"username": "tom"}]} response = client.get("/") assert response.status_code == 404 client = TestClient(mixed_hosts_app, base_url="https://www.example.org/") response = client.get("/users") assert response.status_code == 200 assert response.text == "All users" response = client.get("/") assert response.status_code == 200 def test_host_reverse_urls(): assert ( mixed_hosts_app.url_path_for("homepage").make_absolute_url("https://whatever") == "https://www.example.org/" ) assert ( mixed_hosts_app.url_path_for("users").make_absolute_url("https://whatever") == "https://www.example.org/users" ) assert ( mixed_hosts_app.url_path_for("api:users").make_absolute_url("https://whatever") == "https://api.example.org/users" ) async def subdomain_app(scope, receive, send): response = JSONResponse({"subdomain": scope["path_params"]["subdomain"]}) await response(scope, receive, send) subdomain_app = Router( routes=[Host("{subdomain}.example.org", app=subdomain_app, name="subdomains")] ) def test_subdomain_routing(): client = TestClient(subdomain_app, base_url="https://foo.example.org/") response = client.get("/") assert response.status_code == 200 assert response.json() == {"subdomain": "foo"} def test_subdomain_reverse_urls(): assert ( subdomain_app.url_path_for( "subdomains", subdomain="foo", path="/homepage" ).make_absolute_url("https://whatever") == "https://foo.example.org/homepage" )