""" Tests for web/auth.py - JWT token lifecycle, bcrypt helpers. """ import pytest from datetime import datetime, timedelta, timezone from web import auth class TestPasswordHashing: def test_hash_and_verify(self): h = auth.hash_password("hunter2") assert auth.verify_password("hunter2", h) def test_wrong_password_fails(self): h = auth.hash_password("hunter2") assert not auth.verify_password("wrong", h) class TestAccessToken: def test_encode_decode_roundtrip(self): token = auth.create_access_token("user-1", "admin") payload = auth.decode_access_token(token) assert payload is not None assert payload["sub"] == "user-1" assert payload["role"] == "admin" assert payload["type"] == "access" def test_invalid_token_returns_none(self): assert auth.decode_access_token("not.a.token") is None def test_tampered_token_returns_none(self): token = auth.create_access_token("user-1", "admin") # Flip last character tampered = token[:-1] + ("A" if token[-1] != "A" else "B") assert auth.decode_access_token(tampered) is None def test_refresh_token_rejected_as_access(self): token, _jti, _exp = auth.create_refresh_token("user-1") # A refresh token must NOT be accepted as an access token assert auth.decode_access_token(token) is None class TestRefreshToken: def test_encode_decode_roundtrip(self): token, jti, expires_at = auth.create_refresh_token("user-2") payload = auth.decode_refresh_token(token) assert payload is not None assert payload["sub"] == "user-2" assert payload["jti"] == jti assert payload["type"] == "refresh" def test_expires_at_in_future(self): _token, _jti, expires_at = auth.create_refresh_token("user-2") assert expires_at > datetime.now(timezone.utc) def test_access_token_rejected_as_refresh(self): token = auth.create_access_token("user-1", "reader") assert auth.decode_refresh_token(token) is None def test_invalid_token_returns_none(self): assert auth.decode_refresh_token("garbage") is None