mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-16 17:55:26 -04:00
ee72d71872
Added PASSWORD_MIN_LENGTH and RESERVED_USERNAMES to src/constants.py as the single source of truth. Previously PASSWORD_MIN_LENGTH was hardcoded as 8 in four route handlers and all three JS validation paths; RESERVED_USERNAMES was an inline frozenset duplicated in core/auth.py, routes/assistant_routes.py, routes/research_routes.py, and src/task_scheduler.py. Added GET /api/auth/policy (unauthenticated) so the frontend reads the real values from the server instead of hardcoding them in JS. Added missing empty-username guard to /setup and admin POST /users. Both returned a misleading 500/409 on whitespace-only input. /signup already had the check; this makes all three consistent.
218 lines
7.2 KiB
Python
218 lines
7.2 KiB
Python
"""Tests for auth policy endpoint and password length validation."""
|
|
|
|
import asyncio
|
|
import importlib
|
|
import sys
|
|
import types
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
from fastapi import HTTPException
|
|
|
|
from tests.helpers.import_state import clear_module
|
|
|
|
|
|
def _real_core_package():
|
|
root = Path(__file__).resolve().parent.parent
|
|
core_path = str(root / "core")
|
|
core = sys.modules.get("core")
|
|
if core is None:
|
|
core = types.ModuleType("core")
|
|
sys.modules["core"] = core
|
|
core.__path__ = [core_path]
|
|
clear_module("core.auth")
|
|
return core
|
|
|
|
|
|
def _auth_module():
|
|
_real_core_package()
|
|
return importlib.import_module("core.auth")
|
|
|
|
|
|
def _make_manager(tmp_path):
|
|
auth_mod = _auth_module()
|
|
auth_mod._hash_password = lambda password: f"hash:{password}"
|
|
auth_mod._verify_password = lambda password, hashed: hashed == f"hash:{password}"
|
|
auth_path = tmp_path / "auth.json"
|
|
mgr = auth_mod.AuthManager(str(auth_path))
|
|
return mgr
|
|
|
|
|
|
async def _immediate_to_thread(fn, *args, **kwargs):
|
|
return fn(*args, **kwargs)
|
|
|
|
|
|
# ── AuthManager.policy() ───────────────────────────────────────────────
|
|
|
|
|
|
def test_policy_returns_password_min_length(tmp_path):
|
|
mgr = _make_manager(tmp_path)
|
|
policy = mgr.policy()
|
|
assert policy["password_min_length"] == 8
|
|
|
|
|
|
def test_policy_returns_reserved_usernames(tmp_path):
|
|
mgr = _make_manager(tmp_path)
|
|
policy = mgr.policy()
|
|
assert "internal-tool" in policy["reserved_usernames"]
|
|
assert "api" in policy["reserved_usernames"]
|
|
assert "demo" in policy["reserved_usernames"]
|
|
assert "system" in policy["reserved_usernames"]
|
|
assert isinstance(policy["reserved_usernames"], list)
|
|
|
|
|
|
def test_policy_returns_signup_enabled(tmp_path):
|
|
mgr = _make_manager(tmp_path)
|
|
policy = mgr.policy()
|
|
assert policy["signup_enabled"] is False # default
|
|
|
|
|
|
def test_policy_returns_session_days(tmp_path):
|
|
mgr = _make_manager(tmp_path)
|
|
policy = mgr.policy()
|
|
assert policy["session_days"] == 7
|
|
|
|
|
|
# ── GET /api/auth/policy endpoint ──────────────────────────────────────
|
|
|
|
|
|
def _policy_endpoint(auth_manager):
|
|
sys.modules.pop("routes.auth_routes", None)
|
|
_real_core_package()
|
|
from routes.auth_routes import setup_auth_routes
|
|
|
|
router = setup_auth_routes(auth_manager)
|
|
for route in router.routes:
|
|
if getattr(route, "path", None) == "/api/auth/policy":
|
|
return route.endpoint
|
|
raise AssertionError("policy route not found")
|
|
|
|
|
|
def test_policy_endpoint_returns_dict(tmp_path):
|
|
mgr = _make_manager(tmp_path)
|
|
endpoint = _policy_endpoint(mgr)
|
|
result = asyncio.run(endpoint())
|
|
assert isinstance(result, dict)
|
|
assert "password_min_length" in result
|
|
assert "reserved_usernames" in result
|
|
assert "signup_enabled" in result
|
|
assert "session_days" in result
|
|
|
|
|
|
def test_policy_endpoint_values_match_manager(tmp_path):
|
|
mgr = _make_manager(tmp_path)
|
|
endpoint = _policy_endpoint(mgr)
|
|
result = asyncio.run(endpoint())
|
|
assert result == mgr.policy()
|
|
|
|
|
|
# ── Password length validation ─────────────────────────────────────────
|
|
|
|
|
|
def _setup_endpoint(auth_manager):
|
|
sys.modules.pop("routes.auth_routes", None)
|
|
_real_core_package()
|
|
from routes.auth_routes import SetupRequest, setup_auth_routes
|
|
|
|
router = setup_auth_routes(auth_manager)
|
|
for route in router.routes:
|
|
if getattr(route, "path", None) == "/api/auth/setup":
|
|
return route.endpoint, SetupRequest
|
|
raise AssertionError("setup route not found")
|
|
|
|
|
|
def _signup_endpoint(auth_manager):
|
|
sys.modules.pop("routes.auth_routes", None)
|
|
_real_core_package()
|
|
from routes.auth_routes import SignupRequest, setup_auth_routes
|
|
|
|
router = setup_auth_routes(auth_manager)
|
|
for route in router.routes:
|
|
if getattr(route, "path", None) == "/api/auth/signup":
|
|
return route.endpoint, SignupRequest
|
|
raise AssertionError("signup route not found")
|
|
|
|
|
|
def _change_password_endpoint(auth_manager):
|
|
sys.modules.pop("routes.auth_routes", None)
|
|
_real_core_package()
|
|
from routes.auth_routes import ChangePasswordRequest, setup_auth_routes
|
|
|
|
router = setup_auth_routes(auth_manager)
|
|
for route in router.routes:
|
|
if getattr(route, "path", None) == "/api/auth/change-password":
|
|
return route.endpoint, ChangePasswordRequest
|
|
raise AssertionError("change-password route not found")
|
|
|
|
|
|
def test_setup_rejects_short_password(tmp_path):
|
|
mgr = _make_manager(tmp_path)
|
|
endpoint, SetupRequest = _setup_endpoint(mgr)
|
|
request = SimpleNamespace(client=SimpleNamespace(host="127.0.0.1"))
|
|
body = SetupRequest(username="admin", password="short")
|
|
|
|
with pytest.raises(HTTPException) as exc:
|
|
asyncio.run(endpoint(body=body, request=request))
|
|
|
|
assert exc.value.status_code == 400
|
|
assert "8 characters" in exc.value.detail
|
|
|
|
|
|
def test_signup_rejects_short_password(tmp_path):
|
|
mgr = _make_manager(tmp_path)
|
|
mgr.create_user("admin", "admin-password", is_admin=True)
|
|
mgr.signup_enabled = True
|
|
endpoint, SignupRequest = _signup_endpoint(mgr)
|
|
request = SimpleNamespace(client=SimpleNamespace(host="127.0.0.1"))
|
|
body = SignupRequest(username="newuser", password="short")
|
|
|
|
with pytest.raises(HTTPException) as exc:
|
|
asyncio.run(endpoint(body=body, request=request))
|
|
|
|
assert exc.value.status_code == 400
|
|
assert "8 characters" in exc.value.detail
|
|
|
|
|
|
def test_change_password_rejects_short_password(tmp_path):
|
|
mgr = _make_manager(tmp_path)
|
|
mgr.create_user("alice", "old-password", is_admin=False)
|
|
endpoint, ChangePasswordRequest = _change_password_endpoint(mgr)
|
|
request = SimpleNamespace(
|
|
cookies={"odysseus_session": "current-token"},
|
|
client=SimpleNamespace(host="127.0.0.1"),
|
|
)
|
|
# Mock get_username_for_token to return alice
|
|
mgr.get_username_for_token = MagicMock(return_value="alice")
|
|
body = ChangePasswordRequest(current_password="old-password", new_password="short")
|
|
|
|
with pytest.raises(HTTPException) as exc:
|
|
asyncio.run(endpoint(body=body, request=request))
|
|
|
|
assert exc.value.status_code == 400
|
|
assert "8 characters" in exc.value.detail
|
|
|
|
|
|
def test_setup_accepts_exactly_min_length_password(tmp_path):
|
|
mgr = _make_manager(tmp_path)
|
|
endpoint, SetupRequest = _setup_endpoint(mgr)
|
|
request = SimpleNamespace(client=SimpleNamespace(host="127.0.0.1"))
|
|
body = SetupRequest(username="admin", password="12345678")
|
|
|
|
result = asyncio.run(endpoint(body=body, request=request))
|
|
|
|
assert result == {"ok": True, "message": "Admin account created"}
|
|
|
|
|
|
def test_setup_rejects_seven_char_password(tmp_path):
|
|
mgr = _make_manager(tmp_path)
|
|
endpoint, SetupRequest = _setup_endpoint(mgr)
|
|
request = SimpleNamespace(client=SimpleNamespace(host="127.0.0.1"))
|
|
body = SetupRequest(username="admin", password="1234567")
|
|
|
|
with pytest.raises(HTTPException) as exc:
|
|
asyncio.run(endpoint(body=body, request=request))
|
|
|
|
assert exc.value.status_code == 400
|