Spaces:
Sleeping
Sleeping
| """Unit tests for the auth crypto utilities — argon2 + JWT. | |
| Crypto code is small but high-stakes. Tests assert: | |
| - argon2 hashes are not reversible to plaintext | |
| - verify() returns False on tampered hash / wrong password | |
| - rehashing on parameter upgrade is detected | |
| - JWT issuance + decode round-trip | |
| - JWT signature tamper detection | |
| - JWT expiry enforcement | |
| - JWT issued for slot_id matches the regex | |
| """ | |
| from __future__ import annotations | |
| import time | |
| import pytest | |
| def test_argon2_hash_is_not_reversible(): | |
| from app.auth.passwords import hash_password, verify_password | |
| h = hash_password("correct horse battery staple") | |
| assert h != "correct horse battery staple" | |
| assert h.startswith("$argon2"), h | |
| def test_argon2_verify_correct_password(): | |
| from app.auth.passwords import hash_password, verify_password | |
| h = hash_password("hunter2!") | |
| assert verify_password(h, "hunter2!") is True | |
| def test_argon2_verify_wrong_password(): | |
| from app.auth.passwords import hash_password, verify_password | |
| h = hash_password("hunter2!") | |
| assert verify_password(h, "hunter3!") is False | |
| assert verify_password(h, "") is False | |
| assert verify_password(h, "hunter2! ") is False # trailing space matters | |
| def test_argon2_verify_tampered_hash(): | |
| from app.auth.passwords import hash_password, verify_password | |
| h = hash_password("hunter2!") | |
| tampered = h[:-3] + "AAA" | |
| assert verify_password(tampered, "hunter2!") is False | |
| def test_argon2_rejects_empty_or_huge_password(): | |
| from app.auth.passwords import hash_password, PasswordRejected | |
| with pytest.raises(PasswordRejected): | |
| hash_password("") | |
| # Argon2 rejects very long strings to avoid DoS via long inputs. | |
| with pytest.raises(PasswordRejected): | |
| hash_password("x" * 4096) | |
| def test_jwt_roundtrip(monkeypatch): | |
| monkeypatch.setenv("JWT_SECRET", "x" * 32) | |
| from app.auth.tokens import decode_access_token, issue_access_token | |
| token = issue_access_token(client_id="11111111-1111-1111-1111-111111111111", slot_id="user_slot_001") | |
| claims = decode_access_token(token) | |
| assert claims["sub"] == "11111111-1111-1111-1111-111111111111" | |
| assert claims["aid"] == "user_slot_001" | |
| assert claims["typ"] == "access" | |
| def test_jwt_signature_tamper(monkeypatch): | |
| monkeypatch.setenv("JWT_SECRET", "x" * 32) | |
| from app.auth.tokens import decode_access_token, issue_access_token, JWTInvalid | |
| token = issue_access_token(client_id="u", slot_id="user_slot_001") | |
| # Flip the last byte of the signature segment. | |
| parts = token.split(".") | |
| parts[2] = parts[2][:-2] + ("AA" if parts[2][-2:] != "AA" else "BB") | |
| tampered = ".".join(parts) | |
| with pytest.raises(JWTInvalid): | |
| decode_access_token(tampered) | |
| def test_jwt_wrong_secret(monkeypatch): | |
| monkeypatch.setenv("JWT_SECRET", "x" * 32) | |
| from app.auth.tokens import decode_access_token, issue_access_token, JWTInvalid | |
| token = issue_access_token(client_id="u", slot_id="user_slot_001") | |
| monkeypatch.setenv("JWT_SECRET", "y" * 32) | |
| with pytest.raises(JWTInvalid): | |
| decode_access_token(token) | |
| def test_jwt_expired(monkeypatch): | |
| monkeypatch.setenv("JWT_SECRET", "x" * 32) | |
| monkeypatch.setenv("ACCESS_TOKEN_TTL_SECONDS", "1") | |
| from app.auth.tokens import decode_access_token, issue_access_token, JWTInvalid | |
| token = issue_access_token(client_id="u", slot_id="user_slot_001") | |
| time.sleep(1.2) | |
| with pytest.raises(JWTInvalid): | |
| decode_access_token(token) | |
| def test_jwt_rejects_invalid_slot_id(monkeypatch): | |
| """Per multi-tenant-auth §5 JWT shape, aid must match user_slot_<digits>.""" | |
| monkeypatch.setenv("JWT_SECRET", "x" * 32) | |
| from app.auth.tokens import decode_access_token, JWTInvalid, issue_access_token | |
| # Forge a token with an aid that violates the regex. | |
| token = issue_access_token(client_id="u", slot_id="user_slot_001") | |
| # Decode legit first — should pass. | |
| decode_access_token(token) | |
| # Now build a manual token with bad aid and check decode rejects. | |
| import jwt | |
| import os | |
| bad = jwt.encode( | |
| {"sub": "u", "aid": "../etc/passwd", "typ": "access", "iat": int(time.time()), "exp": int(time.time()) + 60}, | |
| os.environ["JWT_SECRET"], | |
| algorithm="HS256", | |
| ) | |
| with pytest.raises(JWTInvalid): | |
| decode_access_token(bad) | |
| def test_refresh_token_roundtrip(monkeypatch): | |
| monkeypatch.setenv("JWT_SECRET", "x" * 32) | |
| from app.auth.tokens import decode_refresh_token, issue_refresh_token | |
| token = issue_refresh_token(client_id="u") | |
| claims = decode_refresh_token(token) | |
| assert claims["sub"] == "u" | |
| assert claims["typ"] == "refresh" | |
| def test_refresh_token_rejected_as_access(monkeypatch): | |
| monkeypatch.setenv("JWT_SECRET", "x" * 32) | |
| from app.auth.tokens import decode_access_token, issue_refresh_token, JWTInvalid | |
| rtoken = issue_refresh_token(client_id="u") | |
| with pytest.raises(JWTInvalid): | |
| decode_access_token(rtoken) | |