feat: auth & admin
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user