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

357 lines
12 KiB
Python

"""Tests for the Child service.
Demonstrates service-layer testing patterns: test business logic, validation,
and error handling. Aim for >=90% coverage on new code (ARCHITECTURE.md §10).
"""
from datetime import date
from uuid import uuid4
import pytest
from app.services.child_service import ChildService
from app.utils.errors import NotFoundError, ValidationError
from tests.conftest import SeededIds
class TestChildService:
"""Test ChildService business logic."""
async def test_create_with_valid_inputs(
self,
child_service: ChildService,
seeded_ids: SeededIds,
) -> None:
"""Create a child with valid inputs."""
result = await child_service.create(
workspace_id=seeded_ids.workspace_id,
name="Alice",
birth_date=date(2024, 6, 15),
preschool_start_date=date(2027, 8, 1),
)
assert result.name == "Alice"
assert result.birth_date == date(2024, 6, 15)
assert result.preschool_start_date == date(2027, 8, 1)
async def test_create_without_preschool_start_date(
self,
child_service: ChildService,
seeded_ids: SeededIds,
) -> None:
"""Create a child without preschool start date (optional)."""
result = await child_service.create(
workspace_id=seeded_ids.workspace_id,
name="Bob",
birth_date=date(2024, 3, 10),
preschool_start_date=None,
)
assert result.name == "Bob"
assert result.preschool_start_date is None
async def test_create_with_preschool_before_birth_raises(
self,
child_service: ChildService,
seeded_ids: SeededIds,
) -> None:
"""Creating with preschool_start_date <= birth_date raises ValidationError."""
with pytest.raises(ValidationError, match="preschool_start_date must be after birth_date"):
await child_service.create(
workspace_id=seeded_ids.workspace_id,
name="Charlie",
birth_date=date(2024, 6, 15),
preschool_start_date=date(2024, 6, 15),
)
async def test_create_with_preschool_same_date_raises(
self,
child_service: ChildService,
seeded_ids: SeededIds,
) -> None:
"""Creating with preschool_start_date == birth_date raises ValidationError."""
with pytest.raises(ValidationError):
await child_service.create(
workspace_id=seeded_ids.workspace_id,
name="Charlie",
birth_date=date(2024, 6, 15),
preschool_start_date=date(2024, 6, 14),
)
async def test_get_existing_child(
self,
child_service: ChildService,
seeded_ids: SeededIds,
) -> None:
"""Get an existing child by ID."""
created = await child_service.create(
workspace_id=seeded_ids.workspace_id,
name="Diana",
birth_date=date(2025, 1, 1),
)
fetched = await child_service.get(created.id)
assert fetched.id == created.id
assert fetched.name == "Diana"
async def test_get_missing_child_raises(
self,
child_service: ChildService,
) -> None:
"""Getting a missing child raises NotFoundError."""
with pytest.raises(NotFoundError, match="not found"):
await child_service.get(uuid4())
async def test_list_by_workspace(
self,
child_service: ChildService,
seeded_ids: SeededIds,
) -> None:
"""List all children in a workspace."""
ws = seeded_ids.workspace_id
c1 = await child_service.create(
workspace_id=ws,
name="Child1",
birth_date=date(2024, 1, 1),
)
c2 = await child_service.create(
workspace_id=ws,
name="Child2",
birth_date=date(2024, 6, 1),
)
result = await child_service.list_by_workspace(ws)
assert len(result) >= 2
ids = {c.id for c in result}
assert c1.id in ids
assert c2.id in ids
async def test_list_by_workspace_empty(
self,
child_service: ChildService,
) -> None:
"""Listing children for workspace with none returns empty list."""
result = await child_service.list_by_workspace(uuid4())
assert result == []
async def test_update_name_only(
self,
child_service: ChildService,
seeded_ids: SeededIds,
) -> None:
"""Update only the name."""
created = await child_service.create(
workspace_id=seeded_ids.workspace_id,
name="Old Name",
birth_date=date(2024, 6, 15),
)
updated = await child_service.update(
child_id=created.id,
name="New Name",
)
assert updated.name == "New Name"
assert updated.birth_date == date(2024, 6, 15)
async def test_update_birth_date_only(
self,
child_service: ChildService,
seeded_ids: SeededIds,
) -> None:
"""Update only the birth date."""
created = await child_service.create(
workspace_id=seeded_ids.workspace_id,
name="Emma",
birth_date=date(2024, 6, 15),
)
updated = await child_service.update(
child_id=created.id,
birth_date=date(2024, 7, 20),
)
assert updated.name == "Emma"
assert updated.birth_date == date(2024, 7, 20)
async def test_update_add_preschool_date(
self,
child_service: ChildService,
seeded_ids: SeededIds,
) -> None:
"""Add preschool start date to a child that didn't have one."""
created = await child_service.create(
workspace_id=seeded_ids.workspace_id,
name="Frank",
birth_date=date(2024, 6, 15),
preschool_start_date=None,
)
updated = await child_service.update(
child_id=created.id,
preschool_start_date=date(2027, 8, 1),
)
assert updated.preschool_start_date == date(2027, 8, 1)
async def test_update_with_invalid_preschool_date_raises(
self,
child_service: ChildService,
seeded_ids: SeededIds,
) -> None:
"""Updating with invalid preschool date raises ValidationError."""
created = await child_service.create(
workspace_id=seeded_ids.workspace_id,
name="Grace",
birth_date=date(2024, 6, 15),
)
with pytest.raises(ValidationError):
await child_service.update(
child_id=created.id,
preschool_start_date=date(2024, 6, 15),
)
async def test_update_missing_child_raises(
self,
child_service: ChildService,
) -> None:
"""Updating a missing child raises NotFoundError."""
with pytest.raises(NotFoundError):
await child_service.update(
child_id=uuid4(),
name="Henry",
)
async def test_delete_existing_child(
self,
child_service: ChildService,
seeded_ids: SeededIds,
) -> None:
"""Delete an existing child."""
created = await child_service.create(
workspace_id=seeded_ids.workspace_id,
name="Iris",
birth_date=date(2024, 6, 15),
)
await child_service.delete(created.id)
with pytest.raises(NotFoundError):
await child_service.get(created.id)
async def test_delete_missing_child_raises(
self,
child_service: ChildService,
) -> None:
"""Deleting a missing child raises NotFoundError."""
with pytest.raises(NotFoundError):
await child_service.delete(uuid4())
async def test_create_defaults_preschool_planning_days_to_six(
self,
child_service: ChildService,
seeded_ids: SeededIds,
) -> None:
"""Creating a child without specifying preschool_planning_days_per_year defaults to 6."""
result = await child_service.create(
workspace_id=seeded_ids.workspace_id,
name="Karl",
birth_date=date(2024, 6, 15),
)
assert result.preschool_planning_days_per_year == 6
async def test_create_with_valid_preschool_planning_days(
self,
child_service: ChildService,
seeded_ids: SeededIds,
) -> None:
"""Creating a child with a valid custom preschool_planning_days_per_year succeeds."""
result = await child_service.create(
workspace_id=seeded_ids.workspace_id,
name="Lena",
birth_date=date(2024, 6, 15),
preschool_planning_days_per_year=12,
)
assert result.preschool_planning_days_per_year == 12
async def test_create_with_invalid_preschool_planning_days_raises(
self,
child_service: ChildService,
seeded_ids: SeededIds,
) -> None:
"""Creating a child with an out-of-range preschool_planning_days_per_year raises."""
with pytest.raises(ValidationError, match="preschool_planning_days_per_year"):
await child_service.create(
workspace_id=seeded_ids.workspace_id,
name="Mona",
birth_date=date(2024, 6, 15),
preschool_planning_days_per_year=31,
)
async def test_create_with_negative_preschool_planning_days_raises(
self,
child_service: ChildService,
seeded_ids: SeededIds,
) -> None:
"""A negative preschool_planning_days_per_year is out of range."""
with pytest.raises(ValidationError, match="preschool_planning_days_per_year"):
await child_service.create(
workspace_id=seeded_ids.workspace_id,
name="Nils",
birth_date=date(2024, 6, 15),
preschool_planning_days_per_year=-1,
)
async def test_update_preschool_planning_days_valid(
self,
child_service: ChildService,
seeded_ids: SeededIds,
) -> None:
"""Updating preschool_planning_days_per_year to a valid value succeeds."""
created = await child_service.create(
workspace_id=seeded_ids.workspace_id,
name="Olle",
birth_date=date(2024, 6, 15),
)
updated = await child_service.update(
child_id=created.id,
preschool_planning_days_per_year=0,
)
assert updated.preschool_planning_days_per_year == 0
async def test_update_preschool_planning_days_invalid_raises(
self,
child_service: ChildService,
seeded_ids: SeededIds,
) -> None:
"""Updating preschool_planning_days_per_year out of range raises ValidationError."""
created = await child_service.create(
workspace_id=seeded_ids.workspace_id,
name="Petra",
birth_date=date(2024, 6, 15),
)
with pytest.raises(ValidationError, match="preschool_planning_days_per_year"):
await child_service.update(
child_id=created.id,
preschool_planning_days_per_year=100,
)
async def test_update_multiple_fields(
self,
child_service: ChildService,
seeded_ids: SeededIds,
) -> None:
"""Update multiple fields at once."""
created = await child_service.create(
workspace_id=seeded_ids.workspace_id,
name="Jack",
birth_date=date(2024, 6, 15),
preschool_start_date=date(2027, 8, 1),
)
updated = await child_service.update(
child_id=created.id,
name="Jake",
birth_date=date(2024, 7, 1),
preschool_start_date=date(2027, 9, 1),
)
assert updated.name == "Jake"
assert updated.birth_date == date(2024, 7, 1)
assert updated.preschool_start_date == date(2027, 9, 1)