Merge branch 'vacation-rescue-and-wrap'
Vacation rescue/wrap in allocation_service, new plan models/endpoints, VacationRescueTip, useRescueDayActions, vacationSwaps util, sv+en i18n, Dashboard/Calendar wiring. Conflicts came from main's "day assertions" restructure and the earlier merges; both intents kept: - plan.py: both import sets; main's sync-with-wish handler kept next to the branch's new /rescue-days/dismiss endpoint; HTTP_422_UNPROCESSABLE_CONTENT applied to the branch's code (and a survivor in auth.py). - Accepting a rescue day now goes through POST /api/day-assertions (an ad-hoc 'vacation' assertion, no wish) instead of /plan/apply with an `is_pinned` flag that no longer exists: on main a pin IS a day assertion (see PlanningService.add_planned_day's docstring), and an assertion is what keeps a later sync from retyping the day. useRescueDayActions, its tests, VacationRescueTip's tests and the backend integration flow follow that contract; applyPlan stays as a client for the live endpoint minus the dropped field. The hook also gets the security branch's useLatestRequest guard with an unconditionally cleared busy flag. - Calendar.tsx: `locale` threaded into main's applyWishSyncResult call of buildSyncToastMessage (the branch's new parameter); the branch's swap-visibility toast tests drive the preset path through syncWithWish, which is how main saves a wish now. - types/api.ts: ProposedDayApplyItem kept (used by applyPlan) without isPinned; the Add/UpdatePlannedDayRequest types stay removed as on main. - fixtures/plan.ts: ResourceShortage import dropped (type removed on main). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T4wjgbVL4HAC5Rdaw75VbV
This commit is contained in:
@@ -6,12 +6,13 @@ Follow the `vacation_repo` / `vacation_service` pattern to add fixtures for
|
||||
other entities.
|
||||
"""
|
||||
|
||||
from collections.abc import AsyncIterator
|
||||
from collections.abc import AsyncIterator, Iterator
|
||||
from dataclasses import dataclass
|
||||
from datetime import date, datetime
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
import aiosqlite
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
|
||||
@@ -22,6 +23,7 @@ from app.entities.personal_data import PersonalData
|
||||
from app.entities.user import User
|
||||
from app.entities.workspace import Workspace
|
||||
from app.main import app
|
||||
from app.rate_limit import limiter
|
||||
from app.repositories_impl.sqlite.child_repository import SQLiteChildRepository
|
||||
from app.repositories_impl.sqlite.day_assertion_repository import SQLiteDayAssertionRepository
|
||||
from app.repositories_impl.sqlite.day_override_repository import SQLiteDayOverrideRepository
|
||||
@@ -56,6 +58,51 @@ from app.services.vacation_days_service import VacationDaysService
|
||||
from app.services.workspace_service import WorkspaceService
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Rate limiting — OFF by default for the whole test session.
|
||||
#
|
||||
# `limiter` (app/rate_limit.py) keys on remote address via slowapi's
|
||||
# `get_remote_address`, which assumes one IP == one client. Every test in a
|
||||
# pytest run shares the same `app.main.app` instance and drives it through
|
||||
# `httpx.ASGITransport`, so the whole session looks like one client to the
|
||||
# limiter — the same problem `frontend/playwright.config.ts` already works
|
||||
# around for the E2E suite via `APP_RATE_LIMIT_ENABLED=false`. Left on here,
|
||||
# `tests/integration/test_user_journey.py` alone makes exactly ten
|
||||
# `/api/auth/register` calls, spending the entire 10/minute budget, so any
|
||||
# other integration test that registers a user 429s if it happens to run
|
||||
# afterwards in the same process — a suite that was only green by accident
|
||||
# of file ordering.
|
||||
#
|
||||
# We flip the already-constructed `limiter` object directly rather than
|
||||
# `settings.rate_limit_enabled`: `Limiter(..., enabled=...)` reads that
|
||||
# setting once at import time in app/rate_limit.py, so mutating `settings`
|
||||
# afterwards would no longer reach the singleton the app actually calls.
|
||||
# Production is unaffected — `settings.rate_limit_enabled` still defaults to
|
||||
# True, so a deployed process still builds its limiter enabled; only this
|
||||
# pytest process ever runs the line below.
|
||||
limiter.enabled = False
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def rate_limiting_enabled() -> Iterator[None]:
|
||||
"""Opt-in fixture for a test that specifically exercises rate limiting.
|
||||
|
||||
Turns the shared limiter back on and clears its in-memory counters
|
||||
first — in case some earlier test's traffic ever slipped through while
|
||||
it was "enabled" — then restores both the disabled default and a clean
|
||||
counter set afterwards. `limiter` is a process-global in-memory
|
||||
limiter, so without this a test that enables it would leak state into
|
||||
whichever test runs next.
|
||||
"""
|
||||
limiter.reset()
|
||||
limiter.enabled = True
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
limiter.enabled = False
|
||||
limiter.reset()
|
||||
|
||||
|
||||
@dataclass
|
||||
class SeededIds:
|
||||
workspace_id: UUID
|
||||
|
||||
Reference in New Issue
Block a user