feat(api): Core-API mit Authentifizierung, CRUD und Monatsreport
- Anmeldung über Argon2id und JWT in httpOnly-Cookies, Refresh mit echter Rotation über die neue Tabelle refresh_token - AuthProvider-Protokoll als Vorbereitung für OIDC, Administrator-Anlage beim Erststart mit erzwungenem Passwortwechsel - CRUD für Konten, Kategorien (zweistufiger Baum), Firmen, Recurrences, Preisversionen, Buchungen, Budgets, Vorlagen und Sparziele - Fälligkeiten mit Overlay-Logik: abrufen, bestätigen, auslassen, zurücksetzen - Kontosalden zum Stichtag, Monatsübersicht mit Plan-Ist-Vergleich - SECRET_KEY jetzt mindestens 32 Zeichen; Platzhalter in Produktion abgelehnt - 61 neue Integrationstests, insgesamt 148 grün Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014e7t8UpmoVNMtWivY5LiSH
This commit is contained in:
@@ -10,13 +10,16 @@ os.environ.setdefault(
|
||||
"postgresql+asyncpg://moneyfy:moneyfy@127.0.0.1:5432/moneyfy_test",
|
||||
),
|
||||
)
|
||||
os.environ.setdefault("SECRET_KEY", "test-secret-key")
|
||||
os.environ.setdefault("SECRET_KEY", "test-secret-key-mindestens-32-zeichen-lang")
|
||||
os.environ.setdefault("ENVIRONMENT", "test")
|
||||
|
||||
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,
|
||||
@@ -24,9 +27,14 @@ from sqlalchemy.ext.asyncio import (
|
||||
)
|
||||
|
||||
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 Base
|
||||
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")
|
||||
@@ -76,3 +84,55 @@ async def client(session: AsyncSession) -> AsyncGenerator[AsyncClient, None]:
|
||||
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"),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user