Files
F-r-ldraledigplannerare/backend/tests/conftest.py
T
jacobh2 76b1e712b9 Complete NextSteps roadmap: planning engine UI, calendar cluster, flexible wishes
Works through every numbered item in NextSteps260913.md section 3, plus the
three known verification issues in section 4.1.

Surface what the backend already computed:
- 3.1 render surplus_advice in PlanResults, mirroring the shortfall remedies
- 3.2 surface VacationBalance.is_assumed so assumed statutory years are visible
- 3.3 age-4 expiry given the proposed plan, via a new Age4Projection reusing
  the existing _age4_projections arithmetic (Dashboard stat verified, not rebuilt)

Calendar cluster (one pass over the holiday data):
- new GET /api/calendar/days exposing roda dagar, klamdagar and day overrides,
  paginated four years at a time
- 3.4 shade roda dagar/klamdagar, with R/K badges so colour is not the only signal
- 3.6 click-to-override behind an explicit mode toggle, leaving drag multi-select intact
- 3.7 click month/year to advance
- 3.8 per-year red-days list in Settings, statutory days plus overrides

i18n:
- 3.5 five backend-generated Swedish surfaces converted to message_key + data,
  following the tips_service pattern; old string fields replaced, not kept

Flexible wishes (3.9):
- PLANNING_ENGINE_SPEC section 10: window_mode, candidate search, cost function,
  deterministic tie-break, two-pass ordering, test matrix rows 15-24
- backend: entity, migration 006, _FULL_SCHEMA sync, repository, allocator
- frontend: season-preset toggle in WishEditor, unplaced outcome rendering

Fixes (4.1):
- pin typecheck to the local tsc rather than npx resolving a newer TypeScript
- ignore generated coverage/ and dist/ in the ESLint flat config
- fix the flaky usePlanApply test properly, with waitFor rather than a retry

Tests: backend 361 -> 404, frontend 540 -> 704. Coverage 89.47% -> 96.52% lines.

Not yet verified in a browser.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RXMhc9nT6sNz1SL4j8dznL
2026-09-14 02:33:03 +02:00

255 lines
8.1 KiB
Python

"""Pytest fixtures.
Each test gets a fresh in-memory SQLite connection with the FULL schema applied
(all 9 tables), so all entities can be tested in isolation without Alembic.
Follow the `vacation_repo` / `vacation_service` pattern to add fixtures for
other entities.
"""
from collections.abc import AsyncIterator
from dataclasses import dataclass
from datetime import date, datetime
from uuid import UUID, uuid4
import aiosqlite
import pytest_asyncio
from app.db.connection import ensure_schema
from app.entities.child import Child
from app.entities.personal_data import PersonalData
from app.entities.workspace import Workspace
from app.repositories_impl.sqlite.child_repository import SQLiteChildRepository
from app.repositories_impl.sqlite.day_override_repository import SQLiteDayOverrideRepository
from app.repositories_impl.sqlite.leave_wish_repository import SQLiteLeaveWishRepository
from app.repositories_impl.sqlite.parental_leave_repository import SQLiteParentalLeaveRepository
from app.repositories_impl.sqlite.personal_data_repository import SQLitePersonalDataRepository
from app.repositories_impl.sqlite.planned_leave_repository import SQLitePlannedLeaveRepository
from app.repositories_impl.sqlite.user_repository import SQLiteUserRepository
from app.repositories_impl.sqlite.vacation_days_repository import (
SQLiteVacationDaysRepository,
)
from app.repositories_impl.sqlite.workspace_repository import (
SQLiteWorkspaceInvitationRepository,
SQLiteWorkspaceRepository,
)
from app.services.analytics_service import AnalyticsService
from app.services.auth_service import AuthService
from app.services.child_service import ChildService
from app.services.day_override_service import DayOverrideService
from app.services.leave_wish_service import LeaveWishService
from app.services.planning_service import PlanningService
from app.services.vacation_days_service import VacationDaysService
from app.services.workspace_service import WorkspaceService
@dataclass
class SeededIds:
workspace_id: UUID
personal_data_uuid: UUID
child_id: UUID
@pytest_asyncio.fixture
async def connection() -> AsyncIterator[aiosqlite.Connection]:
conn = await aiosqlite.connect(":memory:")
conn.row_factory = aiosqlite.Row
await conn.execute("PRAGMA foreign_keys = ON;")
await ensure_schema(conn)
try:
yield conn
finally:
await conn.close()
# ---------------------------------------------------------------------------
# VacationDays (reference slice — keep unchanged)
# ---------------------------------------------------------------------------
@pytest_asyncio.fixture
async def vacation_repo(
connection: aiosqlite.Connection,
) -> SQLiteVacationDaysRepository:
return SQLiteVacationDaysRepository(connection)
@pytest_asyncio.fixture
async def vacation_service(
vacation_repo: SQLiteVacationDaysRepository,
) -> VacationDaysService:
return VacationDaysService(vacation_repo)
# ---------------------------------------------------------------------------
# Auth
# ---------------------------------------------------------------------------
@pytest_asyncio.fixture
async def user_repo(connection: aiosqlite.Connection) -> SQLiteUserRepository:
return SQLiteUserRepository(connection)
@pytest_asyncio.fixture
async def personal_data_repo(
connection: aiosqlite.Connection,
) -> SQLitePersonalDataRepository:
return SQLitePersonalDataRepository(connection)
@pytest_asyncio.fixture
async def auth_service(
user_repo: SQLiteUserRepository,
personal_data_repo: SQLitePersonalDataRepository,
workspace_repo: SQLiteWorkspaceRepository,
) -> AuthService:
return AuthService(user_repo, personal_data_repo, workspace_repo)
# ---------------------------------------------------------------------------
# Workspace
# ---------------------------------------------------------------------------
@pytest_asyncio.fixture
async def workspace_repo(
connection: aiosqlite.Connection,
) -> SQLiteWorkspaceRepository:
return SQLiteWorkspaceRepository(connection)
@pytest_asyncio.fixture
async def invitation_repo(
connection: aiosqlite.Connection,
) -> SQLiteWorkspaceInvitationRepository:
return SQLiteWorkspaceInvitationRepository(connection)
@pytest_asyncio.fixture
async def workspace_service(
workspace_repo: SQLiteWorkspaceRepository,
invitation_repo: SQLiteWorkspaceInvitationRepository,
personal_data_repo: SQLitePersonalDataRepository,
) -> WorkspaceService:
return WorkspaceService(workspace_repo, invitation_repo, personal_data_repo)
# ---------------------------------------------------------------------------
# Shared FK seed — used by any test that needs real workspace + personal_data rows
# ---------------------------------------------------------------------------
@pytest_asyncio.fixture
async def seeded_ids(
workspace_repo: SQLiteWorkspaceRepository,
personal_data_repo: SQLitePersonalDataRepository,
child_repo: SQLiteChildRepository,
) -> SeededIds:
"""Pre-create a workspace + personal_data + child row so FK constraints pass."""
ws = await workspace_repo.create(
Workspace(id=uuid4(), name="Test WS", created_at=datetime.now())
)
pd = await personal_data_repo.create(
PersonalData(
uuid=uuid4(), workspace_id=ws.id, name="Test Parent", created_at=datetime.now()
)
)
child = await child_repo.create(
Child(
id=uuid4(),
workspace_id=ws.id,
name="Test Child",
birth_date=date(2024, 1, 1),
created_at=datetime.now(),
)
)
return SeededIds(workspace_id=ws.id, personal_data_uuid=pd.uuid, child_id=child.id)
# ---------------------------------------------------------------------------
# Child & leave
# ---------------------------------------------------------------------------
@pytest_asyncio.fixture
async def child_repo(connection: aiosqlite.Connection) -> SQLiteChildRepository:
return SQLiteChildRepository(connection)
@pytest_asyncio.fixture
async def parental_leave_repo(
connection: aiosqlite.Connection,
) -> SQLiteParentalLeaveRepository:
return SQLiteParentalLeaveRepository(connection)
@pytest_asyncio.fixture
async def planned_leave_repo(
connection: aiosqlite.Connection,
) -> SQLitePlannedLeaveRepository:
return SQLitePlannedLeaveRepository(connection)
# ---------------------------------------------------------------------------
# LeaveWish
# ---------------------------------------------------------------------------
@pytest_asyncio.fixture
async def leave_wish_repo(
connection: aiosqlite.Connection,
) -> SQLiteLeaveWishRepository:
return SQLiteLeaveWishRepository(connection)
@pytest_asyncio.fixture
async def leave_wish_service(
leave_wish_repo: SQLiteLeaveWishRepository,
) -> LeaveWishService:
return LeaveWishService(leave_wish_repo)
# ---------------------------------------------------------------------------
# DayOverride
# ---------------------------------------------------------------------------
@pytest_asyncio.fixture
async def day_override_repo(
connection: aiosqlite.Connection,
) -> SQLiteDayOverrideRepository:
return SQLiteDayOverrideRepository(connection)
@pytest_asyncio.fixture
async def day_override_service(
day_override_repo: SQLiteDayOverrideRepository,
) -> DayOverrideService:
return DayOverrideService(day_override_repo)
@pytest_asyncio.fixture
async def child_service(
child_repo: SQLiteChildRepository,
) -> ChildService:
return ChildService(child_repo)
@pytest_asyncio.fixture
async def analytics_service(
parental_leave_repo: SQLiteParentalLeaveRepository,
planned_leave_repo: SQLitePlannedLeaveRepository,
child_repo: SQLiteChildRepository,
personal_data_repo: SQLitePersonalDataRepository,
) -> AnalyticsService:
return AnalyticsService(parental_leave_repo, planned_leave_repo, child_repo, personal_data_repo)
@pytest_asyncio.fixture
async def planning_service(
planned_leave_repo: SQLitePlannedLeaveRepository,
analytics_service: AnalyticsService,
parental_leave_repo: SQLiteParentalLeaveRepository,
) -> PlanningService:
return PlanningService(planned_leave_repo, analytics_service, parental_leave_repo)