itcloud/backend/tests/test_security.py

265 lines
9.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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"]