from __future__ import annotations from pathlib import Path from fastapi.testclient import TestClient from app.main import create_app from app.services.auth_service import AuthService def auth_client(monkeypatch) -> TestClient: password_hash = AuthService.hash_password( "correct horse battery staple", salt=b"geointel-test-salt", iterations=100_000, ) monkeypatch.setenv("GEOINTEL_AUTH_ENABLED", "true") monkeypatch.setenv("GEOINTEL_AUTH_USERNAME", "operator") monkeypatch.setenv("GEOINTEL_AUTH_PASSWORD_HASH", password_hash) monkeypatch.setenv("GEOINTEL_AUTH_SESSION_SECRET", "test-session-secret-that-is-long-enough") return TestClient(create_app()) def test_auth_session_and_health_are_public_but_api_is_protected(monkeypatch) -> None: client = auth_client(monkeypatch) session = client.get("/api/v1/auth/session") protected = client.get("/api/v1/protected-probe") health = client.get("/health/live") assert session.status_code == 200 assert session.json()["data"] == { "authentication_required": True, "authenticated": False, "username": None, "expires_at": None, } assert protected.status_code == 401 assert protected.json()["error"] == "AUTHENTICATION_REQUIRED" assert health.status_code == 200 def test_login_uses_http_only_session_cookie_and_logout_revokes_browser_access(monkeypatch) -> None: client = auth_client(monkeypatch) invalid = client.post( "/api/v1/auth/login", json={"username": "operator", "password": "wrong"}, ) login = client.post( "/api/v1/auth/login", json={"username": "operator", "password": "correct horse battery staple"}, ) authenticated = client.get("/api/v1/auth/session") protected_after_login = client.get("/api/v1/protected-probe") logout = client.post("/api/v1/auth/logout") protected_after_logout = client.get("/api/v1/protected-probe") assert invalid.status_code == 401 assert invalid.json()["error"] == "INVALID_CREDENTIALS" assert login.status_code == 200 assert login.json()["data"]["username"] == "operator" cookie = login.headers["set-cookie"].lower() assert "httponly" in cookie assert "samesite=strict" in cookie assert authenticated.json()["data"]["authenticated"] is True assert protected_after_login.status_code == 404 assert logout.status_code == 200 assert protected_after_logout.status_code == 401 def test_password_hash_and_session_signatures_fail_closed(monkeypatch) -> None: client = auth_client(monkeypatch) login = client.post( "/api/v1/auth/login", json={"username": "operator", "password": "correct horse battery staple"}, ) token = login.cookies.get("geointel_session") assert token client.cookies.set("geointel_session", f"{token}tampered") session = client.get("/api/v1/auth/session") assert session.status_code == 200 assert session.json()["data"]["authenticated"] is False def test_unraid_runtime_carries_only_hashed_operator_credentials() -> None: root = Path(__file__).resolve().parents[2] runner = (root / "deploy/unraid/run-dockerman-container.sh").read_text(encoding="utf-8") example = (root / "deploy/unraid/geointel.env.example").read_text(encoding="utf-8") browser_smoke = (root / "scripts/verify_browser_runtime.sh").read_text(encoding="utf-8") assert '-e GEOINTEL_AUTH_PASSWORD_HASH="$GEOINTEL_AUTH_PASSWORD_HASH"' in runner assert "GEOINTEL_AUTH_PASSWORD_HASH=" in example assert "GEOINTEL_AUTH_PASSWORD=" not in runner assert "/api/v1/auth/session" in browser_smoke