76b1e712b9
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
891 lines
31 KiB
Python
891 lines
31 KiB
Python
"""Tests for the LeaveWish repository and service.
|
|
|
|
Demonstrates the testing pattern for entity-specific repos and services.
|
|
Aim for >=90% coverage on new code (ARCHITECTURE.md §10).
|
|
"""
|
|
|
|
from datetime import date, datetime
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
|
|
from app.entities.leave_wish import LeaveWish
|
|
from app.entities.workspace import Workspace
|
|
from app.repositories_impl.sqlite.leave_wish_repository import SQLiteLeaveWishRepository
|
|
from app.repositories_impl.sqlite.workspace_repository import SQLiteWorkspaceRepository
|
|
from app.services.leave_wish_service import LeaveWishService
|
|
from app.utils.errors import NotFoundError, ValidationError
|
|
from tests.conftest import SeededIds
|
|
|
|
|
|
def _make(workspace_id=None, **overrides) -> LeaveWish:
|
|
"""Helper to create a test LeaveWish entity."""
|
|
base = dict(
|
|
id=uuid4(),
|
|
workspace_id=workspace_id or uuid4(),
|
|
child_id=None,
|
|
name="Sommar",
|
|
window_start_month=6,
|
|
window_start_day=15,
|
|
window_end_month=8,
|
|
window_end_day=15,
|
|
target_weeks=6,
|
|
recurrence="yearly",
|
|
first_year=2027,
|
|
last_year=None,
|
|
priority=1,
|
|
parent_mode="either",
|
|
parent_personal_data_uuid=None,
|
|
goal_override=None,
|
|
created_at=datetime.now(),
|
|
updated_at=datetime.now(),
|
|
)
|
|
base.update(overrides)
|
|
return LeaveWish(**base)
|
|
|
|
|
|
class TestLeaveWishWindowLogic:
|
|
"""Direct tests of LeaveWish's recurring-window helpers.
|
|
|
|
These are the rules the allocator (Phase B) depends on most — especially
|
|
year-wrapping, which is the easiest thing in this feature to get backwards.
|
|
"""
|
|
|
|
def test_window_wraps_false_when_end_after_start(self) -> None:
|
|
wish = _make(
|
|
window_start_month=6, window_start_day=15, window_end_month=8, window_end_day=15
|
|
)
|
|
assert wish.window_wraps() is False
|
|
|
|
def test_window_wraps_true_when_end_before_start(self) -> None:
|
|
# Jullov: Dec 20 -> Jan 7 (end sorts before start -> wraps)
|
|
wish = _make(
|
|
window_start_month=12, window_start_day=20, window_end_month=1, window_end_day=7
|
|
)
|
|
assert wish.window_wraps() is True
|
|
|
|
def test_window_wraps_false_when_start_equals_end(self) -> None:
|
|
wish = _make(window_start_month=6, window_start_day=1, window_end_month=6, window_end_day=1)
|
|
assert wish.window_wraps() is False
|
|
|
|
def test_window_for_year_no_wrap_stays_in_same_year(self) -> None:
|
|
wish = _make(
|
|
window_start_month=6, window_start_day=15, window_end_month=8, window_end_day=15
|
|
)
|
|
start, end = wish.window_for_year(2027)
|
|
assert start == date(2027, 6, 15)
|
|
assert end == date(2027, 8, 15)
|
|
|
|
def test_window_for_year_wraps_end_into_next_year(self) -> None:
|
|
wish = _make(
|
|
window_start_month=12, window_start_day=20, window_end_month=1, window_end_day=7
|
|
)
|
|
start, end = wish.window_for_year(2027)
|
|
assert start == date(2027, 12, 20)
|
|
assert end == date(2028, 1, 7)
|
|
|
|
def test_applies_to_year_before_first_year_is_false(self) -> None:
|
|
wish = _make(recurrence="yearly", first_year=2027, last_year=None)
|
|
assert wish.applies_to_year(2026) is False
|
|
|
|
def test_applies_to_year_once_matches_only_first_year(self) -> None:
|
|
wish = _make(recurrence="once", first_year=2027, last_year=2027)
|
|
assert wish.applies_to_year(2027) is True
|
|
assert wish.applies_to_year(2028) is False
|
|
|
|
def test_applies_to_year_yearly_with_no_last_year_recurs_indefinitely(self) -> None:
|
|
wish = _make(recurrence="yearly", first_year=2027, last_year=None)
|
|
assert wish.applies_to_year(2027) is True
|
|
assert wish.applies_to_year(2050) is True
|
|
|
|
def test_applies_to_year_yearly_stops_after_last_year(self) -> None:
|
|
wish = _make(recurrence="yearly", first_year=2027, last_year=2029)
|
|
assert wish.applies_to_year(2029) is True
|
|
assert wish.applies_to_year(2030) is False
|
|
|
|
|
|
class TestSQLiteLeaveWishRepository:
|
|
"""Test LeaveWishRepository SQLite implementation."""
|
|
|
|
async def test_create_and_get_by_id(
|
|
self,
|
|
leave_wish_repo: SQLiteLeaveWishRepository,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
entity = _make(workspace_id=seeded_ids.workspace_id)
|
|
created = await leave_wish_repo.create(entity)
|
|
|
|
fetched = await leave_wish_repo.get_by_id(created.id)
|
|
assert fetched is not None
|
|
assert fetched.id == created.id
|
|
assert fetched.name == "Sommar"
|
|
assert fetched.target_weeks == 6
|
|
assert fetched.recurrence == "yearly"
|
|
assert fetched.parent_mode == "either"
|
|
assert fetched.child_id is None
|
|
assert fetched.parent_personal_data_uuid is None
|
|
assert fetched.goal_override is None
|
|
|
|
async def test_get_by_id_missing(
|
|
self,
|
|
leave_wish_repo: SQLiteLeaveWishRepository,
|
|
) -> None:
|
|
result = await leave_wish_repo.get_by_id(uuid4())
|
|
assert result is None
|
|
|
|
async def test_create_preserves_all_fields(
|
|
self,
|
|
leave_wish_repo: SQLiteLeaveWishRepository,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
entity = _make(
|
|
workspace_id=seeded_ids.workspace_id,
|
|
child_id=seeded_ids.child_id,
|
|
name="Jullov",
|
|
window_start_month=12,
|
|
window_start_day=20,
|
|
window_end_month=1,
|
|
window_end_day=7,
|
|
target_weeks=4,
|
|
recurrence="yearly",
|
|
first_year=2027,
|
|
last_year=2031,
|
|
priority=2,
|
|
parent_mode="specific",
|
|
parent_personal_data_uuid=seeded_ids.personal_data_uuid,
|
|
goal_override="max_time",
|
|
)
|
|
created = await leave_wish_repo.create(entity)
|
|
|
|
assert created.name == "Jullov"
|
|
assert created.child_id == seeded_ids.child_id
|
|
assert created.window_start_month == 12
|
|
assert created.window_start_day == 20
|
|
assert created.window_end_month == 1
|
|
assert created.window_end_day == 7
|
|
assert created.target_weeks == 4
|
|
assert created.recurrence == "yearly"
|
|
assert created.first_year == 2027
|
|
assert created.last_year == 2031
|
|
assert created.priority == 2
|
|
assert created.parent_mode == "specific"
|
|
assert created.parent_personal_data_uuid == seeded_ids.personal_data_uuid
|
|
assert created.goal_override == "max_time"
|
|
|
|
# Round-trip through the DB, not just the in-memory object.
|
|
fetched = await leave_wish_repo.get_by_id(created.id)
|
|
assert fetched is not None
|
|
assert fetched.child_id == seeded_ids.child_id
|
|
assert fetched.last_year == 2031
|
|
assert fetched.parent_personal_data_uuid == seeded_ids.personal_data_uuid
|
|
assert fetched.goal_override == "max_time"
|
|
|
|
async def test_get_by_workspace(
|
|
self,
|
|
leave_wish_repo: SQLiteLeaveWishRepository,
|
|
seeded_ids: SeededIds,
|
|
workspace_repo: SQLiteWorkspaceRepository,
|
|
) -> None:
|
|
ws1 = seeded_ids.workspace_id
|
|
ws2 = await workspace_repo.create(
|
|
Workspace(id=uuid4(), name="WS2", created_at=datetime.now())
|
|
)
|
|
|
|
w1 = await leave_wish_repo.create(_make(workspace_id=ws1, name="W1"))
|
|
w2 = await leave_wish_repo.create(_make(workspace_id=ws1, name="W2"))
|
|
w3 = await leave_wish_repo.create(_make(workspace_id=ws2.id, name="W3"))
|
|
|
|
rows = await leave_wish_repo.get_by_workspace(ws1)
|
|
assert len(rows) == 2
|
|
ids = {r.id for r in rows}
|
|
assert w1.id in ids
|
|
assert w2.id in ids
|
|
assert w3.id not in ids
|
|
|
|
async def test_get_by_workspace_empty(
|
|
self,
|
|
leave_wish_repo: SQLiteLeaveWishRepository,
|
|
) -> None:
|
|
result = await leave_wish_repo.get_by_workspace(uuid4())
|
|
assert result == []
|
|
|
|
async def test_update(
|
|
self,
|
|
leave_wish_repo: SQLiteLeaveWishRepository,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
entity = _make(workspace_id=seeded_ids.workspace_id, name="Sommar", target_weeks=6)
|
|
created = await leave_wish_repo.create(entity)
|
|
|
|
created.name = "Sommarlov"
|
|
created.target_weeks = 8
|
|
created.priority = 3
|
|
updated = await leave_wish_repo.update(created.id, created)
|
|
|
|
assert updated.name == "Sommarlov"
|
|
assert updated.target_weeks == 8
|
|
assert updated.priority == 3
|
|
assert updated.id == created.id
|
|
|
|
async def test_update_missing_raises(
|
|
self,
|
|
leave_wish_repo: SQLiteLeaveWishRepository,
|
|
) -> None:
|
|
with pytest.raises(NotFoundError):
|
|
await leave_wish_repo.update(uuid4(), _make())
|
|
|
|
async def test_delete(
|
|
self,
|
|
leave_wish_repo: SQLiteLeaveWishRepository,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
entity = _make(workspace_id=seeded_ids.workspace_id)
|
|
created = await leave_wish_repo.create(entity)
|
|
|
|
await leave_wish_repo.delete(created.id)
|
|
|
|
result = await leave_wish_repo.get_by_id(created.id)
|
|
assert result is None
|
|
|
|
async def test_delete_missing_raises(
|
|
self,
|
|
leave_wish_repo: SQLiteLeaveWishRepository,
|
|
) -> None:
|
|
with pytest.raises(NotFoundError):
|
|
await leave_wish_repo.delete(uuid4())
|
|
|
|
async def test_list(
|
|
self,
|
|
leave_wish_repo: SQLiteLeaveWishRepository,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
ws = seeded_ids.workspace_id
|
|
w1 = await leave_wish_repo.create(_make(workspace_id=ws, name="W1"))
|
|
w2 = await leave_wish_repo.create(_make(workspace_id=ws, name="W2"))
|
|
|
|
rows = await leave_wish_repo.list()
|
|
assert len(rows) >= 2
|
|
ids = {r.id for r in rows}
|
|
assert w1.id in ids
|
|
assert w2.id in ids
|
|
|
|
async def test_list_filters_by_child_id(
|
|
self,
|
|
leave_wish_repo: SQLiteLeaveWishRepository,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
ws = seeded_ids.workspace_id
|
|
tied = await leave_wish_repo.create(
|
|
_make(workspace_id=ws, child_id=seeded_ids.child_id, name="Tied")
|
|
)
|
|
family_wide = await leave_wish_repo.create(
|
|
_make(workspace_id=ws, child_id=None, name="FamilyWide")
|
|
)
|
|
|
|
rows = await leave_wish_repo.list(child_id=seeded_ids.child_id)
|
|
ids = {r.id for r in rows}
|
|
assert tied.id in ids
|
|
assert family_wide.id not in ids
|
|
|
|
|
|
class TestLeaveWishService:
|
|
"""Test LeaveWishService business logic (validation)."""
|
|
|
|
async def test_create_with_valid_fields(
|
|
self,
|
|
leave_wish_service: LeaveWishService,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
result = await leave_wish_service.create(
|
|
workspace_id=seeded_ids.workspace_id,
|
|
name="Sommar",
|
|
window_start_month=6,
|
|
window_start_day=15,
|
|
window_end_month=8,
|
|
window_end_day=15,
|
|
target_weeks=6,
|
|
first_year=2027,
|
|
)
|
|
assert result.name == "Sommar"
|
|
assert result.target_weeks == 6
|
|
assert result.recurrence == "once" # default
|
|
assert result.parent_mode == "either" # default
|
|
|
|
@pytest.mark.parametrize("recurrence", ["once", "yearly"])
|
|
async def test_create_accepts_valid_recurrence(
|
|
self,
|
|
leave_wish_service: LeaveWishService,
|
|
seeded_ids: SeededIds,
|
|
recurrence: str,
|
|
) -> None:
|
|
result = await leave_wish_service.create(
|
|
workspace_id=seeded_ids.workspace_id,
|
|
name="Wish",
|
|
window_start_month=1,
|
|
window_start_day=1,
|
|
window_end_month=1,
|
|
window_end_day=10,
|
|
target_weeks=1,
|
|
first_year=2027,
|
|
recurrence=recurrence,
|
|
)
|
|
assert result.recurrence == recurrence
|
|
|
|
async def test_create_rejects_invalid_recurrence(
|
|
self,
|
|
leave_wish_service: LeaveWishService,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
with pytest.raises(ValidationError, match="recurrence"):
|
|
await leave_wish_service.create(
|
|
workspace_id=seeded_ids.workspace_id,
|
|
name="Wish",
|
|
window_start_month=1,
|
|
window_start_day=1,
|
|
window_end_month=1,
|
|
window_end_day=10,
|
|
target_weeks=1,
|
|
first_year=2027,
|
|
recurrence="monthly",
|
|
)
|
|
|
|
async def test_create_rejects_invalid_parent_mode(
|
|
self,
|
|
leave_wish_service: LeaveWishService,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
with pytest.raises(ValidationError, match="parent_mode"):
|
|
await leave_wish_service.create(
|
|
workspace_id=seeded_ids.workspace_id,
|
|
name="Wish",
|
|
window_start_month=1,
|
|
window_start_day=1,
|
|
window_end_month=1,
|
|
window_end_day=10,
|
|
target_weeks=1,
|
|
first_year=2027,
|
|
parent_mode="everyone",
|
|
)
|
|
|
|
async def test_create_specific_parent_mode_requires_uuid(
|
|
self,
|
|
leave_wish_service: LeaveWishService,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
with pytest.raises(ValidationError, match="parent_personal_data_uuid"):
|
|
await leave_wish_service.create(
|
|
workspace_id=seeded_ids.workspace_id,
|
|
name="Wish",
|
|
window_start_month=1,
|
|
window_start_day=1,
|
|
window_end_month=1,
|
|
window_end_day=10,
|
|
target_weeks=1,
|
|
first_year=2027,
|
|
parent_mode="specific",
|
|
parent_personal_data_uuid=None,
|
|
)
|
|
|
|
async def test_create_specific_parent_mode_with_uuid_succeeds(
|
|
self,
|
|
leave_wish_service: LeaveWishService,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
result = await leave_wish_service.create(
|
|
workspace_id=seeded_ids.workspace_id,
|
|
name="Wish",
|
|
window_start_month=1,
|
|
window_start_day=1,
|
|
window_end_month=1,
|
|
window_end_day=10,
|
|
target_weeks=1,
|
|
first_year=2027,
|
|
parent_mode="specific",
|
|
parent_personal_data_uuid=seeded_ids.personal_data_uuid,
|
|
)
|
|
assert result.parent_mode == "specific"
|
|
assert result.parent_personal_data_uuid == seeded_ids.personal_data_uuid
|
|
|
|
async def test_create_rejects_parent_uuid_when_not_specific(
|
|
self,
|
|
leave_wish_service: LeaveWishService,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
with pytest.raises(ValidationError, match="parent_personal_data_uuid"):
|
|
await leave_wish_service.create(
|
|
workspace_id=seeded_ids.workspace_id,
|
|
name="Wish",
|
|
window_start_month=1,
|
|
window_start_day=1,
|
|
window_end_month=1,
|
|
window_end_day=10,
|
|
target_weeks=1,
|
|
first_year=2027,
|
|
parent_mode="either",
|
|
parent_personal_data_uuid=seeded_ids.personal_data_uuid,
|
|
)
|
|
|
|
@pytest.mark.parametrize("goal_override", ["max_time", "max_money", "balanced", None])
|
|
async def test_create_accepts_valid_goal_override(
|
|
self,
|
|
leave_wish_service: LeaveWishService,
|
|
seeded_ids: SeededIds,
|
|
goal_override: str | None,
|
|
) -> None:
|
|
result = await leave_wish_service.create(
|
|
workspace_id=seeded_ids.workspace_id,
|
|
name="Wish",
|
|
window_start_month=1,
|
|
window_start_day=1,
|
|
window_end_month=1,
|
|
window_end_day=10,
|
|
target_weeks=1,
|
|
first_year=2027,
|
|
goal_override=goal_override,
|
|
)
|
|
assert result.goal_override == goal_override
|
|
|
|
async def test_create_rejects_invalid_goal_override(
|
|
self,
|
|
leave_wish_service: LeaveWishService,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
with pytest.raises(ValidationError, match="goal_override"):
|
|
await leave_wish_service.create(
|
|
workspace_id=seeded_ids.workspace_id,
|
|
name="Wish",
|
|
window_start_month=1,
|
|
window_start_day=1,
|
|
window_end_month=1,
|
|
window_end_day=10,
|
|
target_weeks=1,
|
|
first_year=2027,
|
|
goal_override="minimize_stress",
|
|
)
|
|
|
|
# -- window_mode (§10.1) ------------------------------------------------
|
|
|
|
async def test_create_defaults_to_fixed_window_mode(
|
|
self,
|
|
leave_wish_service: LeaveWishService,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
result = await leave_wish_service.create(
|
|
workspace_id=seeded_ids.workspace_id,
|
|
name="Wish",
|
|
window_start_month=1,
|
|
window_start_day=1,
|
|
window_end_month=1,
|
|
window_end_day=10,
|
|
target_weeks=1,
|
|
first_year=2027,
|
|
)
|
|
assert result.window_mode == "fixed"
|
|
|
|
async def test_create_rejects_invalid_window_mode(
|
|
self,
|
|
leave_wish_service: LeaveWishService,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
with pytest.raises(ValidationError, match="window_mode"):
|
|
await leave_wish_service.create(
|
|
workspace_id=seeded_ids.workspace_id,
|
|
name="Wish",
|
|
window_start_month=1,
|
|
window_start_day=1,
|
|
window_end_month=1,
|
|
window_end_day=10,
|
|
target_weeks=1,
|
|
first_year=2027,
|
|
window_mode="whenever",
|
|
)
|
|
|
|
async def test_create_accepts_flexible_window_mode_with_wide_enough_bound(
|
|
self,
|
|
leave_wish_service: LeaveWishService,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
# Bound = 2027-03-01 -> 2027-05-31 (92 days), target_weeks=1 (7 days).
|
|
result = await leave_wish_service.create(
|
|
workspace_id=seeded_ids.workspace_id,
|
|
name="Vårlov",
|
|
window_start_month=3,
|
|
window_start_day=1,
|
|
window_end_month=5,
|
|
window_end_day=31,
|
|
target_weeks=1,
|
|
first_year=2027,
|
|
window_mode="flexible",
|
|
)
|
|
assert result.window_mode == "flexible"
|
|
|
|
async def test_create_rejects_flexible_wish_whose_bound_is_shorter_than_target_weeks(
|
|
self,
|
|
leave_wish_service: LeaveWishService,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
# Bound = 2027-03-01 -> 2027-03-10 (10 days), target_weeks=2 (14
|
|
# days) -- a legitimate future shortfall must not be silently
|
|
# accepted; this is a user-input error (§10.1).
|
|
with pytest.raises(ValidationError, match="flexible"):
|
|
await leave_wish_service.create(
|
|
workspace_id=seeded_ids.workspace_id,
|
|
name="Vårlov",
|
|
window_start_month=3,
|
|
window_start_day=1,
|
|
window_end_month=3,
|
|
window_end_day=10,
|
|
target_weeks=2,
|
|
first_year=2027,
|
|
window_mode="flexible",
|
|
)
|
|
|
|
async def test_update_can_switch_window_mode_to_flexible(
|
|
self,
|
|
leave_wish_service: LeaveWishService,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
entity = _make(
|
|
workspace_id=seeded_ids.workspace_id,
|
|
window_start_month=3,
|
|
window_start_day=1,
|
|
window_end_month=5,
|
|
window_end_day=31,
|
|
target_weeks=1,
|
|
)
|
|
created = await leave_wish_service._repo.create(entity)
|
|
assert created.window_mode == "fixed"
|
|
|
|
updated = await leave_wish_service.update(created.id, window_mode="flexible")
|
|
assert updated.window_mode == "flexible"
|
|
|
|
async def test_update_to_flexible_still_validates_bound_length(
|
|
self,
|
|
leave_wish_service: LeaveWishService,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
entity = _make(
|
|
workspace_id=seeded_ids.workspace_id,
|
|
window_start_month=3,
|
|
window_start_day=1,
|
|
window_end_month=3,
|
|
window_end_day=10, # 10-day bound
|
|
target_weeks=2, # needs 14 days
|
|
)
|
|
created = await leave_wish_service._repo.create(entity)
|
|
|
|
with pytest.raises(ValidationError, match="flexible"):
|
|
await leave_wish_service.update(created.id, window_mode="flexible")
|
|
|
|
@pytest.mark.parametrize(
|
|
"month,day",
|
|
[(0, 15), (13, 15), (6, 0), (6, 32), (2, 30), (4, 31)],
|
|
)
|
|
async def test_create_rejects_invalid_month_day(
|
|
self,
|
|
leave_wish_service: LeaveWishService,
|
|
seeded_ids: SeededIds,
|
|
month: int,
|
|
day: int,
|
|
) -> None:
|
|
with pytest.raises(ValidationError):
|
|
await leave_wish_service.create(
|
|
workspace_id=seeded_ids.workspace_id,
|
|
name="Wish",
|
|
window_start_month=month,
|
|
window_start_day=day,
|
|
window_end_month=6,
|
|
window_end_day=20,
|
|
target_weeks=1,
|
|
first_year=2027,
|
|
)
|
|
|
|
async def test_create_accepts_leap_day(
|
|
self,
|
|
leave_wish_service: LeaveWishService,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
result = await leave_wish_service.create(
|
|
workspace_id=seeded_ids.workspace_id,
|
|
name="Wish",
|
|
window_start_month=2,
|
|
window_start_day=29,
|
|
window_end_month=3,
|
|
window_end_day=1,
|
|
target_weeks=1,
|
|
first_year=2027,
|
|
)
|
|
assert result.window_start_day == 29
|
|
|
|
async def test_create_rejects_zero_target_weeks(
|
|
self,
|
|
leave_wish_service: LeaveWishService,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
with pytest.raises(ValidationError, match="target_weeks"):
|
|
await leave_wish_service.create(
|
|
workspace_id=seeded_ids.workspace_id,
|
|
name="Wish",
|
|
window_start_month=1,
|
|
window_start_day=1,
|
|
window_end_month=1,
|
|
window_end_day=10,
|
|
target_weeks=0,
|
|
first_year=2027,
|
|
)
|
|
|
|
async def test_create_rejects_last_year_before_first_year(
|
|
self,
|
|
leave_wish_service: LeaveWishService,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
with pytest.raises(ValidationError, match="last_year"):
|
|
await leave_wish_service.create(
|
|
workspace_id=seeded_ids.workspace_id,
|
|
name="Wish",
|
|
window_start_month=1,
|
|
window_start_day=1,
|
|
window_end_month=1,
|
|
window_end_day=10,
|
|
target_weeks=1,
|
|
first_year=2030,
|
|
last_year=2029,
|
|
)
|
|
|
|
async def test_create_allows_last_year_equal_first_year(
|
|
self,
|
|
leave_wish_service: LeaveWishService,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
result = await leave_wish_service.create(
|
|
workspace_id=seeded_ids.workspace_id,
|
|
name="Wish",
|
|
window_start_month=1,
|
|
window_start_day=1,
|
|
window_end_month=1,
|
|
window_end_day=10,
|
|
target_weeks=1,
|
|
first_year=2027,
|
|
last_year=2027,
|
|
)
|
|
assert result.last_year == 2027
|
|
|
|
async def test_create_allows_null_last_year(
|
|
self,
|
|
leave_wish_service: LeaveWishService,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
result = await leave_wish_service.create(
|
|
workspace_id=seeded_ids.workspace_id,
|
|
name="Wish",
|
|
window_start_month=1,
|
|
window_start_day=1,
|
|
window_end_month=1,
|
|
window_end_day=10,
|
|
target_weeks=1,
|
|
first_year=2027,
|
|
last_year=None,
|
|
)
|
|
assert result.last_year is None
|
|
|
|
async def test_get(
|
|
self,
|
|
leave_wish_service: LeaveWishService,
|
|
leave_wish_repo: SQLiteLeaveWishRepository,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
entity = _make(workspace_id=seeded_ids.workspace_id)
|
|
created = await leave_wish_repo.create(entity)
|
|
|
|
fetched = await leave_wish_service.get(created.id)
|
|
assert fetched.id == created.id
|
|
assert fetched.name == "Sommar"
|
|
|
|
async def test_get_missing_raises(
|
|
self,
|
|
leave_wish_service: LeaveWishService,
|
|
) -> None:
|
|
with pytest.raises(NotFoundError):
|
|
await leave_wish_service.get(uuid4())
|
|
|
|
async def test_list_for_workspace(
|
|
self,
|
|
leave_wish_service: LeaveWishService,
|
|
leave_wish_repo: SQLiteLeaveWishRepository,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
ws = seeded_ids.workspace_id
|
|
w1 = await leave_wish_repo.create(_make(workspace_id=ws, name="W1"))
|
|
w2 = await leave_wish_repo.create(_make(workspace_id=ws, name="W2"))
|
|
|
|
result = await leave_wish_service.list_for_workspace(ws)
|
|
assert len(result) == 2
|
|
ids = {r.id for r in result}
|
|
assert w1.id in ids
|
|
assert w2.id in ids
|
|
|
|
async def test_update_partial_only_touches_given_fields(
|
|
self,
|
|
leave_wish_service: LeaveWishService,
|
|
leave_wish_repo: SQLiteLeaveWishRepository,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
entity = _make(workspace_id=seeded_ids.workspace_id, name="Sommar", target_weeks=6)
|
|
created = await leave_wish_repo.create(entity)
|
|
|
|
result = await leave_wish_service.update(
|
|
leave_wish_id=created.id,
|
|
name="Sommarlov",
|
|
)
|
|
assert result.name == "Sommarlov"
|
|
assert result.target_weeks == 6 # untouched
|
|
assert result.window_start_month == 6 # untouched
|
|
|
|
async def test_update_can_set_all_optional_fields(
|
|
self,
|
|
leave_wish_service: LeaveWishService,
|
|
leave_wish_repo: SQLiteLeaveWishRepository,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
"""Every non-clear-flag optional field, set to a real value in one call."""
|
|
entity = _make(workspace_id=seeded_ids.workspace_id, recurrence="once", last_year=None)
|
|
created = await leave_wish_repo.create(entity)
|
|
|
|
result = await leave_wish_service.update(
|
|
leave_wish_id=created.id,
|
|
child_id=seeded_ids.child_id,
|
|
window_start_month=3,
|
|
window_start_day=10,
|
|
window_end_month=4,
|
|
window_end_day=20,
|
|
target_weeks=9,
|
|
recurrence="yearly",
|
|
first_year=2028,
|
|
last_year=2030,
|
|
priority=5,
|
|
parent_mode="specific",
|
|
parent_personal_data_uuid=seeded_ids.personal_data_uuid,
|
|
goal_override="balanced",
|
|
)
|
|
|
|
assert result.child_id == seeded_ids.child_id
|
|
assert result.window_start_month == 3
|
|
assert result.window_start_day == 10
|
|
assert result.window_end_month == 4
|
|
assert result.window_end_day == 20
|
|
assert result.target_weeks == 9
|
|
assert result.recurrence == "yearly"
|
|
assert result.first_year == 2028
|
|
assert result.last_year == 2030
|
|
assert result.priority == 5
|
|
assert result.parent_mode == "specific"
|
|
assert result.parent_personal_data_uuid == seeded_ids.personal_data_uuid
|
|
assert result.goal_override == "balanced"
|
|
|
|
async def test_update_rejects_invalid_values(
|
|
self,
|
|
leave_wish_service: LeaveWishService,
|
|
leave_wish_repo: SQLiteLeaveWishRepository,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
entity = _make(workspace_id=seeded_ids.workspace_id)
|
|
created = await leave_wish_repo.create(entity)
|
|
|
|
with pytest.raises(ValidationError):
|
|
await leave_wish_service.update(leave_wish_id=created.id, target_weeks=0)
|
|
|
|
async def test_update_can_clear_goal_override(
|
|
self,
|
|
leave_wish_service: LeaveWishService,
|
|
leave_wish_repo: SQLiteLeaveWishRepository,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
entity = _make(workspace_id=seeded_ids.workspace_id, goal_override="max_time")
|
|
created = await leave_wish_repo.create(entity)
|
|
assert created.goal_override == "max_time"
|
|
|
|
result = await leave_wish_service.update(
|
|
leave_wish_id=created.id,
|
|
_clear_goal_override=True,
|
|
)
|
|
assert result.goal_override is None
|
|
|
|
async def test_update_can_clear_last_year(
|
|
self,
|
|
leave_wish_service: LeaveWishService,
|
|
leave_wish_repo: SQLiteLeaveWishRepository,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
entity = _make(workspace_id=seeded_ids.workspace_id, last_year=2030)
|
|
created = await leave_wish_repo.create(entity)
|
|
|
|
result = await leave_wish_service.update(
|
|
leave_wish_id=created.id,
|
|
_clear_last_year=True,
|
|
)
|
|
assert result.last_year is None
|
|
|
|
async def test_update_can_clear_child_id(
|
|
self,
|
|
leave_wish_service: LeaveWishService,
|
|
leave_wish_repo: SQLiteLeaveWishRepository,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
entity = _make(workspace_id=seeded_ids.workspace_id, child_id=seeded_ids.child_id)
|
|
created = await leave_wish_repo.create(entity)
|
|
|
|
result = await leave_wish_service.update(
|
|
leave_wish_id=created.id,
|
|
_clear_child_id=True,
|
|
)
|
|
assert result.child_id is None
|
|
|
|
async def test_update_can_clear_parent_personal_data_uuid(
|
|
self,
|
|
leave_wish_service: LeaveWishService,
|
|
leave_wish_repo: SQLiteLeaveWishRepository,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
entity = _make(
|
|
workspace_id=seeded_ids.workspace_id,
|
|
parent_mode="specific",
|
|
parent_personal_data_uuid=seeded_ids.personal_data_uuid,
|
|
)
|
|
created = await leave_wish_repo.create(entity)
|
|
|
|
result = await leave_wish_service.update(
|
|
leave_wish_id=created.id,
|
|
parent_mode="either",
|
|
_clear_parent_personal_data_uuid=True,
|
|
)
|
|
assert result.parent_mode == "either"
|
|
assert result.parent_personal_data_uuid is None
|
|
|
|
async def test_update_missing_raises(
|
|
self,
|
|
leave_wish_service: LeaveWishService,
|
|
) -> None:
|
|
with pytest.raises(NotFoundError):
|
|
await leave_wish_service.update(leave_wish_id=uuid4(), name="Name")
|
|
|
|
async def test_delete(
|
|
self,
|
|
leave_wish_service: LeaveWishService,
|
|
leave_wish_repo: SQLiteLeaveWishRepository,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
entity = _make(workspace_id=seeded_ids.workspace_id)
|
|
created = await leave_wish_repo.create(entity)
|
|
|
|
await leave_wish_service.delete(created.id)
|
|
|
|
with pytest.raises(NotFoundError):
|
|
await leave_wish_service.get(created.id)
|
|
|
|
async def test_delete_missing_raises(
|
|
self,
|
|
leave_wish_service: LeaveWishService,
|
|
) -> None:
|
|
with pytest.raises(NotFoundError):
|
|
await leave_wish_service.delete(uuid4())
|