92 lines
3.1 KiB
Python
92 lines
3.1 KiB
Python
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,
|
|
}
|