Files
F-r-ldraledigplannerare/backend/tests/unit/test_leave_wish_repository.py
T
jacobh2 dc4c0a5edd Show preset wish names in the current UI language
Preset wishes were created with `name: t(preset.labelKey)`, so the
translated string was persisted as the wish's name. A wish created while
the UI was Swedish stayed "Jul" in English forever -- the Swedish text
was the data, so no display-layer change could fix it.

Wish names are user-editable free text, so `name` cannot simply be
translated at render time. Migration 007 adds a nullable
`leave_wish.name_message_key` recording where a name came from: preset
wishes store the i18n key alongside the translated literal, and the UI
renders the key when present.

The key is cleared server-side the moment `name` actually changes,
compared against the stored value rather than trusting the caller, so no
client can forget to clear it and silently revert a user's own name to a
preset label.

An unknown key (a stale row, a renamed key) falls back to the stored
literal via an isTranslationKey guard, so a bad key can never crash the
page or leak a raw key to the user. Rows predating this column keep
displaying their stored name.

_FULL_SCHEMA in db/connection.py is updated in step with the migration.

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

1082 lines
38 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_and_get_round_trip_name_message_key(
self,
leave_wish_repo: SQLiteLeaveWishRepository,
seeded_ids: SeededIds,
) -> None:
entity = _make(
workspace_id=seeded_ids.workspace_id,
name="Jul",
name_message_key="wishEditor.seasonJul",
)
created = await leave_wish_repo.create(entity)
assert created.name_message_key == "wishEditor.seasonJul"
fetched = await leave_wish_repo.get_by_id(created.id)
assert fetched is not None
assert fetched.name_message_key == "wishEditor.seasonJul"
async def test_create_without_name_message_key_defaults_to_none(
self,
leave_wish_repo: SQLiteLeaveWishRepository,
seeded_ids: SeededIds,
) -> None:
"""Existing-row simulation: a wish created before this field existed
(or a free-typed custom name) has no key and must read back `None`,
not a synthesized or empty-string value.
"""
entity = _make(workspace_id=seeded_ids.workspace_id, name="Custom trip")
created = await leave_wish_repo.create(entity)
assert created.name_message_key is None
fetched = await leave_wish_repo.get_by_id(created.id)
assert fetched is not None
assert fetched.name_message_key is None
async def test_update_round_trips_name_message_key(
self,
leave_wish_repo: SQLiteLeaveWishRepository,
seeded_ids: SeededIds,
) -> None:
entity = _make(workspace_id=seeded_ids.workspace_id, name="Jul", name_message_key=None)
created = await leave_wish_repo.create(entity)
created.name_message_key = "wishEditor.seasonJul"
updated = await leave_wish_repo.update(created.id, created)
assert updated.name_message_key == "wishEditor.seasonJul"
fetched = await leave_wish_repo.get_by_id(created.id)
assert fetched is not None
assert fetched.name_message_key == "wishEditor.seasonJul"
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
async def test_list_rejects_unsupported_filter(
self,
leave_wish_repo: SQLiteLeaveWishRepository,
seeded_ids: SeededIds,
) -> None:
"""Passing an unsupported filter (like a typo) must raise ValueError, not return all rows."""
with pytest.raises(ValueError, match="parent_personal_data_uuid"):
await leave_wish_repo.list(parent_personal_data_uuid=seeded_ids.personal_data_uuid)
# Verify the error message lists what filters ARE supported
with pytest.raises(ValueError, match="child_id.*workspace_id"):
await leave_wish_repo.list(parent_personal_data_uuid=seeded_ids.personal_data_uuid)
async def test_list_with_supported_filter_still_works(
self,
leave_wish_repo: SQLiteLeaveWishRepository,
seeded_ids: SeededIds,
) -> None:
"""Verify filter validation doesn't break existing filtering behavior."""
ws = seeded_ids.workspace_id
created_1 = await leave_wish_repo.create(_make(workspace_id=ws, name="W1"))
created_2 = await leave_wish_repo.create(_make(workspace_id=ws, name="W2"))
rows = await leave_wish_repo.list(workspace_id=ws)
assert len(rows) >= 2
ids = {r.id for r in rows}
assert created_1.id in ids
assert created_2.id in ids
async def test_list_rejects_multiple_unsupported_filters(
self,
leave_wish_repo: SQLiteLeaveWishRepository,
seeded_ids: SeededIds,
) -> None:
"""Error message should list all unsupported filters."""
with pytest.raises(ValueError, match="parent_personal_data_uuid.*typo_filter"):
await leave_wish_repo.list(
parent_personal_data_uuid=seeded_ids.personal_data_uuid,
typo_filter="value",
)
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
# -- name_message_key (fixes the "Jul"/"Höstlov" always-Swedish bug) ---
async def test_create_passes_through_name_message_key(
self,
leave_wish_service: LeaveWishService,
seeded_ids: SeededIds,
) -> None:
result = await leave_wish_service.create(
workspace_id=seeded_ids.workspace_id,
name="Jul",
name_message_key="wishEditor.seasonJul",
window_start_month=12,
window_start_day=1,
window_end_month=1,
window_end_day=31,
target_weeks=4,
first_year=2027,
)
assert result.name_message_key == "wishEditor.seasonJul"
async def test_update_without_changing_name_preserves_name_message_key(
self,
leave_wish_service: LeaveWishService,
leave_wish_repo: SQLiteLeaveWishRepository,
seeded_ids: SeededIds,
) -> None:
entity = _make(
workspace_id=seeded_ids.workspace_id,
name="Jul",
name_message_key="wishEditor.seasonJul",
)
created = await leave_wish_repo.create(entity)
result = await leave_wish_service.update(leave_wish_id=created.id, target_weeks=5)
assert result.name_message_key == "wishEditor.seasonJul"
assert result.name == "Jul"
async def test_update_that_resends_the_same_name_preserves_name_message_key(
self,
leave_wish_service: LeaveWishService,
leave_wish_repo: SQLiteLeaveWishRepository,
seeded_ids: SeededIds,
) -> None:
"""The auto-save flow resends the unchanged `name` on every edit —
only an *actual* change to the text may clear the key.
"""
entity = _make(
workspace_id=seeded_ids.workspace_id,
name="Jul",
name_message_key="wishEditor.seasonJul",
)
created = await leave_wish_repo.create(entity)
result = await leave_wish_service.update(leave_wish_id=created.id, name="Jul")
assert result.name_message_key == "wishEditor.seasonJul"
async def test_update_renaming_clears_name_message_key_even_without_being_told_to(
self,
leave_wish_service: LeaveWishService,
leave_wish_repo: SQLiteLeaveWishRepository,
seeded_ids: SeededIds,
) -> None:
"""Backend enforcement, not caller-trust: renaming clears the key
even if the caller never mentions `name_message_key` at all — this
is the critical guard against a user's custom name silently
reverting to a stale preset label.
"""
entity = _make(
workspace_id=seeded_ids.workspace_id,
name="Jul",
name_message_key="wishEditor.seasonJul",
)
created = await leave_wish_repo.create(entity)
result = await leave_wish_service.update(leave_wish_id=created.id, name="Vinterresa")
assert result.name == "Vinterresa"
assert result.name_message_key is None
async def test_update_can_explicitly_clear_name_message_key(
self,
leave_wish_service: LeaveWishService,
leave_wish_repo: SQLiteLeaveWishRepository,
seeded_ids: SeededIds,
) -> None:
entity = _make(
workspace_id=seeded_ids.workspace_id,
name="Jul",
name_message_key="wishEditor.seasonJul",
)
created = await leave_wish_repo.create(entity)
result = await leave_wish_service.update(
leave_wish_id=created.id, _clear_name_message_key=True
)
assert result.name_message_key is None
assert result.name == "Jul" # unrelated to the clear
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())