import pytest from app.core.config import Settings from app.services.auth_service import ( derive_refresh_token, hash_password, issue_refresh_token, issue_token, verify_password, verify_token, ) def settings(**overrides): return Settings( target_base_url="https://target.test", judge_base_url="https://judge.test", **overrides, ) def test_password_hash_is_salted_and_verifiable(): encoded = hash_password("correct-horse-battery-staple") assert encoded.startswith("pbkdf2_sha256$600000$") assert "correct-horse-battery-staple" not in encoded assert verify_password("correct-horse-battery-staple", encoded) assert not verify_password("wrong", encoded) assert not verify_password("wrong", "not-a-password-hash") def test_signed_token_rejects_tampering_and_requires_secret_outside_tests(): token, expires_at = issue_token(7, settings(auth_token_secret="x" * 32)) assert expires_at > 0 assert verify_token(token, settings(auth_token_secret="x" * 32)) == 7 assert verify_token(token + "x", settings(auth_token_secret="x" * 32)) is None with pytest.raises(ValueError): issue_token(7, settings(app_env="production", auth_token_secret="")) with pytest.raises(ValueError): issue_token(7, settings(app_env="production", auth_token_secret="too-short")) def test_refresh_tokens_are_random_but_idempotent_rotation_is_stable(): config = settings(auth_token_secret="x" * 32) first, second = issue_refresh_token(), issue_refresh_token() assert first != second assert derive_refresh_token(first, "attempt-1", config) == derive_refresh_token( first, "attempt-1", config ) assert derive_refresh_token(first, "attempt-1", config) != derive_refresh_token( first, "attempt-2", config ) def test_session_timing_configuration_requires_refresh_window_before_expiry(): assert settings().auth_token_ttl_seconds == 3600 assert settings().auth_token_refresh_window_seconds == 1800 assert settings().auth_refresh_token_ttl_seconds == 28800 with pytest.raises(ValueError): settings(auth_token_ttl_seconds=1800, auth_token_refresh_window_seconds=1800) def test_create_user_cli_prompts_and_never_accepts_password_argument(monkeypatch): from app import create_user created = {} class FakeDatabase: def __enter__(self): return self def __exit__(self, *_): return None class FakeRepository: def __init__(self, _): pass def create(self, *, username, password, is_admin): created.update(username=username, password=password, is_admin=is_admin) monkeypatch.setattr(create_user, "SessionLocal", FakeDatabase) monkeypatch.setattr(create_user, "UserRepository", FakeRepository) monkeypatch.setattr(create_user.getpass, "getpass", lambda _: "correct-horse-battery-staple") monkeypatch.setattr("sys.argv", ["create_user", "auditor"]) create_user.main() assert created == { "username": "auditor", "password": "correct-horse-battery-staple", "is_admin": True, }