Files
F-r-ldraledigplannerare/backend/tests/unit/test_day_override_repository.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

193 lines
7.1 KiB
Python

"""Tests for the DayOverride repository + service.
Follows the VacationDays reference pattern: arrange an entity, exercise the
repo/service, assert on the result. See PLANNING task notes / AGENTS.md.
"""
from datetime import date, datetime
from uuid import uuid4
import pytest
from app.entities.day_override import DayOverride
from app.entities.workspace import Workspace
from app.repositories_impl.sqlite.day_override_repository import SQLiteDayOverrideRepository
from app.repositories_impl.sqlite.workspace_repository import SQLiteWorkspaceRepository
from app.services.day_override_service import DayOverrideService
from app.utils.errors import NotFoundError, ValidationError
from tests.conftest import SeededIds
def _make(workspace_id=None, **overrides) -> DayOverride:
base = dict(
id=uuid4(),
workspace_id=workspace_id or uuid4(),
date=date(2027, 12, 24),
is_working_day=True,
note=None,
created_at=datetime.now(),
updated_at=datetime.now(),
)
base.update(overrides)
return DayOverride(**base)
class TestSQLiteDayOverrideRepository:
async def test_create_and_get_by_id(
self,
day_override_repo: SQLiteDayOverrideRepository,
seeded_ids: SeededIds,
) -> None:
entity = _make(workspace_id=seeded_ids.workspace_id)
await day_override_repo.create(entity)
fetched = await day_override_repo.get_by_id(entity.id)
assert fetched is not None
assert fetched.id == entity.id
assert fetched.is_working_day is True
assert fetched.date == date(2027, 12, 24)
async def test_get_by_workspace_date(
self,
day_override_repo: SQLiteDayOverrideRepository,
seeded_ids: SeededIds,
) -> None:
entity = _make(workspace_id=seeded_ids.workspace_id)
await day_override_repo.create(entity)
found = await day_override_repo.get_by_workspace_date(
seeded_ids.workspace_id, date(2027, 12, 24)
)
assert found is not None
assert found.id == entity.id
missing = await day_override_repo.get_by_workspace_date(uuid4(), date(2027, 12, 24))
assert missing is None
async def test_update(
self,
day_override_repo: SQLiteDayOverrideRepository,
seeded_ids: SeededIds,
) -> None:
entity = _make(workspace_id=seeded_ids.workspace_id, is_working_day=True)
await day_override_repo.create(entity)
entity.is_working_day = False
entity.note = "Ledig enligt kollektivavtal"
updated = await day_override_repo.update(entity.id, entity)
assert updated.is_working_day is False
assert updated.note == "Ledig enligt kollektivavtal"
async def test_update_missing_raises(
self, day_override_repo: SQLiteDayOverrideRepository
) -> None:
with pytest.raises(NotFoundError):
await day_override_repo.update(uuid4(), _make())
async def test_delete(
self,
day_override_repo: SQLiteDayOverrideRepository,
seeded_ids: SeededIds,
) -> None:
entity = _make(workspace_id=seeded_ids.workspace_id)
await day_override_repo.create(entity)
await day_override_repo.delete(entity.id)
assert await day_override_repo.get_by_id(entity.id) is None
async def test_delete_missing_raises(
self, day_override_repo: SQLiteDayOverrideRepository
) -> None:
with pytest.raises(NotFoundError):
await day_override_repo.delete(uuid4())
async def test_list_by_workspace_filters_and_orders_by_date(
self,
day_override_repo: SQLiteDayOverrideRepository,
seeded_ids: SeededIds,
workspace_repo: SQLiteWorkspaceRepository,
) -> None:
ws = seeded_ids.workspace_id
ws2 = await workspace_repo.create(
Workspace(id=uuid4(), name="WS2", created_at=datetime.now())
)
await day_override_repo.create(_make(workspace_id=ws, date=date(2027, 12, 31)))
await day_override_repo.create(_make(workspace_id=ws, date=date(2027, 5, 7)))
await day_override_repo.create(_make(workspace_id=ws2.id, date=date(2027, 12, 24)))
rows = await day_override_repo.list_by_workspace(ws)
assert [r.date for r in rows] == [date(2027, 5, 7), date(2027, 12, 31)]
async def test_unique_constraint_per_workspace_date(
self,
day_override_repo: SQLiteDayOverrideRepository,
seeded_ids: SeededIds,
) -> None:
await day_override_repo.create(_make(workspace_id=seeded_ids.workspace_id))
with pytest.raises(Exception): # sqlite3.IntegrityError via aiosqlite
await day_override_repo.create(_make(workspace_id=seeded_ids.workspace_id))
class TestDayOverrideService:
async def test_upsert_creates_then_updates(
self,
day_override_service: DayOverrideService,
seeded_ids: SeededIds,
) -> None:
ws = seeded_ids.workspace_id
created = await day_override_service.upsert(ws, date(2027, 12, 24), True)
assert created.is_working_day is True
assert created.note is None
updated = await day_override_service.upsert(
ws, date(2027, 12, 24), False, note="Ledigt i år"
)
assert updated.id == created.id # same (workspace, date) row
assert updated.is_working_day is False
assert updated.note == "Ledigt i år"
async def test_upsert_rejects_overlong_note(
self, day_override_service: DayOverrideService, seeded_ids: SeededIds
) -> None:
with pytest.raises(ValidationError):
await day_override_service.upsert(
seeded_ids.workspace_id, date(2027, 12, 24), True, note="x" * 281
)
async def test_get_missing_raises(self, day_override_service: DayOverrideService) -> None:
with pytest.raises(NotFoundError):
await day_override_service.get(uuid4())
async def test_list_for_workspace(
self, day_override_service: DayOverrideService, seeded_ids: SeededIds
) -> None:
ws = seeded_ids.workspace_id
await day_override_service.upsert(ws, date(2027, 5, 7), False)
await day_override_service.upsert(ws, date(2027, 12, 24), True)
rows = await day_override_service.list_for_workspace(ws)
assert len(rows) == 2
async def test_delete(
self, day_override_service: DayOverrideService, seeded_ids: SeededIds
) -> None:
created = await day_override_service.upsert(
seeded_ids.workspace_id, date(2027, 12, 24), True
)
await day_override_service.delete(created.id)
with pytest.raises(NotFoundError):
await day_override_service.get(created.id)
async def test_get_effective_map(
self, day_override_service: DayOverrideService, seeded_ids: SeededIds
) -> None:
ws = seeded_ids.workspace_id
await day_override_service.upsert(ws, date(2027, 12, 24), True)
await day_override_service.upsert(ws, date(2027, 2, 2), False)
effective = await day_override_service.get_effective_map(ws)
assert effective == {
date(2027, 12, 24): True,
date(2027, 2, 2): False,
}