Files
ss-tools/backend/tests/test_app_ws_auth.py
busya f87c873027 test(app): add split app.py tests and extend migration_engine coverage
- 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
2026-06-10 14:57:18 +03:00

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