d06c8c8449
Applied batches were validated against the remaining föräldrapenning tiers but never against semester. That held only while the allocator was the sole writer, since its ledger spends vacation against a per-year budget. The calendar is about to let a user force any day to semester and pin it, which writes straight past that ledger, so nothing would have stopped forty pinned vacation days against a twenty-five day entitlement. The check mirrors the allocator's own derivation rather than restating it: available days are total plus days saved in, and a year with no entered row falls back to the statutory entitlement instead of reading as zero. The semesterlagen floor stays out of it, being a bound on what may be carried forward rather than a cap on what may be spent. Vacation is budgeted per calendar year, so a period crossing New Year is charged to both.
311 lines
9.9 KiB
Python
311 lines
9.9 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.allocation_service import AllocationInputLoader, PlanningEngineService
|
|
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,
|
|
user_repo: SQLiteUserRepository,
|
|
leave_wish_repo: SQLiteLeaveWishRepository,
|
|
planned_leave_repo: SQLitePlannedLeaveRepository,
|
|
parental_leave_repo: SQLiteParentalLeaveRepository,
|
|
vacation_repo: SQLiteVacationDaysRepository,
|
|
) -> WorkspaceService:
|
|
return WorkspaceService(
|
|
workspace_repo,
|
|
invitation_repo,
|
|
personal_data_repo,
|
|
user_repo,
|
|
leave_wish_repo,
|
|
planned_leave_repo,
|
|
parental_leave_repo,
|
|
vacation_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,
|
|
workspace_repo: SQLiteWorkspaceRepository,
|
|
) -> AnalyticsService:
|
|
return AnalyticsService(
|
|
parental_leave_repo, planned_leave_repo, child_repo, personal_data_repo, workspace_repo
|
|
)
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def allocation_input_loader(
|
|
workspace_repo: SQLiteWorkspaceRepository,
|
|
child_repo: SQLiteChildRepository,
|
|
personal_data_repo: SQLitePersonalDataRepository,
|
|
parental_leave_repo: SQLiteParentalLeaveRepository,
|
|
vacation_repo: SQLiteVacationDaysRepository,
|
|
planned_leave_repo: SQLitePlannedLeaveRepository,
|
|
leave_wish_repo: SQLiteLeaveWishRepository,
|
|
day_override_repo: SQLiteDayOverrideRepository,
|
|
) -> AllocationInputLoader:
|
|
return AllocationInputLoader(
|
|
workspace_repo=workspace_repo,
|
|
child_repo=child_repo,
|
|
personal_data_repo=personal_data_repo,
|
|
parental_leave_repo=parental_leave_repo,
|
|
vacation_repo=vacation_repo,
|
|
planned_leave_repo=planned_leave_repo,
|
|
leave_wish_repo=leave_wish_repo,
|
|
day_override_repo=day_override_repo,
|
|
)
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def planning_engine_service(
|
|
allocation_input_loader: AllocationInputLoader,
|
|
) -> PlanningEngineService:
|
|
return PlanningEngineService(allocation_input_loader)
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def planning_service(
|
|
planned_leave_repo: SQLitePlannedLeaveRepository,
|
|
analytics_service: AnalyticsService,
|
|
parental_leave_repo: SQLiteParentalLeaveRepository,
|
|
vacation_repo: SQLiteVacationDaysRepository,
|
|
planning_engine_service: PlanningEngineService,
|
|
) -> PlanningService:
|
|
return PlanningService(
|
|
planned_leave_repo,
|
|
analytics_service,
|
|
parental_leave_repo,
|
|
vacation_repo,
|
|
planning_engine_service,
|
|
)
|