- app.py split from 1966-line test_app.py into 7 files (68 tests, 92% coverage): - test_app_lifespan: lifespan, ensure_initial_admin_user - test_app_handlers: exception handlers, HSTS - test_app_middleware: log_requests, middleware chain - test_app_ws_auth: _authenticate_websocket - test_app_ws_endpoint: WS main loop, 5 endpoint handlers - test_app_ws_events: task/maintenance/dataset/translate WS streams - test_app_spa: SPA serving, TestClient integration - migration_engine: extended coverage with init, edge cases, error paths
121 lines
5.5 KiB
Python
121 lines
5.5 KiB
Python
# #region Test.AppModule.WsAuth [C:3] [TYPE Module] [SEMANTICS test,app,ws,auth,jwt,apikey]
|
|
# @BRIEF Tests for app.py — _authenticate_websocket: JWT/API key auth for WebSocket connections.
|
|
# @RELATION BINDS_TO -> [AppModule]
|
|
# @TEST_EDGE: ws_missing_token -> returns False
|
|
# @TEST_EDGE: ws_jwt_auth -> accepts valid JWT
|
|
# @TEST_EDGE: ws_jwt_fallback_apikey -> accepts valid API key after JWT fail
|
|
# @TEST_EDGE: ws_both_fail -> returns False when both auth methods fail
|
|
# @TEST_INVARIANT: ws_auth_gate -> VERIFIED_BY: test_websocket_auth_missing_token, test_websocket_auth_jwt_valid, test_websocket_auth_api_key
|
|
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
|
|
|
import pytest
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
|
|
class TestAuthenticateWebsocket:
|
|
"""_authenticate_websocket — JWT/API key auth."""
|
|
|
|
# #region test_missing_token [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_missing_token(self):
|
|
from src.app import _authenticate_websocket
|
|
ws = MagicMock(); ws.query_params = {}
|
|
assert await _authenticate_websocket(ws, "ws/logs") is False
|
|
# #endregion test_missing_token
|
|
|
|
# #region test_jwt_valid [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_jwt_valid(self):
|
|
from src.app import _authenticate_websocket
|
|
ws = MagicMock(); ws.query_params = {"token": "valid.jwt"}
|
|
with patch("src.core.auth.jwt.decode_token", return_value={"sub": "testuser"}) as md:
|
|
assert await _authenticate_websocket(ws, "ws/logs") is True
|
|
md.assert_called_once_with("valid.jwt")
|
|
# #endregion test_jwt_valid
|
|
|
|
# #region test_jwt_no_sub [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_jwt_no_sub(self):
|
|
from src.app import _authenticate_websocket
|
|
ws = MagicMock(); ws.query_params = {"token": "no-sub"}
|
|
with (
|
|
patch("src.core.auth.jwt.decode_token", return_value={"role": "admin"}),
|
|
patch("src.core.auth.api_key.hash_api_key", return_value="hk"),
|
|
patch("src.core.database.SessionLocal") as msl,
|
|
):
|
|
msl.return_value = MagicMock()
|
|
msl.return_value.query.return_value.filter.return_value.first.return_value = None
|
|
assert await _authenticate_websocket(ws, "ws/logs") is False
|
|
# #endregion test_jwt_no_sub
|
|
|
|
# #region test_jwt_fallback_apikey [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_jwt_fallback_apikey(self):
|
|
from src.app import _authenticate_websocket
|
|
ws = MagicMock(); ws.query_params = {"token": "valid-key"}
|
|
key = MagicMock(); key.active = True; key.name = "Test"
|
|
with (
|
|
patch("src.core.auth.jwt.decode_token", side_effect=Exception("expired")),
|
|
patch("src.core.auth.api_key.hash_api_key", return_value="hk"),
|
|
patch("src.core.database.SessionLocal") as msl,
|
|
):
|
|
msl.return_value = MagicMock()
|
|
msl.return_value.query.return_value.filter.return_value.first.return_value = key
|
|
assert await _authenticate_websocket(ws, "ws/logs") is True
|
|
# #endregion test_jwt_fallback_apikey
|
|
|
|
# #region test_apikey_inactive [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_apikey_inactive(self):
|
|
from src.app import _authenticate_websocket
|
|
ws = MagicMock(); ws.query_params = {"token": "inactive"}
|
|
key = MagicMock(); key.active = False
|
|
with (
|
|
patch("src.core.auth.jwt.decode_token", side_effect=Exception("fail")),
|
|
patch("src.core.auth.api_key.hash_api_key", return_value="hk"),
|
|
patch("src.core.database.SessionLocal") as msl,
|
|
):
|
|
msl.return_value = MagicMock()
|
|
msl.return_value.query.return_value.filter.return_value.first.return_value = key
|
|
assert await _authenticate_websocket(ws, "ws/logs") is False
|
|
# #endregion test_apikey_inactive
|
|
|
|
# #region test_both_fail [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_both_fail(self):
|
|
from src.app import _authenticate_websocket
|
|
ws = MagicMock(); ws.query_params = {"token": "bad"}
|
|
with (
|
|
patch("src.core.auth.jwt.decode_token", side_effect=Exception("invalid")),
|
|
patch("src.core.auth.api_key.hash_api_key", return_value="hk"),
|
|
patch("src.core.database.SessionLocal") as msl,
|
|
):
|
|
msl.return_value = MagicMock()
|
|
msl.return_value.query.return_value.filter.return_value.first.return_value = None
|
|
assert await _authenticate_websocket(ws, "ws/logs") is False
|
|
# #endregion test_both_fail
|
|
|
|
|
|
class TestAuthenticateWebsocketApiKeyException:
|
|
"""_authenticate_websocket — API key DB exception is caught."""
|
|
|
|
# #region test_apikey_db_exception [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_apikey_db_exception(self):
|
|
from src.app import _authenticate_websocket
|
|
ws = MagicMock(); ws.query_params = {"token": "crash"}
|
|
with (
|
|
patch("src.core.auth.jwt.decode_token", side_effect=Exception("JWT fail")),
|
|
patch("src.core.auth.api_key.hash_api_key", return_value="hk"),
|
|
patch("src.core.database.SessionLocal") as msl,
|
|
):
|
|
db = MagicMock(); db.query.side_effect = Exception("DB lost")
|
|
msl.return_value = db
|
|
assert await _authenticate_websocket(ws, "ws/logs") is False
|
|
# #endregion test_apikey_db_exception
|
|
# #endregion Test.AppModule.WsAuth
|