Fix CI failures: add backend/conftest.py for module resolution and run black formatting
- Add backend/conftest.py that inserts the backend directory into sys.path, fixing ModuleNotFoundError when pytest runs from the backend/ directory (as CI does with `cd backend && pytest tests/`) - Run black formatter on all 28 backend files that needed reformatting - All 53 tests pass with both `pytest tests/` and `python -m pytest tests/` Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> Agent-Logs-Url: https://github.com/christianlouis/pop_puller_to_gmail/sessions/beb47db2-da25-416a-8fb0-c1452a3b22a7
This commit is contained in:
+23
-16
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Test configuration and fixtures.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import asyncio
|
||||
from typing import AsyncGenerator, Generator
|
||||
@@ -15,7 +16,9 @@ from app.models.database_models import User
|
||||
from app.core.security import get_password_hash, create_access_token
|
||||
|
||||
# Test database URL (use different database for tests)
|
||||
TEST_DATABASE_URL = settings.DATABASE_URL.replace("/pop3_forwarder", "/pop3_forwarder_test")
|
||||
TEST_DATABASE_URL = settings.DATABASE_URL.replace(
|
||||
"/pop3_forwarder", "/pop3_forwarder_test"
|
||||
)
|
||||
|
||||
|
||||
# Note: event_loop fixture removed - pytest-asyncio provides this automatically
|
||||
@@ -30,18 +33,18 @@ async def db_engine():
|
||||
poolclass=NullPool,
|
||||
echo=False,
|
||||
)
|
||||
|
||||
|
||||
# Create tables
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.drop_all)
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
|
||||
yield engine
|
||||
|
||||
|
||||
# Drop tables
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.drop_all)
|
||||
|
||||
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
@@ -53,7 +56,7 @@ async def db_session(db_engine) -> AsyncGenerator[AsyncSession, None]:
|
||||
class_=AsyncSession,
|
||||
expire_on_commit=False,
|
||||
)
|
||||
|
||||
|
||||
async with async_session_maker() as session:
|
||||
yield session
|
||||
|
||||
@@ -61,15 +64,15 @@ async def db_session(db_engine) -> AsyncGenerator[AsyncSession, None]:
|
||||
@pytest.fixture(scope="function")
|
||||
async def client(db_session: AsyncSession) -> AsyncGenerator[AsyncClient, None]:
|
||||
"""Create test client with database session override"""
|
||||
|
||||
|
||||
async def override_get_db():
|
||||
yield db_session
|
||||
|
||||
|
||||
app.dependency_overrides[get_db] = override_get_db
|
||||
|
||||
|
||||
async with AsyncClient(app=app, base_url="http://test") as client:
|
||||
yield client
|
||||
|
||||
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
@@ -120,9 +123,11 @@ def admin_auth_headers(test_admin_user: User) -> dict:
|
||||
|
||||
# Factory fixtures for creating test data
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user_factory(db_session: AsyncSession):
|
||||
"""Factory for creating test users"""
|
||||
|
||||
async def _create_user(
|
||||
email: str = None,
|
||||
password: str = "testpassword123",
|
||||
@@ -132,8 +137,9 @@ def user_factory(db_session: AsyncSession):
|
||||
) -> User:
|
||||
if email is None:
|
||||
import uuid
|
||||
|
||||
email = f"test-{uuid.uuid4()}@example.com"
|
||||
|
||||
|
||||
user = User(
|
||||
email=email,
|
||||
hashed_password=get_password_hash(password),
|
||||
@@ -145,7 +151,7 @@ def user_factory(db_session: AsyncSession):
|
||||
await db_session.commit()
|
||||
await db_session.refresh(user)
|
||||
return user
|
||||
|
||||
|
||||
return _create_user
|
||||
|
||||
|
||||
@@ -154,7 +160,7 @@ def mail_account_factory(db_session: AsyncSession):
|
||||
"""Factory for creating test mail accounts"""
|
||||
from app.models.database_models import MailAccount
|
||||
from app.core.security import encrypt_password
|
||||
|
||||
|
||||
async def _create_mail_account(
|
||||
user_id: int,
|
||||
host: str = "pop.example.com",
|
||||
@@ -166,10 +172,11 @@ def mail_account_factory(db_session: AsyncSession):
|
||||
) -> MailAccount:
|
||||
if username is None:
|
||||
import uuid
|
||||
|
||||
username = f"test-{uuid.uuid4()}@example.com"
|
||||
|
||||
|
||||
encrypted_password = encrypt_password(password, user_id)
|
||||
|
||||
|
||||
account = MailAccount(
|
||||
user_id=user_id,
|
||||
host=host,
|
||||
@@ -184,5 +191,5 @@ def mail_account_factory(db_session: AsyncSession):
|
||||
await db_session.commit()
|
||||
await db_session.refresh(account)
|
||||
return account
|
||||
|
||||
|
||||
return _create_mail_account
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Unit tests for configuration module.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
from app.core.config import Settings
|
||||
@@ -8,7 +9,7 @@ from app.core.config import Settings
|
||||
|
||||
class TestConfigValidation:
|
||||
"""Test configuration validation"""
|
||||
|
||||
|
||||
def test_default_secret_key_rejected(self):
|
||||
"""Test that default SECRET_KEY is rejected"""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
@@ -16,9 +17,9 @@ class TestConfigValidation:
|
||||
SECRET_KEY="change-this-to-a-secure-random-secret-key-in-production",
|
||||
ENCRYPTION_KEY="this-is-a-secure-32-character-key-for-testing",
|
||||
)
|
||||
|
||||
|
||||
assert "SECRET_KEY must be changed from default" in str(exc_info.value)
|
||||
|
||||
|
||||
def test_short_secret_key_rejected(self):
|
||||
"""Test that short SECRET_KEY is rejected"""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
@@ -26,9 +27,9 @@ class TestConfigValidation:
|
||||
SECRET_KEY="short",
|
||||
ENCRYPTION_KEY="this-is-a-secure-32-character-key-for-testing",
|
||||
)
|
||||
|
||||
|
||||
assert "at least 32 characters" in str(exc_info.value)
|
||||
|
||||
|
||||
def test_default_encryption_key_rejected(self):
|
||||
"""Test that default ENCRYPTION_KEY is rejected"""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
@@ -36,9 +37,9 @@ class TestConfigValidation:
|
||||
SECRET_KEY="this-is-a-secure-32-character-key-for-testing",
|
||||
ENCRYPTION_KEY="change-this-to-a-secure-encryption-key",
|
||||
)
|
||||
|
||||
|
||||
assert "ENCRYPTION_KEY must be changed from default" in str(exc_info.value)
|
||||
|
||||
|
||||
def test_short_encryption_key_rejected(self):
|
||||
"""Test that short ENCRYPTION_KEY is rejected"""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
@@ -46,15 +47,21 @@ class TestConfigValidation:
|
||||
SECRET_KEY="this-is-a-secure-32-character-key-for-testing",
|
||||
ENCRYPTION_KEY="short",
|
||||
)
|
||||
|
||||
|
||||
assert "at least 32 characters" in str(exc_info.value)
|
||||
|
||||
|
||||
def test_valid_keys_accepted(self):
|
||||
"""Test that valid keys are accepted"""
|
||||
settings = Settings(
|
||||
SECRET_KEY="this-is-a-secure-32-character-key-for-testing-secret",
|
||||
ENCRYPTION_KEY="this-is-a-secure-32-character-key-for-encryption",
|
||||
)
|
||||
|
||||
assert settings.SECRET_KEY == "this-is-a-secure-32-character-key-for-testing-secret"
|
||||
assert settings.ENCRYPTION_KEY == "this-is-a-secure-32-character-key-for-encryption"
|
||||
|
||||
assert (
|
||||
settings.SECRET_KEY
|
||||
== "this-is-a-secure-32-character-key-for-testing-secret"
|
||||
)
|
||||
assert (
|
||||
settings.ENCRYPTION_KEY
|
||||
== "this-is-a-secure-32-character-key-for-encryption"
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Unit tests for Gmail service module.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch, AsyncMock
|
||||
from app.services.gmail_service import GmailService, GmailInjectionError, GMAIL_SCOPES
|
||||
@@ -91,7 +92,9 @@ class TestGmailService:
|
||||
service = GmailService(access_token="test-access-token")
|
||||
|
||||
mock_api = MagicMock()
|
||||
mock_api.users().messages().insert().execute.side_effect = Exception("API Error")
|
||||
mock_api.users().messages().insert().execute.side_effect = Exception(
|
||||
"API Error"
|
||||
)
|
||||
service._service = mock_api
|
||||
|
||||
with pytest.raises(GmailInjectionError, match="Failed to inject email"):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Unit tests for provider presets and mail server auto-detection.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from app.services.mail_processor import MailServerAutoDetect
|
||||
|
||||
@@ -156,11 +157,13 @@ class TestProviderPresets:
|
||||
def test_provider_presets_import(self):
|
||||
"""Test that provider presets can be imported"""
|
||||
from app.api.v1.endpoints.providers import PROVIDER_PRESETS
|
||||
|
||||
assert len(PROVIDER_PRESETS) > 0
|
||||
|
||||
def test_all_presets_have_required_fields(self):
|
||||
"""Test that all presets have required fields"""
|
||||
from app.api.v1.endpoints.providers import PROVIDER_PRESETS
|
||||
|
||||
for preset in PROVIDER_PRESETS:
|
||||
assert preset.id
|
||||
assert preset.name
|
||||
@@ -171,6 +174,7 @@ class TestProviderPresets:
|
||||
def test_gmail_preset_exists(self):
|
||||
"""Test that Gmail preset is included"""
|
||||
from app.api.v1.endpoints.providers import PROVIDER_PRESETS
|
||||
|
||||
gmail = next((p for p in PROVIDER_PRESETS if p.id == "gmail"), None)
|
||||
assert gmail is not None
|
||||
assert gmail.imap_ssl is not None
|
||||
@@ -179,6 +183,7 @@ class TestProviderPresets:
|
||||
def test_gmx_preset_exists(self):
|
||||
"""Test that GMX preset is included"""
|
||||
from app.api.v1.endpoints.providers import PROVIDER_PRESETS
|
||||
|
||||
gmx = next((p for p in PROVIDER_PRESETS if p.id == "gmx"), None)
|
||||
assert gmx is not None
|
||||
assert "gmx.de" in gmx.domains
|
||||
@@ -186,6 +191,7 @@ class TestProviderPresets:
|
||||
def test_webde_preset_exists(self):
|
||||
"""Test that WEB.DE preset is included"""
|
||||
from app.api.v1.endpoints.providers import PROVIDER_PRESETS
|
||||
|
||||
webde = next((p for p in PROVIDER_PRESETS if p.id == "webde"), None)
|
||||
assert webde is not None
|
||||
assert "web.de" in webde.domains
|
||||
@@ -193,6 +199,7 @@ class TestProviderPresets:
|
||||
def test_outlook_preset_exists(self):
|
||||
"""Test that Outlook preset is included"""
|
||||
from app.api.v1.endpoints.providers import PROVIDER_PRESETS
|
||||
|
||||
outlook = next((p for p in PROVIDER_PRESETS if p.id == "outlook"), None)
|
||||
assert outlook is not None
|
||||
assert "hotmail.com" in outlook.domains
|
||||
@@ -200,17 +207,20 @@ class TestProviderPresets:
|
||||
def test_yahoo_preset_exists(self):
|
||||
"""Test that Yahoo preset is included"""
|
||||
from app.api.v1.endpoints.providers import PROVIDER_PRESETS
|
||||
|
||||
yahoo = next((p for p in PROVIDER_PRESETS if p.id == "yahoo"), None)
|
||||
assert yahoo is not None
|
||||
|
||||
def test_aol_preset_exists(self):
|
||||
"""Test that AOL preset is included"""
|
||||
from app.api.v1.endpoints.providers import PROVIDER_PRESETS
|
||||
|
||||
aol = next((p for p in PROVIDER_PRESETS if p.id == "aol"), None)
|
||||
assert aol is not None
|
||||
|
||||
def test_tonline_preset_exists(self):
|
||||
"""Test that T-Online preset is included"""
|
||||
from app.api.v1.endpoints.providers import PROVIDER_PRESETS
|
||||
|
||||
tonline = next((p for p in PROVIDER_PRESETS if p.id == "tonline"), None)
|
||||
assert tonline is not None
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Unit tests for security module.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from app.core.security import (
|
||||
get_password_hash,
|
||||
@@ -12,99 +13,99 @@ from app.core.security import (
|
||||
|
||||
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
|
||||
assert token.count(".") == 2 # JWT has 3 parts
|
||||
|
||||
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user