feat: Milestone 1 Security & Infrastructure improvements
- 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
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
"""
|
||||
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"
|
||||
Reference in New Issue
Block a user