# #region Test.AppModule.Middleware [C:3] [TYPE Module] [SEMANTICS test,app,middleware,hsts,cors,logging] # @BRIEF Tests for app.py — middleware: HSTS, log_requests, CORS, Session, TraceContext, router registration. # @RELATION BINDS_TO -> [AppModule] # @TEST_EDGE: hsts_enabled -> header set when FORCE_HTTPS=true # @TEST_EDGE: hsts_disabled -> no header when FORCE_HTTPS=false # @TEST_EDGE: polling_endpoint_no_log -> polling /api/tasks GET does not log # @TEST_INVARIANT: hsts_gate -> VERIFIED_BY: test_hsts_middleware_enabled, test_hsts_middleware_disabled from pathlib import Path import sys import os sys.path.insert(0, str(Path(__file__).parent.parent / "src")) import pytest from unittest.mock import AsyncMock, MagicMock, patch from fastapi import Request, HTTPException # #region _make_mock_request [C:1] [TYPE Function] def _make_mock_request(method="GET", path="/api/test", client_host="127.0.0.1"): req = MagicMock(spec=Request) req.method = method req.url = MagicMock() req.url.path = path req.client = MagicMock() req.client.host = client_host req.query_params = {} return req # #endregion _make_mock_request class TestHSTSMiddleware: """HSTSMiddleware — Strict-Transport-Security header.""" # #region test_hsts_enabled [C:2] [TYPE Function] @patch.dict(os.environ, {"FORCE_HTTPS": "true"}, clear=False) @pytest.mark.asyncio async def test_hsts_enabled(self): from src.app import HSTSMiddleware async def call_next(r): resp = MagicMock(); resp.headers = {}; return resp mw = HSTSMiddleware(MagicMock()) resp = await mw.dispatch(_make_mock_request(), call_next) assert resp.headers.get("Strict-Transport-Security") == "max-age=31536000; includeSubDomains" # #endregion test_hsts_enabled # #region test_hsts_disabled [C:2] [TYPE Function] @patch.dict(os.environ, {"FORCE_HTTPS": "false"}, clear=False) @pytest.mark.asyncio async def test_hsts_disabled(self): from src.app import HSTSMiddleware async def call_next(r): resp = MagicMock(); resp.headers = {}; return resp mw = HSTSMiddleware(MagicMock()) assert mw._enabled is False resp = await mw.dispatch(_make_mock_request(), call_next) assert "Strict-Transport-Security" not in resp.headers # #endregion test_hsts_disabled # #region test_hsts_empty_env [C:2] [TYPE Function] @patch.dict(os.environ, {}, clear=False) def test_hsts_empty_env(self): os.environ.pop("FORCE_HTTPS", None) from src.app import HSTSMiddleware mw = HSTSMiddleware(MagicMock()) assert mw._enabled is False # #endregion test_hsts_empty_env class TestLogRequests: """log_requests — HTTP request/response logging.""" # #region test_log_non_polling [C:2] [TYPE Function] @pytest.mark.asyncio async def test_log_non_polling(self): from src.app import log_requests async def call_next(r): resp = MagicMock(); resp.status_code = 200; return resp resp = await log_requests(_make_mock_request(path="/api/dashboards"), call_next) assert resp.status_code == 200 # #endregion test_log_non_polling # #region test_log_polling_skipped [C:2] [TYPE Function] @pytest.mark.asyncio async def test_log_polling_skipped(self): from src.app import log_requests async def call_next(r): resp = MagicMock(); resp.status_code = 200; return resp resp = await log_requests(_make_mock_request(method="GET", path="/api/tasks"), call_next) assert resp.status_code == 200 # #endregion test_log_polling_skipped # #region test_log_post_polling_not_skipped [C:2] [TYPE Function] @pytest.mark.asyncio async def test_log_post_polling_not_skipped(self): from src.app import log_requests async def call_next(r): resp = MagicMock(); resp.status_code = 201; return resp resp = await log_requests(_make_mock_request(method="POST", path="/api/tasks"), call_next) assert resp.status_code == 201 # #endregion test_log_post_polling_not_skipped # #region test_log_network_error [C:2] [TYPE Function] @pytest.mark.asyncio async def test_log_network_error(self): from src.app import log_requests from src.core.utils.network import NetworkError async def call_next(r): raise NetworkError("timeout") with pytest.raises(HTTPException) as e: await log_requests(_make_mock_request(path="/api/dashboards"), call_next) assert e.value.status_code == 503 # #endregion test_log_network_error class TestLogRequestsStderrFallback: """log_requests — stderr JSON dump when logger level > INFO.""" # #region test_stderr_fallback_req [C:2] [TYPE Function] @pytest.mark.asyncio async def test_stderr_fallback_req(self): from src.app import log_requests async def call_next(r): resp = MagicMock(); resp.status_code = 200; return resp with patch("src.app.logger.isEnabledFor", return_value=False): resp = await log_requests(_make_mock_request(path="/api/test-stderr"), call_next) assert resp.status_code == 200 # #endregion test_stderr_fallback_req # #region test_stderr_fallback_resp [C:2] [TYPE Function] @pytest.mark.asyncio async def test_stderr_fallback_resp(self): from src.app import log_requests async def call_next(r): resp = MagicMock(); resp.status_code = 500; return resp with patch("src.app.logger.isEnabledFor", return_value=False): resp = await log_requests(_make_mock_request(path="/api/test-stderr-resp"), call_next) assert resp.status_code == 500 # #endregion test_stderr_fallback_resp class TestMiddlewareConfig: """Middleware registration — Session, CORS, HSTS, TraceContext.""" def test_session_middleware(self): from src.app import app names = [m.cls.__name__ for m in app.user_middleware] assert "SessionMiddleware" in names def test_cors_middleware(self): from src.app import app names = [m.cls.__name__ for m in app.user_middleware] assert "CORSMiddleware" in names def test_hsts_middleware(self): from src.app import app names = [m.cls.__name__ for m in app.user_middleware] assert "HSTSMiddleware" in names def test_trace_context_middleware(self): from src.app import app names = [m.cls.__name__ for m in app.user_middleware] assert "TraceContextMiddleware" in names class TestCorsConfig: """CORS and Session configuration edge cases.""" def test_cors_empty_allowed_origins(self): from src.app import app names = [m.cls.__name__ for m in app.user_middleware] assert "CORSMiddleware" in names def test_session_secret_fallback(self): from src.app import app names = [m.cls.__name__ for m in app.user_middleware] assert "SessionMiddleware" in names class TestRouterRegistration: """Verify all expected route groups are registered.""" def test_expected_routers_registered(self): from src.app import app routes = "\n".join(r.path for r in app.routes) assert "/api/dashboards" in routes or "/api/plugins" in routes assert "/api/tasks" in routes assert "/api/settings" in routes assert "/api/git" in routes or "/api/mappings" in routes assert "/ws/logs/" in routes assert "/ws/task-events" in routes class TestAlembicMigrations: """run_alembic_migrations — test happy path and error path.""" def test_run_alembic_migrations_success(self): """Alembic migrations apply successfully (lines 205-213).""" from src.app import run_alembic_migrations with patch("alembic.command.upgrade") as mock_upgrade, \ patch("alembic.config.Config") as mock_cfg_cls: mock_cfg = MagicMock() mock_cfg_cls.return_value = mock_cfg run_alembic_migrations() mock_upgrade.assert_called_once_with(mock_cfg, "head") def test_run_alembic_migrations_failure(self): """Alembic migration failure raises (lines 214-219).""" from src.app import run_alembic_migrations with patch("alembic.command.upgrade", side_effect=Exception("Migration failed")) as mock_upgrade, \ patch("alembic.config.Config") as mock_cfg_cls: mock_cfg = MagicMock() mock_cfg_cls.return_value = mock_cfg with pytest.raises(Exception, match="Migration failed"): run_alembic_migrations() mock_upgrade.assert_called_once_with(mock_cfg, "head") # #endregion Test.AppModule.Middleware