"""Tests for optional app-wide HTTP Basic authentication.""" from __future__ import annotations import base64 import pytest from fastapi import FastAPI, WebSocket from fastapi.testclient import TestClient from starlette.testclient import WebSocketDenialResponse from app.config import Settings from app.security import add_optional_basic_auth_middleware def _auth_header(username: str, password: str) -> dict[str, str]: token = base64.b64encode(f"{username}:{password}".encode()).decode("ascii") return {"Authorization": f"Basic {token}"} def _build_app(*, username: str = "", password: str = "") -> FastAPI: settings = Settings( serial_port="", tcp_host="", ble_address="", basic_auth_username=username, basic_auth_password=password, ) app = FastAPI() add_optional_basic_auth_middleware(app, settings) @app.get("/protected") async def protected(): return {"ok": True} @app.websocket("/ws") async def websocket_endpoint(websocket: WebSocket) -> None: await websocket.accept() await websocket.send_json({"ok": True}) await websocket.close() return app def test_http_request_is_denied_without_basic_auth_credentials(): app = _build_app(username="mesh", password="secret") with TestClient(app) as client: response = client.get("/protected") assert response.status_code == 401 assert response.json() == {"detail": "Unauthorized"} assert response.headers["www-authenticate"] == 'Basic realm="RemoteTerm", charset="UTF-8"' assert response.headers["cache-control"] == "no-store" def test_http_request_is_allowed_with_valid_basic_auth_credentials(): app = _build_app(username="mesh", password="secret") with TestClient(app) as client: response = client.get("/protected", headers=_auth_header("mesh", "secret")) assert response.status_code == 200 assert response.json() == {"ok": True} def test_http_request_accepts_case_insensitive_basic_auth_scheme(): app = _build_app(username="mesh", password="secret") header = _auth_header("mesh", "secret") header["Authorization"] = header["Authorization"].replace("Basic", "basic") with TestClient(app) as client: response = client.get("/protected", headers=header) assert response.status_code == 200 assert response.json() == {"ok": True} def test_websocket_handshake_is_denied_without_basic_auth_credentials(): app = _build_app(username="mesh", password="secret") with TestClient(app) as client: with pytest.raises(WebSocketDenialResponse) as exc_info: with client.websocket_connect("/ws"): pass response = exc_info.value assert response.status_code == 401 assert response.json() == {"detail": "Unauthorized"} assert response.headers["www-authenticate"] == 'Basic realm="RemoteTerm", charset="UTF-8"' def test_websocket_handshake_is_allowed_with_valid_basic_auth_credentials(): app = _build_app(username="mesh", password="secret") with TestClient(app) as client: with client.websocket_connect("/ws", headers=_auth_header("mesh", "secret")) as websocket: assert websocket.receive_json() == {"ok": True}