"""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)