68d982bb94
Repository.list(**filters) let each implementation pick out the filter names it understood and silently ignore the rest, so a typo or an unsupported filter returned every row in the table instead of an error. This was hit for real: LeaveWishRepository.list(parent_personal_data_uuid =...) returns every wish in the database, because that implementation only recognises workspace_id and child_id. A guard written on top of it would have passed while checking nothing. All ten implementations now validate against their supported set and raise ValueError naming the offending key. ValueError rather than a domain error because passing a filter that does not exist is a programming bug to fix, not a runtime condition to handle -- it must not become an HTTP 4xx. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXMhc9nT6sNz1SL4j8dznL
932 lines
32 KiB
Python
932 lines
32 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
|
|
|
|
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
|
|
|
|
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())
|