feat: add operator landing and login
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
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
|
||||
Reference in New Issue
Block a user