Files
moneyfy/backend/tests/conftest.py
T
Jonas MenzelandClaude Opus 5 bdf4097bd7
Images bauen / build (backend) (push) Failing after 29s
Images bauen / build (frontend) (push) Failing after 30s
CI / backend (push) Successful in 2m6s
CI / frontend (push) Successful in 6m5s
fix(tests): Testsuite unabhängig von einer lokalen .env machen
Ohne backend/.env galt COOKIE_SECURE=true. Der Testclient spricht über http,
httpx verwarf die Secure-Cookies, und 106 Tests scheiterten mit 401. Lokal fiel
das nicht auf, weil dort eine nicht eingecheckte .env den Wert überschrieb – die
CI hat es aufgedeckt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014e7t8UpmoVNMtWivY5LiSH
2026-09-09 17:25:34 +02:00

147 lines
4.9 KiB
Python
Raw Permalink 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.
"""Gemeinsame Test-Fixtures. Nutzt eine separate Testdatenbank."""
import os
import tempfile
# Muss vor dem ersten Import der Anwendung gesetzt sein, da die Settings gecacht werden.
os.environ.setdefault(
"DATABASE_URL",
os.environ.get(
"TEST_DATABASE_URL",
"postgresql+asyncpg://moneyfy:moneyfy@127.0.0.1:5432/moneyfy_test",
),
)
os.environ.setdefault("SECRET_KEY", "test-secret-key-mindestens-32-zeichen-lang")
os.environ.setdefault("ENVIRONMENT", "test")
# Der Testclient spricht über http; `Secure`-Cookies würde httpx verwerfen und
# jede geschützte Route liefe in einen 401. Ohne diese Zeile hinge die Suite an
# einer lokalen .env die CI hat genau das aufgedeckt.
os.environ.setdefault("COOKIE_SECURE", "false")
# Die Hintergrundsuche liefe außerhalb der Testtransaktion sie wird gezielt aufgerufen.
os.environ.setdefault("LOGO_AUTO_RESOLVE", "false")
os.environ.setdefault("LOGO_STORAGE_DIR", tempfile.mkdtemp(prefix="moneyfy-logos-"))
from collections.abc import AsyncGenerator
from datetime import date
from decimal import Decimal
import pytest
from httpx import ASGITransport, AsyncClient
from sqlalchemy import select
from sqlalchemy.ext.asyncio import (
AsyncSession,
async_sessionmaker,
create_async_engine,
)
from app.core.config import settings
from app.core.security import hash_password
from app.db.session import get_session
from app.main import app as fastapi_app
from app.models import Account, AppUser, Base, Category
from app.models.enums import AccountType
from app.services.seed import seed_all
TEST_PASSWORD = "sicher-genug-123"
@pytest.fixture(scope="session")
def anyio_backend() -> str:
return "asyncio"
@pytest.fixture(scope="session")
async def engine():
"""Legt das Schema einmal pro Testlauf frisch an."""
test_engine = create_async_engine(str(settings.database_url), poolclass=None)
async with test_engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await conn.run_sync(Base.metadata.create_all)
yield test_engine
await test_engine.dispose()
@pytest.fixture
async def session(engine) -> AsyncGenerator[AsyncSession, None]:
"""Eine Session je Test, am Ende wird zurückgerollt."""
connection = await engine.connect()
transaction = await connection.begin()
# `create_savepoint` sorgt dafür, dass session.commit() nur den Savepoint freigibt
# und der abschließende Rollback wirklich alle Testdaten entfernt.
maker = async_sessionmaker(
bind=connection,
expire_on_commit=False,
autoflush=False,
join_transaction_mode="create_savepoint",
)
async with maker() as db_session:
yield db_session
await transaction.rollback()
await connection.close()
@pytest.fixture
async def client(session: AsyncSession) -> AsyncGenerator[AsyncClient, None]:
"""HTTP-Client gegen die App, alle Requests laufen in der Test-Transaktion."""
async def _override() -> AsyncGenerator[AsyncSession, None]:
yield session
fastapi_app.dependency_overrides[get_session] = _override
transport = ASGITransport(app=fastapi_app)
async with AsyncClient(transport=transport, base_url="http://test") as http_client:
yield http_client
fastapi_app.dependency_overrides.clear()
@pytest.fixture
async def user(session: AsyncSession) -> AppUser:
"""Ein angemeldefähiger Benutzer mit bekanntem Passwort."""
account = AppUser(
username="tester",
email="tester@example.com",
password_hash=hash_password(TEST_PASSWORD),
must_change_password=False,
)
session.add(account)
await session.flush()
return account
@pytest.fixture
async def auth_client(client: AsyncClient, user: AppUser) -> AsyncClient:
"""Bereits angemeldeter Client die Cookies bleiben am Client hängen."""
response = await client.post(
"/api/auth/login", json={"username": user.username, "password": TEST_PASSWORD}
)
assert response.status_code == 200, response.text
return client
@pytest.fixture
async def seeded(session: AsyncSession) -> dict[str, int]:
"""Kategoriebaum plus ein Konto die Grundlage der meisten Integrationstests."""
await seed_all(session)
account = Account(
name="Girokonto",
type=AccountType.CHECKING,
opening_balance=Decimal("1000.00"),
opening_balance_date=date(2026, 1, 1),
)
session.add(account)
await session.flush()
async def category_id(name: str) -> int:
stmt = select(Category).where(Category.name == name)
return (await session.execute(stmt)).scalars().first().id
return {
"account_id": account.id,
"streaming": await category_id("Streaming"),
"miete": await category_id("Miete"),
"kredite": await category_id("Kredite"),
"gehalt": await category_id("Gehalt"),
"lebensmittel": await category_id("Lebensmittel"),
}