mmap-backend / tests /test_auth_security.py
jugalgajjar's picture
deploy phase 2 (auth: verification, reset, password rules, blocklist)
1ed7597
Raw
History Blame Contribute Delete
3.12 kB
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