"""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())