d66af4d8ef
- Add Dependabot configuration for pip, npm, GitHub Actions, Docker - Update copilot instructions to require TODO.md and CHANGELOG.md updates - Add unit tests for middleware, security, app factory, and schemas (125 total tests, 57% coverage) - Update TODO.md with current progress - Update CHANGELOG.md with all changes Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> Agent-Logs-Url: https://github.com/christianlouis/pop_puller_to_gmail/sessions/2f15a52f-6812-4586-9d5c-a144226c842b
189 lines
6.4 KiB
Python
189 lines
6.4 KiB
Python
"""
|
|
Unit tests for extended security module functionality.
|
|
"""
|
|
|
|
from datetime import timedelta
|
|
from app.core.security import (
|
|
create_access_token,
|
|
create_refresh_token,
|
|
decode_token,
|
|
generate_random_token,
|
|
encrypt_credential,
|
|
decrypt_credential,
|
|
CredentialEncryption,
|
|
)
|
|
|
|
|
|
class TestAccessToken:
|
|
"""Test JWT access token creation and decoding"""
|
|
|
|
def test_create_access_token_with_custom_expiry(self):
|
|
"""Test creating an access token with custom expiry"""
|
|
data = {"sub": "test@example.com"}
|
|
token = create_access_token(data, expires_delta=timedelta(hours=1))
|
|
|
|
assert isinstance(token, str)
|
|
assert token.count(".") == 2
|
|
|
|
def test_decode_valid_access_token(self):
|
|
"""Test decoding a valid access token"""
|
|
data = {"sub": "user123"}
|
|
token = create_access_token(data)
|
|
|
|
payload = decode_token(token)
|
|
assert payload is not None
|
|
assert payload["sub"] == "user123"
|
|
assert payload["type"] == "access"
|
|
|
|
def test_decode_invalid_token_returns_none(self):
|
|
"""Test that decoding an invalid token returns None"""
|
|
result = decode_token("invalid.token.string")
|
|
assert result is None
|
|
|
|
def test_decode_empty_token_returns_none(self):
|
|
"""Test that decoding an empty string returns None"""
|
|
result = decode_token("")
|
|
assert result is None
|
|
|
|
def test_access_token_contains_type(self):
|
|
"""Test that access token payload contains type 'access'"""
|
|
data = {"sub": "user@example.com"}
|
|
token = create_access_token(data)
|
|
payload = decode_token(token)
|
|
|
|
assert payload is not None
|
|
assert payload["type"] == "access"
|
|
|
|
def test_access_token_contains_expiry(self):
|
|
"""Test that access token payload contains expiry"""
|
|
data = {"sub": "user@example.com"}
|
|
token = create_access_token(data)
|
|
payload = decode_token(token)
|
|
|
|
assert payload is not None
|
|
assert "exp" in payload
|
|
|
|
|
|
class TestRefreshToken:
|
|
"""Test JWT refresh token creation and decoding"""
|
|
|
|
def test_create_refresh_token(self):
|
|
"""Test refresh token creation"""
|
|
data = {"sub": "test@example.com"}
|
|
token = create_refresh_token(data)
|
|
|
|
assert isinstance(token, str)
|
|
assert token.count(".") == 2
|
|
|
|
def test_decode_refresh_token(self):
|
|
"""Test decoding a valid refresh token"""
|
|
data = {"sub": "user456"}
|
|
token = create_refresh_token(data)
|
|
|
|
payload = decode_token(token)
|
|
assert payload is not None
|
|
assert payload["sub"] == "user456"
|
|
assert payload["type"] == "refresh"
|
|
|
|
def test_refresh_token_different_from_access(self):
|
|
"""Test that refresh and access tokens are different"""
|
|
data = {"sub": "test@example.com"}
|
|
access = create_access_token(data)
|
|
refresh = create_refresh_token(data)
|
|
|
|
assert access != refresh
|
|
|
|
|
|
class TestRandomToken:
|
|
"""Test random token generation"""
|
|
|
|
def test_generate_random_token_default_length(self):
|
|
"""Test generating a random token with default length"""
|
|
token = generate_random_token()
|
|
assert isinstance(token, str)
|
|
assert len(token) > 0
|
|
|
|
def test_generate_random_token_custom_length(self):
|
|
"""Test generating a random token with custom length"""
|
|
token = generate_random_token(64)
|
|
assert isinstance(token, str)
|
|
assert len(token) > 0
|
|
|
|
def test_random_tokens_are_unique(self):
|
|
"""Test that generated tokens are unique"""
|
|
tokens = {generate_random_token() for _ in range(10)}
|
|
assert len(tokens) == 10
|
|
|
|
|
|
class TestGlobalEncryptionFunctions:
|
|
"""Test global encryption convenience functions"""
|
|
|
|
def test_encrypt_credential_returns_string(self):
|
|
"""Test that encrypt_credential returns a non-empty string"""
|
|
encrypted = encrypt_credential("my-password")
|
|
assert isinstance(encrypted, str)
|
|
assert len(encrypted) > 0
|
|
|
|
def test_decrypt_credential_roundtrip(self):
|
|
"""Test encrypt/decrypt roundtrip with global functions"""
|
|
original = "super-secret-password-123"
|
|
encrypted = encrypt_credential(original)
|
|
decrypted = decrypt_credential(encrypted)
|
|
assert decrypted == original
|
|
|
|
def test_encrypt_credential_is_not_plaintext(self):
|
|
"""Test that encrypted credential differs from plaintext"""
|
|
password = "my-password"
|
|
encrypted = encrypt_credential(password)
|
|
assert encrypted != password
|
|
|
|
|
|
class TestCredentialEncryptionEdgeCases:
|
|
"""Test edge cases in credential encryption"""
|
|
|
|
def test_encrypt_empty_string(self):
|
|
"""Test encrypting an empty string"""
|
|
encryptor = CredentialEncryption(user_id=1)
|
|
encrypted = encryptor.encrypt("")
|
|
decrypted = encryptor.decrypt(encrypted)
|
|
assert decrypted == ""
|
|
|
|
def test_encrypt_long_string(self):
|
|
"""Test encrypting a very long string"""
|
|
long_password = "a" * 10000
|
|
encryptor = CredentialEncryption(user_id=1)
|
|
encrypted = encryptor.encrypt(long_password)
|
|
decrypted = encryptor.decrypt(encrypted)
|
|
assert decrypted == long_password
|
|
|
|
def test_encrypt_special_characters(self):
|
|
"""Test encrypting a string with special characters"""
|
|
special = "p@$$w0rd!#%^&*()_+-=[]{}|;':\",./<>?"
|
|
encryptor = CredentialEncryption(user_id=1)
|
|
encrypted = encryptor.encrypt(special)
|
|
decrypted = encryptor.decrypt(encrypted)
|
|
assert decrypted == special
|
|
|
|
def test_encrypt_unicode(self):
|
|
"""Test encrypting unicode characters"""
|
|
unicode_str = "密码テスト🔒"
|
|
encryptor = CredentialEncryption(user_id=1)
|
|
encrypted = encryptor.encrypt(unicode_str)
|
|
decrypted = encryptor.decrypt(encrypted)
|
|
assert decrypted == unicode_str
|
|
|
|
def test_custom_key(self):
|
|
"""Test encryption with a custom key"""
|
|
key = "custom-encryption-key-that-is-at-least-32-chars-long"
|
|
encryptor = CredentialEncryption(key=key, user_id=1)
|
|
encrypted = encryptor.encrypt("test-data")
|
|
decrypted = encryptor.decrypt(encrypted)
|
|
assert decrypted == "test-data"
|
|
|
|
def test_system_salt_without_user_id(self):
|
|
"""Test encryption with system salt (no user_id)"""
|
|
encryptor = CredentialEncryption()
|
|
encrypted = encryptor.encrypt("system-data")
|
|
decrypted = encryptor.decrypt(encrypted)
|
|
assert decrypted == "system-data"
|