itcloud/backend/tests/test_security.py

265 lines
9.1 KiB
Python
Raw Normal View History

2025-12-30 15:36:41 +01:00
"""Tests for security module."""
import pytest
from app.infra.security import (
hash_password,
verify_password,
create_access_token,
create_refresh_token,
decode_access_token,
decode_refresh_token,
get_subject,
)
class TestPasswordHashing:
"""Test password hashing and verification."""
def test_hash_password_creates_hash(self):
"""Test that hash_password creates a valid Argon2 hash."""
password = "test_password_123"
hashed = hash_password(password)
# Argon2 hashes start with $argon2
assert hashed.startswith("$argon2")
# Argon2 hashes are longer than bcrypt (typically 90+ characters)
assert len(hashed) > 80
def test_verify_password_correct(self):
"""Test that verify_password returns True for correct password."""
password = "my_secure_password"
hashed = hash_password(password)
assert verify_password(password, hashed) is True
def test_verify_password_incorrect(self):
"""Test that verify_password returns False for incorrect password."""
password = "my_secure_password"
hashed = hash_password(password)
assert verify_password("wrong_password", hashed) is False
def test_short_password(self):
"""Test that short passwords work correctly."""
password = "abc"
hashed = hash_password(password)
assert verify_password(password, hashed) is True
assert verify_password("xyz", hashed) is False
def test_long_password_under_72_bytes(self):
"""Test passwords under 72 bytes."""
# 50 character password (50 bytes in ASCII)
password = "a" * 50
hashed = hash_password(password)
assert verify_password(password, hashed) is True
assert verify_password("a" * 49, hashed) is False
def test_long_password_over_72_bytes(self):
"""Test that passwords over 72 bytes work (this is the critical test)."""
# 100 character password (100 bytes in ASCII)
password = "a" * 100
hashed = hash_password(password)
# Should work without ValueError
assert verify_password(password, hashed) is True
# Different password should fail
assert verify_password("a" * 99, hashed) is False
assert verify_password("a" * 101, hashed) is False
def test_very_long_password(self):
"""Test extremely long passwords (200+ bytes)."""
password = "x" * 200
hashed = hash_password(password)
assert verify_password(password, hashed) is True
assert verify_password("x" * 199, hashed) is False
def test_unicode_password(self):
"""Test passwords with unicode characters."""
password = "пароль_с_юникодом_🔒"
hashed = hash_password(password)
assert verify_password(password, hashed) is True
assert verify_password("пароль_с_юникодом_🔓", hashed) is False
def test_long_unicode_password(self):
"""Test long unicode password (each Cyrillic char is 2 bytes in UTF-8)."""
# 50 Cyrillic characters = 100 bytes in UTF-8
password = "п" * 50
hashed = hash_password(password)
assert verify_password(password, hashed) is True
assert verify_password("п" * 49, hashed) is False
def test_same_password_different_hashes(self):
"""Test that same password produces different hashes (salt)."""
password = "same_password"
hash1 = hash_password(password)
hash2 = hash_password(password)
# Hashes should be different (Argon2 uses random salt)
assert hash1 != hash2
# But both should verify correctly
assert verify_password(password, hash1) is True
assert verify_password(password, hash2) is True
def test_empty_password_raises_error(self):
"""Test that empty password raises ValueError."""
with pytest.raises(ValueError, match="password must be a non-empty string"):
hash_password("")
def test_none_password_raises_error(self):
"""Test that None password raises ValueError."""
with pytest.raises(ValueError, match="password must be a non-empty string"):
hash_password(None)
def test_verify_empty_password_returns_false(self):
"""Test that verifying with empty password/hash returns False."""
hashed = hash_password("validpassword")
assert verify_password("", hashed) is False
assert verify_password("validpassword", "") is False
def test_password_with_special_chars(self):
"""Test password with special characters."""
password = "P@ssw0rd!#$%^&*()_+-=[]{}|;:',.<>?/~`"
hashed = hash_password(password)
assert verify_password(password, hashed) is True
def test_password_with_spaces(self):
"""Test password with spaces."""
password = "password with spaces"
hashed = hash_password(password)
assert verify_password(password, hashed) is True
assert verify_password("passwordwithspaces", hashed) is False
def test_realistic_long_password(self):
"""Test realistic long password scenario."""
# Simulate a password manager generated password
password = "Xy8#mK9$pL2@nQ7!wE6%rT5^yU4&iO3*aS2(dF1)"
hashed = hash_password(password)
assert verify_password(password, hashed) is True
# One character different should fail
wrong = "Xy8#mK9$pL2@nQ7!wE6%rT5^yU4&iO3*aS2(dF2)"
assert verify_password(wrong, hashed) is False
class TestJWTTokens:
"""Test JWT token creation and validation."""
def test_create_access_token(self):
"""Test access token creation."""
user_id = "user123"
token = create_access_token(subject=user_id)
assert token is not None
assert isinstance(token, str)
assert len(token) > 50
def test_create_refresh_token(self):
"""Test refresh token creation."""
user_id = "user456"
token = create_refresh_token(subject=user_id)
assert token is not None
assert isinstance(token, str)
assert len(token) > 50
def test_decode_access_token(self):
"""Test decoding valid access token."""
user_id = "user789"
token = create_access_token(subject=user_id)
payload = decode_access_token(token)
assert payload is not None
assert payload["sub"] == user_id
assert payload["typ"] == "access"
assert "exp" in payload
assert "iat" in payload
assert "jti" in payload
def test_decode_refresh_token(self):
"""Test decoding valid refresh token."""
user_id = "user999"
token = create_refresh_token(subject=user_id)
payload = decode_refresh_token(token)
assert payload is not None
assert payload["sub"] == user_id
assert payload["typ"] == "refresh"
assert "exp" in payload
assert "iat" in payload
assert "jti" in payload
def test_decode_access_token_rejects_refresh(self):
"""Test that decode_access_token rejects refresh tokens."""
user_id = "user111"
refresh_token = create_refresh_token(subject=user_id)
payload = decode_access_token(refresh_token)
assert payload is None
def test_decode_refresh_token_rejects_access(self):
"""Test that decode_refresh_token rejects access tokens."""
user_id = "user222"
access_token = create_access_token(subject=user_id)
payload = decode_refresh_token(access_token)
assert payload is None
def test_decode_invalid_token(self):
"""Test decoding invalid token."""
payload = decode_access_token("invalid.token.here")
assert payload is None
def test_decode_empty_token(self):
"""Test decoding empty token."""
payload = decode_access_token("")
assert payload is None
def test_get_subject(self):
"""Test extracting subject from payload."""
user_id = "user333"
token = create_access_token(subject=user_id)
payload = decode_access_token(token)
subject = get_subject(payload)
assert subject == user_id
def test_get_subject_from_empty_payload(self):
"""Test extracting subject from empty payload."""
subject = get_subject({})
assert subject is None
def test_create_access_token_with_extra_claims(self):
"""Test creating access token with extra claims."""
user_id = "user444"
extra = {"role": "admin", "email": "test@example.com"}
token = create_access_token(subject=user_id, extra=extra)
payload = decode_access_token(token)
assert payload is not None
assert payload["sub"] == user_id
assert payload.get("role") == "admin"
assert payload.get("email") == "test@example.com"
def test_unique_jti_per_token(self):
"""Test that each token has unique jti."""
user_id = "user555"
token1 = create_access_token(subject=user_id)
token2 = create_access_token(subject=user_id)
payload1 = decode_access_token(token1)
payload2 = decode_access_token(token2)
assert payload1["jti"] != payload2["jti"]