Spaces:
Sleeping
Sleeping
| from datetime import UTC, datetime, timedelta | |
| import jwt | |
| import pytest | |
| from app.auth.security import ( | |
| create_access_token, | |
| decode_access_token, | |
| generate_code, | |
| hash_password, | |
| verify_password, | |
| ) | |
| from app.core.config import settings | |
| class TestPasswordHashing: | |
| def test_hash_is_not_plaintext(self): | |
| hashed = hash_password("secret123") | |
| assert hashed != "secret123" | |
| assert len(hashed) > 30 | |
| def test_hash_is_salted(self): | |
| a = hash_password("secret123") | |
| b = hash_password("secret123") | |
| assert a != b | |
| def test_verify_returns_true_for_correct_password(self): | |
| hashed = hash_password("secret123") | |
| assert verify_password("secret123", hashed) is True | |
| def test_verify_returns_false_for_wrong_password(self): | |
| hashed = hash_password("secret123") | |
| assert verify_password("wrong", hashed) is False | |
| def test_verify_returns_false_for_malformed_hash(self): | |
| assert verify_password("secret123", "not-a-bcrypt-hash") is False | |
| class TestJWT: | |
| def test_create_and_decode_round_trip(self): | |
| token = create_access_token("user-123") | |
| payload = decode_access_token(token) | |
| assert payload["sub"] == "user-123" | |
| assert "exp" in payload | |
| assert "iat" in payload | |
| def test_decode_rejects_tampered_token(self): | |
| token = create_access_token("user-123") | |
| tampered = token[:-4] + "AAAA" | |
| with pytest.raises(jwt.InvalidTokenError): | |
| decode_access_token(tampered) | |
| def test_decode_rejects_expired_token(self): | |
| token = jwt.encode( | |
| { | |
| "sub": "user-123", | |
| "iat": datetime.now(UTC) - timedelta(minutes=10), | |
| "exp": datetime.now(UTC) - timedelta(minutes=5), | |
| }, | |
| settings.jwt_secret, | |
| algorithm=settings.jwt_algorithm, | |
| ) | |
| with pytest.raises(jwt.ExpiredSignatureError): | |
| decode_access_token(token) | |
| def test_decode_rejects_wrong_secret(self): | |
| token = jwt.encode( | |
| {"sub": "user-123", "exp": datetime.now(UTC) + timedelta(minutes=5)}, | |
| "different-secret", | |
| algorithm=settings.jwt_algorithm, | |
| ) | |
| with pytest.raises(jwt.InvalidSignatureError): | |
| decode_access_token(token) | |
| def test_create_respects_custom_expiry(self): | |
| token = create_access_token("user-123", expires_minutes=1) | |
| payload = decode_access_token(token) | |
| delta = payload["exp"] - payload["iat"] | |
| assert 55 <= delta <= 65 | |
| class TestGenerateCode: | |
| def test_default_length_8(self): | |
| assert len(generate_code()) == 8 | |
| def test_custom_length(self): | |
| assert len(generate_code(12)) == 12 | |
| def test_alphabet_excludes_ambiguous(self): | |
| # 0/O and 1/I/l are hard to tell apart in most fonts. | |
| for _ in range(50): | |
| code = generate_code(8) | |
| assert not any(ch in code for ch in "0O1Il") | |
| def test_codes_differ(self): | |
| seen = {generate_code() for _ in range(20)} | |
| assert len(seen) == 20 | |