Files
codexpi/tests/test_auth.py

90 lines
3.9 KiB
Python
Raw Permalink Normal View History

2026-11-14 20:23:59 +01:00
from dataclasses import replace
from fastapi.testclient import TestClient
from codexpi.main import create_app
def test_login_uses_http_only_strict_cookie(app_env):
client, *_ = app_env
response = client.post("/api/auth/login", json={"username": "admin", "password": "a-very-long-test-password"})
assert response.status_code == 200
cookie = response.headers["set-cookie"].lower()
assert "httponly" in cookie
assert "samesite=strict" in cookie
assert "a-very-long-test-password" not in cookie
def test_production_cookie_is_secure(app_env, tmp_path):
_, app, *_ = app_env
data = tmp_path / "secure-data"
data.mkdir()
settings = replace(app.state.settings, data_dir=data, database_path=data / "secure.db", cookie_secure=True)
secure_app = create_app(settings)
with TestClient(secure_app) as client:
secure_app.state.auth.create_user("secureadmin", "another-long-test-password")
response = client.post("/api/auth/login", json={"username": "secureadmin", "password": "another-long-test-password"})
assert "secure" in response.headers["set-cookie"].lower()
def test_wrong_password_is_rejected(app_env):
client, *_ = app_env
response = client.post("/api/auth/login", json={"username": "admin", "password": "this-password-is-wrong"})
assert response.status_code == 401
log = (app_env[1].state.settings.data_dir / "security.log").read_text(encoding="utf-8")
assert '"event":"auth.login_failed"' in log
assert "this-password-is-wrong" not in log
def test_logout_revokes_and_logs_session(logged_in):
client, app, _, _, headers = logged_in
assert client.post("/api/auth/logout", headers=headers).status_code == 200
assert client.get("/api/auth/me").status_code == 401
assert '"event":"session.deleted"' in (app.state.settings.data_dir / "security.log").read_text(encoding="utf-8")
def test_csrf_required_for_mutations(logged_in):
client, _, projects, _, _ = logged_in
project = projects / "demo"
project.mkdir()
response = client.post("/api/projects", json={"name": "demo", "path": str(project)})
assert response.status_code == 403
def test_security_headers(app_env):
client, *_ = app_env
response = client.get("/api/health")
assert response.headers["x-frame-options"] == "DENY"
assert response.headers["x-content-type-options"] == "nosniff"
assert "frame-ancestors 'none'" in response.headers["content-security-policy"]
def test_admin_manages_accounts_and_users_only_see_their_projects(logged_in):
client, _, projects, _, admin_headers = logged_in
admin_root = projects / "admin-project"
member_root = projects / "member-project"
admin_root.mkdir()
member_root.mkdir()
admin_project = client.post(
"/api/projects", headers=admin_headers, json={"name": "Admin", "path": str(admin_root)},
).json()
created_user = client.post(
"/api/admin/users", headers=admin_headers,
json={"username": "member", "password": "member-password-long", "is_admin": False},
)
assert created_user.status_code == 201
assert created_user.json()["is_admin"] in {False, 0}
assert client.post("/api/auth/logout", headers=admin_headers).status_code == 200
login = client.post("/api/auth/login", json={"username": "member", "password": "member-password-long"})
member_headers = {"X-CSRF-Token": login.json()["csrf_token"]}
assert login.status_code == 200 and login.json()["is_admin"] is False
assert client.get("/api/admin/users").status_code == 403
assert admin_project["id"] not in {item["id"] for item in client.get("/api/projects").json()}
member_project = client.post(
"/api/projects", headers=member_headers, json={"name": "Member", "path": str(member_root)},
)
assert member_project.status_code == 201
assert {item["id"] for item in client.get("/api/projects").json()} == {member_project.json()["id"]}
assert client.get(f"/api/projects/{admin_project['id']}/git").status_code == 404