79 lines
2.6 KiB
Python
79 lines
2.6 KiB
Python
"""Unit tests for the security adapters (no DB, no network)."""
|
|
|
|
import datetime as dt
|
|
import uuid
|
|
|
|
import jwt
|
|
import pytest
|
|
from app.core.config import Settings
|
|
from app.core.security import Argon2PasswordHasher, JwtTokenService
|
|
from app.domain.errors import AuthenticationError
|
|
from app.domain.tokens import TokenType
|
|
|
|
|
|
def _settings(**overrides: object) -> Settings:
|
|
base: dict[str, object] = {"jwt_secret": "unit-test-secret", "access_token_ttl_seconds": 900}
|
|
base.update(overrides)
|
|
return Settings(**base) # type: ignore[arg-type]
|
|
|
|
|
|
def test_password_hash_roundtrip() -> None:
|
|
hasher = Argon2PasswordHasher()
|
|
hashed = hasher.hash("correct horse battery staple")
|
|
assert hashed != "correct horse battery staple"
|
|
|
|
valid, updated = hasher.verify_and_update("correct horse battery staple", hashed)
|
|
assert valid is True
|
|
assert updated is None # fresh hash, no rehash needed
|
|
|
|
wrong, _ = hasher.verify_and_update("wrong password", hashed)
|
|
assert wrong is False
|
|
|
|
|
|
def test_jwt_issue_and_decode_roundtrip() -> None:
|
|
svc = JwtTokenService(_settings())
|
|
subject = uuid.uuid4()
|
|
|
|
issued = svc.issue(subject=subject, token_type=TokenType.ACCESS)
|
|
claims = svc.decode(issued.encoded)
|
|
|
|
assert claims.subject == subject
|
|
assert claims.token_type is TokenType.ACCESS
|
|
assert claims.jti == issued.jti
|
|
|
|
|
|
def test_jwt_rejects_tampered_token() -> None:
|
|
svc = JwtTokenService(_settings())
|
|
issued = svc.issue(subject=uuid.uuid4(), token_type=TokenType.ACCESS)
|
|
tampered = issued.encoded[:-2] + ("aa" if issued.encoded[-2:] != "aa" else "bb")
|
|
|
|
with pytest.raises(AuthenticationError):
|
|
svc.decode(tampered)
|
|
|
|
|
|
def test_jwt_rejects_wrong_secret() -> None:
|
|
issuer = JwtTokenService(_settings(jwt_secret="secret-a"))
|
|
verifier = JwtTokenService(_settings(jwt_secret="secret-b"))
|
|
issued = issuer.issue(subject=uuid.uuid4(), token_type=TokenType.ACCESS)
|
|
|
|
with pytest.raises(AuthenticationError):
|
|
verifier.decode(issued.encoded)
|
|
|
|
|
|
def test_jwt_rejects_expired_token() -> None:
|
|
settings = _settings()
|
|
secret = settings.jwt_secret.get_secret_value()
|
|
expired = jwt.encode(
|
|
{
|
|
"sub": str(uuid.uuid4()),
|
|
"type": "access",
|
|
"jti": str(uuid.uuid4()),
|
|
"iat": int((dt.datetime.now(dt.UTC) - dt.timedelta(hours=2)).timestamp()),
|
|
"exp": int((dt.datetime.now(dt.UTC) - dt.timedelta(hours=1)).timestamp()),
|
|
},
|
|
secret,
|
|
algorithm=settings.jwt_algorithm,
|
|
)
|
|
with pytest.raises(AuthenticationError):
|
|
JwtTokenService(settings).decode(expired)
|