f439f887d0
Backend fixes: - Fix JWT sub claim: encode as str(user.id), decode with int() cast (python-jose requirement) - Replace all deprecated datetime.utcnow() with datetime.now(timezone.utc) - Replace deprecated FastAPI @app.on_event() with modern lifespan context manager - Replace deprecated Pydantic class Config with model_config = ConfigDict(...) - Replace deprecated Pydantic .dict() with .model_dump() - Fix overly broad except (GmailInjectionError, Exception) → except Exception - Remove unused GmailInjectionError import - Fix TokenPayload schema sub field type from int to str Frontend: - Create frontend/src/lib/api.ts — API client module with auth, user, mail accounts, processing runs APIs - Add !frontend/src/lib/ to .gitignore negation Tests: - Add 3 new JWT tests (sub string encoding, access token type, refresh token type) - Update test_token_payload_schema for string sub claim - All 128 tests pass Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> Agent-Logs-Url: https://github.com/christianlouis/pop_puller_to_gmail/sessions/e0b13eb0-8de7-4f02-81e4-e202cbba4608
144 lines
4.4 KiB
Python
144 lines
4.4 KiB
Python
"""
|
|
Unit tests for security module.
|
|
"""
|
|
|
|
import pytest
|
|
from app.core.security import (
|
|
get_password_hash,
|
|
verify_password,
|
|
create_access_token,
|
|
CredentialEncryption,
|
|
)
|
|
|
|
|
|
class TestPasswordHashing:
|
|
"""Test password hashing and verification"""
|
|
|
|
def test_hash_password(self):
|
|
"""Test password hashing"""
|
|
password = "securepassword123"
|
|
hashed = get_password_hash(password)
|
|
|
|
assert hashed != password
|
|
assert len(hashed) > 50
|
|
assert hashed.startswith("$2b$")
|
|
|
|
def test_verify_password_success(self):
|
|
"""Test password verification with correct password"""
|
|
password = "securepassword123"
|
|
hashed = get_password_hash(password)
|
|
|
|
assert verify_password(password, hashed) is True
|
|
|
|
def test_verify_password_failure(self):
|
|
"""Test password verification with wrong password"""
|
|
password = "securepassword123"
|
|
wrong_password = "wrongpassword"
|
|
hashed = get_password_hash(password)
|
|
|
|
assert verify_password(wrong_password, hashed) is False
|
|
|
|
|
|
class TestJWT:
|
|
"""Test JWT token creation and validation"""
|
|
|
|
def test_create_access_token(self):
|
|
"""Test access token creation"""
|
|
data = {"sub": "test@example.com"}
|
|
token = create_access_token(data)
|
|
|
|
assert isinstance(token, str)
|
|
assert len(token) > 50
|
|
assert token.count(".") == 2 # JWT has 3 parts
|
|
|
|
def test_access_token_sub_claim_is_string(self):
|
|
"""Test that sub claim must be passed as a string (python-jose requirement)"""
|
|
from app.core.security import decode_token
|
|
|
|
# sub should be a string (e.g. str(user.id)), not an integer
|
|
token = create_access_token(data={"sub": "42"})
|
|
payload = decode_token(token)
|
|
|
|
assert payload is not None
|
|
assert payload["sub"] == "42"
|
|
assert isinstance(payload["sub"], str)
|
|
|
|
def test_access_token_contains_type_claim(self):
|
|
"""Test that access token includes type=access claim"""
|
|
from app.core.security import decode_token
|
|
|
|
token = create_access_token(data={"sub": "1"})
|
|
payload = decode_token(token)
|
|
|
|
assert payload is not None
|
|
assert payload["type"] == "access"
|
|
|
|
def test_refresh_token_contains_type_claim(self):
|
|
"""Test that refresh token includes type=refresh claim"""
|
|
from app.core.security import create_refresh_token, decode_token
|
|
|
|
token = create_refresh_token(data={"sub": "1"})
|
|
payload = decode_token(token)
|
|
|
|
assert payload is not None
|
|
assert payload["type"] == "refresh"
|
|
|
|
|
|
class TestEncryption:
|
|
"""Test credential encryption/decryption"""
|
|
|
|
def test_encrypt_password(self):
|
|
"""Test password encryption"""
|
|
password = "mailpassword123"
|
|
user_id = 1
|
|
|
|
encryptor = CredentialEncryption(user_id=user_id)
|
|
encrypted = encryptor.encrypt(password)
|
|
|
|
assert encrypted != password
|
|
assert len(encrypted) > 50
|
|
|
|
def test_decrypt_password(self):
|
|
"""Test password decryption"""
|
|
password = "mailpassword123"
|
|
user_id = 1
|
|
|
|
encryptor = CredentialEncryption(user_id=user_id)
|
|
encrypted = encryptor.encrypt(password)
|
|
decrypted = encryptor.decrypt(encrypted)
|
|
|
|
assert decrypted == password
|
|
|
|
def test_encryption_with_different_user_ids(self):
|
|
"""Test that encryption produces different results for different users"""
|
|
password = "mailpassword123"
|
|
user_id_1 = 1
|
|
user_id_2 = 2
|
|
|
|
encryptor_1 = CredentialEncryption(user_id=user_id_1)
|
|
encryptor_2 = CredentialEncryption(user_id=user_id_2)
|
|
|
|
encrypted_1 = encryptor_1.encrypt(password)
|
|
encrypted_2 = encryptor_2.encrypt(password)
|
|
|
|
# Different users should produce different encrypted values
|
|
assert encrypted_1 != encrypted_2
|
|
|
|
# But decryption should work correctly for each
|
|
assert encryptor_1.decrypt(encrypted_1) == password
|
|
assert encryptor_2.decrypt(encrypted_2) == password
|
|
|
|
def test_decrypt_with_wrong_user_id_fails(self):
|
|
"""Test that decryption fails with wrong user ID"""
|
|
password = "mailpassword123"
|
|
user_id = 1
|
|
wrong_user_id = 2
|
|
|
|
encryptor = CredentialEncryption(user_id=user_id)
|
|
wrong_encryptor = CredentialEncryption(user_id=wrong_user_id)
|
|
|
|
encrypted = encryptor.encrypt(password)
|
|
|
|
with pytest.raises(Exception):
|
|
wrong_encryptor.decrypt(encrypted)
|