162 lines
4.5 KiB
Python
162 lines
4.5 KiB
Python
"""Auth: hash/verify, session token, login/logout, CSRF, setup."""
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from app.auth import (
|
|
create_session_token,
|
|
get_stored_admin,
|
|
hash_password,
|
|
is_admin_configured,
|
|
store_admin,
|
|
verify_password,
|
|
verify_session_token,
|
|
)
|
|
|
|
|
|
def test_hash_and_verify_password(fake_redis):
|
|
h = hash_password("supersecret")
|
|
assert h.startswith("$argon2")
|
|
assert verify_password("supersecret", h) is True
|
|
assert verify_password("wrong", h) is False
|
|
|
|
|
|
def test_verify_password_bad_hash_returns_false(fake_redis):
|
|
assert verify_password("anything", "not-a-valid-hash") is False
|
|
|
|
|
|
def test_session_token_roundtrip(fake_redis):
|
|
tok = create_session_token("admin")
|
|
assert verify_session_token(tok) == "admin"
|
|
|
|
|
|
def test_session_token_rejects_tampered(fake_redis):
|
|
tok = create_session_token("admin")
|
|
bad = tok[:-4] + "abcd"
|
|
assert verify_session_token(bad) is None
|
|
|
|
|
|
def test_store_and_get_admin(fake_redis):
|
|
store_admin("alice", "pw-strong-1")
|
|
user, h = get_stored_admin()
|
|
assert user == "alice"
|
|
assert h and h.startswith("$argon2")
|
|
assert is_admin_configured() is True
|
|
|
|
|
|
def test_login_rejects_before_setup(app_client, fake_redis):
|
|
r = app_client.post(
|
|
"/api/login",
|
|
json={"username": "admin", "password": "test"},
|
|
)
|
|
assert r.status_code == 409
|
|
|
|
|
|
def test_login_page_renders_when_configured(app_client, fake_redis):
|
|
store_admin("admin", "testpassword123")
|
|
r = app_client.get("/login")
|
|
assert r.status_code == 200
|
|
assert "text/html" in r.headers["content-type"]
|
|
|
|
|
|
def test_setup_page_renders_before_configured(app_client, fake_redis):
|
|
r = app_client.get("/setup")
|
|
assert r.status_code == 200
|
|
assert "text/html" in r.headers["content-type"]
|
|
|
|
|
|
def test_login_happy_path(app_client, fake_redis):
|
|
store_admin("admin", "testpassword123")
|
|
r = app_client.post(
|
|
"/api/login",
|
|
json={"username": "admin", "password": "testpassword123"},
|
|
)
|
|
assert r.status_code == 200
|
|
body = r.json()
|
|
assert body["ok"] is True
|
|
assert "csrf_token" in body
|
|
# Cookies set
|
|
assert "ups_session" in r.cookies
|
|
assert "csrf_token" in r.cookies
|
|
|
|
|
|
def test_login_bad_password(app_client, fake_redis):
|
|
store_admin("admin", "testpassword123")
|
|
r = app_client.post(
|
|
"/api/login",
|
|
json={"username": "admin", "password": "wrong"},
|
|
)
|
|
assert r.status_code == 401
|
|
|
|
|
|
def test_login_bad_username(app_client, fake_redis):
|
|
store_admin("admin", "testpassword123")
|
|
r = app_client.post(
|
|
"/api/login",
|
|
json={"username": "notadmin", "password": "testpassword123"},
|
|
)
|
|
assert r.status_code == 401
|
|
|
|
|
|
def test_logout_clears_cookies(authed_client):
|
|
r = authed_client.post("/api/logout")
|
|
assert r.status_code == 200
|
|
|
|
|
|
def test_setup_creates_admin(app_client, fake_redis):
|
|
r = app_client.post(
|
|
"/api/setup",
|
|
json={"username": "newadmin", "password": "setup-pw-1234"},
|
|
)
|
|
assert r.status_code == 200
|
|
assert is_admin_configured() is True
|
|
|
|
|
|
def test_setup_rejects_when_configured(app_client, fake_redis):
|
|
store_admin("admin", "already-set-password")
|
|
r = app_client.post(
|
|
"/api/setup",
|
|
json={"username": "newadmin", "password": "setup-pw-1234"},
|
|
)
|
|
assert r.status_code == 409
|
|
|
|
|
|
def test_setup_rejects_short_password(app_client, fake_redis):
|
|
r = app_client.post(
|
|
"/api/setup",
|
|
json={"username": "admin", "password": "short"},
|
|
)
|
|
assert r.status_code == 422 # pydantic validation
|
|
|
|
|
|
def test_setup_rejects_bad_username(app_client, fake_redis):
|
|
r = app_client.post(
|
|
"/api/setup",
|
|
json={"username": "bad user!", "password": "good-password-1"},
|
|
)
|
|
assert r.status_code == 400
|
|
|
|
|
|
def test_protected_endpoint_requires_auth(app_client):
|
|
r = app_client.get("/api/config/ups")
|
|
assert r.status_code == 401
|
|
|
|
|
|
def test_csrf_required_for_mutations(app_client, fake_redis):
|
|
store_admin("admin", "testpassword123")
|
|
r = app_client.post(
|
|
"/api/login",
|
|
json={"username": "admin", "password": "testpassword123"},
|
|
)
|
|
assert r.status_code == 200
|
|
# DELETE without CSRF header
|
|
r2 = app_client.delete("/api/config/ups/nonexistent")
|
|
assert r2.status_code == 403
|
|
|
|
|
|
def test_mutations_work_with_csrf(authed_client):
|
|
# Authed client already has CSRF header set in fixture
|
|
r = authed_client.delete("/api/config/ups/nonexistent")
|
|
# Expected 404 because UPS doesn't exist (NOT 401/403)
|
|
assert r.status_code == 404
|