central/tests/test_setup_gate.py
Matt Johnson f059f982bc feat(gui): add auth core, setup gate, and first-run operator creation
- Add migrations 007-010 for system config, operators, sessions, audit_log
- Implement argon2id password hashing via argon2-cffi
- Implement session-based authentication with database-stored tokens
- Add SetupGateMiddleware to redirect to /setup until first operator created
- Add SessionMiddleware to load session from cookie and attach operator
- Create /setup, /login, /logout, /change-password routes with CSRF protection
- Add periodic session cleanup task (hourly)
- Add audit logging for auth events
- Update systemd unit with EnvironmentFile for /etc/central/central.env
- Add comprehensive tests for auth, middleware, and audit modules

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-17 05:30:49 +00:00

162 lines
5.9 KiB
Python

"""Tests for setup gate middleware."""
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from starlette.testclient import TestClient
from fastapi import FastAPI
from central.gui.middleware import SetupGateMiddleware
class TestSetupGateMiddleware:
"""Tests for SetupGateMiddleware."""
@pytest.mark.asyncio
async def test_allows_setup_route_when_incomplete(self):
"""SetupGateMiddleware allows /setup when setup_complete=False."""
mock_pool = MagicMock()
mock_conn = MagicMock()
mock_conn.fetchrow = AsyncMock(return_value={"setup_complete": False})
mock_conn.__aenter__ = AsyncMock(return_value=mock_conn)
mock_conn.__aexit__ = AsyncMock()
mock_pool.acquire = MagicMock(return_value=mock_conn)
with patch("central.gui.middleware.get_pool", return_value=mock_pool):
app = FastAPI()
@app.get("/setup")
async def setup():
return {"message": "setup"}
app.add_middleware(SetupGateMiddleware)
client = TestClient(app)
response = client.get("/setup")
assert response.status_code == 200
assert response.json() == {"message": "setup"}
@pytest.mark.asyncio
async def test_allows_health_when_incomplete(self):
"""SetupGateMiddleware allows /health regardless of setup state."""
mock_pool = MagicMock()
mock_conn = MagicMock()
mock_conn.fetchrow = AsyncMock(return_value={"setup_complete": False})
mock_conn.__aenter__ = AsyncMock(return_value=mock_conn)
mock_conn.__aexit__ = AsyncMock()
mock_pool.acquire = MagicMock(return_value=mock_conn)
with patch("central.gui.middleware.get_pool", return_value=mock_pool):
app = FastAPI()
@app.get("/health")
async def health():
return {"status": "ok"}
app.add_middleware(SetupGateMiddleware)
client = TestClient(app)
response = client.get("/health")
assert response.status_code == 200
@pytest.mark.asyncio
async def test_redirects_other_routes_when_incomplete(self):
"""SetupGateMiddleware redirects non-setup routes when setup_complete=False."""
mock_pool = MagicMock()
mock_conn = MagicMock()
mock_conn.fetchrow = AsyncMock(return_value={"setup_complete": False})
mock_conn.__aenter__ = AsyncMock(return_value=mock_conn)
mock_conn.__aexit__ = AsyncMock()
mock_pool.acquire = MagicMock(return_value=mock_conn)
with patch("central.gui.middleware.get_pool", return_value=mock_pool):
app = FastAPI()
@app.get("/")
async def index():
return {"message": "home"}
@app.get("/setup")
async def setup():
return {"message": "setup"}
app.add_middleware(SetupGateMiddleware)
client = TestClient(app, follow_redirects=False)
response = client.get("/")
assert response.status_code == 307
assert response.headers["location"] == "/setup"
@pytest.mark.asyncio
async def test_allows_all_routes_when_complete(self):
"""SetupGateMiddleware allows all routes when setup_complete=True."""
mock_pool = MagicMock()
mock_conn = MagicMock()
mock_conn.fetchrow = AsyncMock(return_value={"setup_complete": True})
mock_conn.__aenter__ = AsyncMock(return_value=mock_conn)
mock_conn.__aexit__ = AsyncMock()
mock_pool.acquire = MagicMock(return_value=mock_conn)
with patch("central.gui.middleware.get_pool", return_value=mock_pool):
app = FastAPI()
@app.get("/")
async def index():
return {"message": "home"}
app.add_middleware(SetupGateMiddleware)
client = TestClient(app)
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "home"}
@pytest.mark.asyncio
async def test_allows_static_when_incomplete(self):
"""SetupGateMiddleware allows /static routes when setup_complete=False."""
mock_pool = MagicMock()
mock_conn = MagicMock()
mock_conn.fetchrow = AsyncMock(return_value={"setup_complete": False})
mock_conn.__aenter__ = AsyncMock(return_value=mock_conn)
mock_conn.__aexit__ = AsyncMock()
mock_pool.acquire = MagicMock(return_value=mock_conn)
with patch("central.gui.middleware.get_pool", return_value=mock_pool):
app = FastAPI()
@app.get("/static/test.css")
async def static():
return "css"
app.add_middleware(SetupGateMiddleware)
client = TestClient(app)
response = client.get("/static/test.css")
assert response.status_code == 200
@pytest.mark.asyncio
async def test_redirects_setup_when_complete(self):
"""SetupGateMiddleware redirects /setup to / when setup_complete=True."""
mock_pool = MagicMock()
mock_conn = MagicMock()
mock_conn.fetchrow = AsyncMock(return_value={"setup_complete": True})
mock_conn.__aenter__ = AsyncMock(return_value=mock_conn)
mock_conn.__aexit__ = AsyncMock()
mock_pool.acquire = MagicMock(return_value=mock_conn)
with patch("central.gui.middleware.get_pool", return_value=mock_pool):
app = FastAPI()
@app.get("/")
async def index():
return {"message": "home"}
@app.get("/setup")
async def setup():
return {"message": "setup"}
app.add_middleware(SetupGateMiddleware)
client = TestClient(app, follow_redirects=False)
response = client.get("/setup")
assert response.status_code == 302
assert response.headers["location"] == "/"