From bad9ec2f9c2eccd417b78c42d183b67dd7f61c8d Mon Sep 17 00:00:00 2001 From: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com> Date: Sun, 28 Jun 2026 18:04:15 +0100 Subject: [PATCH] test: localize calendar recurrence helper import (#4944) * test: localize calendar recurrence helper import * test: share calendar route import helper --- tests/helpers/calendar_routes.py | 8 +++++ tests/test_calendar_parse_dt_naive.py | 8 ++--- tests/test_calendar_recurrence.py | 44 +++++++++++++------------- tests/test_calendar_rrule_until_utc.py | 4 +-- tests/test_ics_escape.py | 8 ++--- tests/test_null_owner_gates.py | 24 ++++---------- 6 files changed, 47 insertions(+), 49 deletions(-) create mode 100644 tests/helpers/calendar_routes.py diff --git a/tests/helpers/calendar_routes.py b/tests/helpers/calendar_routes.py new file mode 100644 index 000000000..47c4c0ac3 --- /dev/null +++ b/tests/helpers/calendar_routes.py @@ -0,0 +1,8 @@ +"""Shared imports for calendar route tests.""" + + +def import_calendar_routes(): + """Import the calendar routes module after test stubs are installed.""" + import routes.calendar_routes as cal + + return cal diff --git a/tests/test_calendar_parse_dt_naive.py b/tests/test_calendar_parse_dt_naive.py index b70ea0ba2..25fad2bfd 100644 --- a/tests/test_calendar_parse_dt_naive.py +++ b/tests/test_calendar_parse_dt_naive.py @@ -13,7 +13,7 @@ The fallback now normalizes to UTC and strips tz, exactly like the ISO path. """ import pytest -from tests.test_null_owner_gates import _import_calendar_helpers +from tests.helpers.calendar_routes import import_calendar_routes # Inputs datetime.fromisoformat() rejects (so they hit the dateutil fallback) # but that carry a numeric UTC offset dateutil resolves to tz-aware. @@ -25,7 +25,7 @@ _OFFSET_NONISO = [ @pytest.mark.parametrize("s", _OFFSET_NONISO) def test_parse_dt_dateutil_fallback_returns_naive(s): - cal = _import_calendar_helpers() + cal = import_calendar_routes() d = cal._parse_dt(s) assert d.tzinfo is None, f"{s!r} leaked tz-aware: {d!r}" # +0900 14:00 -> 05:00 UTC, naive. @@ -34,13 +34,13 @@ def test_parse_dt_dateutil_fallback_returns_naive(s): @pytest.mark.parametrize("s", _OFFSET_NONISO) def test_parse_dt_pair_fallback_returns_naive(s): - cal = _import_calendar_helpers() + cal = import_calendar_routes() dt, _is_utc = cal._parse_dt_pair(s) assert dt.tzinfo is None, f"{s!r} leaked tz-aware via _parse_dt_pair: {dt!r}" def test_parse_dt_naive_input_unchanged(): - cal = _import_calendar_helpers() + cal = import_calendar_routes() d = cal._parse_dt("January 5, 2026 14:00") # no offset -> stays as parsed assert d.tzinfo is None assert (d.hour, d.minute) == (14, 0) diff --git a/tests/test_calendar_recurrence.py b/tests/test_calendar_recurrence.py index bc78127ed..6647db332 100644 --- a/tests/test_calendar_recurrence.py +++ b/tests/test_calendar_recurrence.py @@ -1,8 +1,8 @@ """Regression tests for calendar recurrence expansion. Tests _expand_rrule and _resolve_base_uid — imported directly from -routes/calendar_routes using the same stub-friendly import pattern -as test_null_owner_gates.py. No live DB or FastAPI test client needed. +routes/calendar_routes using the shared stub-friendly test helper. +No live DB or FastAPI test client needed. """ from datetime import datetime, timedelta @@ -10,34 +10,34 @@ from types import SimpleNamespace import pytest -from tests.test_null_owner_gates import _import_calendar_helpers +from tests.helpers.calendar_routes import import_calendar_routes # ── _resolve_base_uid ────────────────────────────────────────────────── def test_resolve_base_uid_plain_passthrough(): - cal = _import_calendar_helpers() + cal = import_calendar_routes() assert cal._resolve_base_uid("evt-123") == "evt-123" def test_resolve_base_uid_compound_strips_suffix_date(): - cal = _import_calendar_helpers() + cal = import_calendar_routes() assert cal._resolve_base_uid("evt-123::2026-06-15") == "evt-123" def test_resolve_base_uid_compound_strips_suffix_datetime(): - cal = _import_calendar_helpers() + cal = import_calendar_routes() assert cal._resolve_base_uid("evt-123::2026-06-15T09:00") == "evt-123" def test_resolve_base_uid_rejects_empty(): - cal = _import_calendar_helpers() + cal = import_calendar_routes() with pytest.raises(ValueError, match="empty uid"): cal._resolve_base_uid("") def test_resolve_base_uid_rejects_missing_base(): - cal = _import_calendar_helpers() + cal = import_calendar_routes() with pytest.raises(ValueError, match="malformed compound UID"): cal._resolve_base_uid("::2026-06-15") @@ -73,7 +73,7 @@ def _make_event(**overrides): def test_expand_non_recurring_returns_single(): """Non-recurring events pass through unchanged with series_uid=uid.""" - cal = _import_calendar_helpers() + cal = import_calendar_routes() ev = _make_event(rrule="") results = cal._expand_rrule(ev, datetime(2026, 5, 1), datetime(2026, 7, 1)) @@ -90,7 +90,7 @@ def test_expand_yearly_old_dtstart_later_year_single_occurrence(): This is the explicit regression case from PR review feedback. """ - cal = _import_calendar_helpers() + cal = import_calendar_routes() ev = _make_event( uid="evt-bday-001", summary="Annual Review", @@ -118,7 +118,7 @@ def test_expand_yearly_narrow_window_after_dtstart_returns_one(): """DTSTART=2020, query just two months in 2029 — should return exactly one occurrence (the one that falls in that window). """ - cal = _import_calendar_helpers() + cal = import_calendar_routes() ev = _make_event( uid="evt-ann", dtstart=datetime(2020, 3, 1), @@ -137,7 +137,7 @@ def test_expand_yearly_strict_before_window_returns_empty(): """DTSTART=2020, query a window that ends before the yearly occurrence in that year. Should return zero. """ - cal = _import_calendar_helpers() + cal = import_calendar_routes() ev = _make_event( uid="evt-late", dtstart=datetime(2020, 12, 25), @@ -154,7 +154,7 @@ def test_expand_yearly_strict_after_window_returns_empty(): """DTSTART=2020. Query a window that starts after the occurrence in that year. Should return zero. """ - cal = _import_calendar_helpers() + cal = import_calendar_routes() ev = _make_event( uid="evt-early", dtstart=datetime(2020, 1, 15), @@ -171,7 +171,7 @@ def test_expand_weekly_unique_no_overwrites(): """Multiple occurrences from the same series must have unique UIDs so _allEvents[uid] = ev doesn't overwrite earlier ones. """ - cal = _import_calendar_helpers() + cal = import_calendar_routes() ev = _make_event( uid="evt-wk", dtstart=datetime(2026, 6, 1, 9, 0), @@ -192,7 +192,7 @@ def test_expand_weekly_unique_no_overwrites(): def test_expand_monthly_all_day(): - cal = _import_calendar_helpers() + cal = import_calendar_routes() ev = _make_event( uid="evt-rent", dtstart=datetime(2026, 1, 1), @@ -210,7 +210,7 @@ def test_expand_monthly_all_day(): def test_expand_bad_rrule_graceful(): """Malformed rrule should fall back to returning the base event, but only when the base event overlaps the requested window.""" - cal = _import_calendar_helpers() + cal = import_calendar_routes() ev = _make_event( uid="evt-broken", rrule="FREQ=GARBAGE", @@ -225,7 +225,7 @@ def test_expand_bad_rrule_graceful(): def test_expand_bad_rrule_fallback_rejects_non_overlapping(): """Malformed rrule with a base event outside the requested window must return zero results, not leak the event into an unrelated range.""" - cal = _import_calendar_helpers() + cal = import_calendar_routes() ev = _make_event( uid="evt-old-broken", dtstart=datetime(2020, 1, 1, 9, 0), @@ -243,7 +243,7 @@ def test_expand_bad_rrule_fallback_rejects_non_overlapping(): def test_expand_exclusive_end_boundary(): """An occurrence whose start equals the window end must be excluded. The contract is [start, end), same as the non-recurring SQL filter.""" - cal = _import_calendar_helpers() + cal = import_calendar_routes() ev = _make_event( uid="evt-daily", dtstart=datetime(2026, 6, 1, 9, 0), @@ -260,7 +260,7 @@ def test_expand_exclusive_end_boundary(): def test_expand_multi_day_crossing_range_start(): """A multi-day occurrence that starts before the window but ends inside it must be included (matching non-recurring overlap: dtend > start).""" - cal = _import_calendar_helpers() + cal = import_calendar_routes() ev = _make_event( uid="evt-weekly-multi", summary="Weekend Trip", @@ -285,7 +285,7 @@ def test_expand_multi_day_crossing_range_start(): def test_expand_multi_day_fully_before_window(): """A multi-day occurrence that ends exactly at the window start must be excluded (occ_end <= start).""" - cal = _import_calendar_helpers() + cal = import_calendar_routes() ev = _make_event( uid="evt-multi", dtstart=datetime(2026, 5, 29, 18, 0), @@ -301,7 +301,7 @@ def test_expand_multi_day_fully_before_window(): def test_expand_metadata_inheritance(): """Occurrence dicts must carry the base event's metadata (summary, importance, event_type, color, location).""" - cal = _import_calendar_helpers() + cal = import_calendar_routes() ev = _make_event( uid="evt-meta", summary="Board Meeting", @@ -323,7 +323,7 @@ def test_expand_metadata_inheritance(): def test_expand_daily_rrule_large_window_is_capped_and_marked_truncated(): """Wide recurring windows must not materialize unbounded occurrence lists.""" - cal = _import_calendar_helpers() + cal = import_calendar_routes() ev = _make_event( uid="evt-daily-cap", dtstart=datetime(2020, 1, 1, 9, 0), diff --git a/tests/test_calendar_rrule_until_utc.py b/tests/test_calendar_rrule_until_utc.py index 9aade268a..8ebb8e44b 100644 --- a/tests/test_calendar_rrule_until_utc.py +++ b/tests/test_calendar_rrule_until_utc.py @@ -24,7 +24,7 @@ UNTIL must expand to all of its occurrences. from datetime import datetime from types import SimpleNamespace -from tests.test_null_owner_gates import _import_calendar_helpers +from tests.helpers.calendar_routes import import_calendar_routes _MOCK_CAL = SimpleNamespace(name="Personal", color="#5b8abf") @@ -55,7 +55,7 @@ def _make_event(**overrides): def test_expand_rrule_with_utc_until_keeps_all_occurrences(): """FREQ=DAILY;UNTIL=...Z must expand to every occurrence, not collapse to a single non-recurring event.""" - cal = _import_calendar_helpers() + cal = import_calendar_routes() ev = _make_event(rrule="FREQ=DAILY;UNTIL=20240105T090000Z") results = cal._expand_rrule(ev, datetime(2024, 1, 1), datetime(2024, 1, 10)) diff --git a/tests/test_ics_escape.py b/tests/test_ics_escape.py index e22dee5e2..825b5f30f 100644 --- a/tests/test_ics_escape.py +++ b/tests/test_ics_escape.py @@ -1,9 +1,9 @@ """Tests for iCalendar TEXT escaping in calendar export (RFC 5545 §3.3.11).""" -from tests.test_null_owner_gates import _import_calendar_helpers +from tests.helpers.calendar_routes import import_calendar_routes def _esc(): - return _import_calendar_helpers()._ics_escape + return import_calendar_routes()._ics_escape def test_escapes_comma_and_semicolon(): @@ -26,7 +26,7 @@ def test_empty_and_none_safe(): def test_safe_ics_filename_strips_header_metacharacters(): - safe_filename = _import_calendar_helpers()._safe_ics_filename + safe_filename = import_calendar_routes()._safe_ics_filename assert ( safe_filename('Work\r\nX-Injected: yes";/..\\evil') @@ -35,7 +35,7 @@ def test_safe_ics_filename_strips_header_metacharacters(): def test_safe_ics_filename_falls_back_for_empty_names(): - safe_filename = _import_calendar_helpers()._safe_ics_filename + safe_filename = import_calendar_routes()._safe_ics_filename assert safe_filename("////") == "calendar.ics" assert safe_filename(None) == "calendar.ics" diff --git a/tests/test_null_owner_gates.py b/tests/test_null_owner_gates.py index fee7e8fa0..97e66d007 100644 --- a/tests/test_null_owner_gates.py +++ b/tests/test_null_owner_gates.py @@ -18,6 +18,8 @@ import pytest from types import SimpleNamespace from unittest.mock import MagicMock +from tests.helpers.calendar_routes import import_calendar_routes + # `tests/conftest.py` stubs the heavy optional deps. We additionally # stub `core.database` here because the real module instantiates # SQLAlchemy declarative classes at import-time — which blows up under @@ -64,20 +66,8 @@ from fastapi import HTTPException # calendar._get_or_404_calendar / _get_or_404_event # --------------------------------------------------------------------------- -def _import_calendar_helpers(): - """Import the two private gate helpers without booting the full - calendar router. We patch sys.modules so the module-load side - effects (DB import) don't blow up under the conftest stubs.""" - mod_name = "routes.calendar_routes" - if mod_name in sys.modules: - return sys.modules[mod_name] - # core.database is stubbed by conftest already; the module should - # import cleanly. - return __import__(mod_name, fromlist=["_get_or_404_calendar", "_get_or_404_event"]) - - def test_calendar_gate_rejects_null_owner_for_authenticated_user(): - cal_mod = _import_calendar_helpers() + cal_mod = import_calendar_routes() db = MagicMock() cal = SimpleNamespace(id="c1", owner=None) db.query.return_value.filter.return_value.first.return_value = cal @@ -87,7 +77,7 @@ def test_calendar_gate_rejects_null_owner_for_authenticated_user(): def test_calendar_gate_rejects_cross_owner(): - cal_mod = _import_calendar_helpers() + cal_mod = import_calendar_routes() db = MagicMock() cal = SimpleNamespace(id="c1", owner="bob") db.query.return_value.filter.return_value.first.return_value = cal @@ -97,7 +87,7 @@ def test_calendar_gate_rejects_cross_owner(): def test_calendar_gate_accepts_matching_owner(): - cal_mod = _import_calendar_helpers() + cal_mod = import_calendar_routes() db = MagicMock() cal = SimpleNamespace(id="c1", owner="alice") db.query.return_value.filter.return_value.first.return_value = cal @@ -106,7 +96,7 @@ def test_calendar_gate_accepts_matching_owner(): def test_calendar_event_gate_rejects_null_owner_calendar(): - cal_mod = _import_calendar_helpers() + cal_mod = import_calendar_routes() db = MagicMock() cal = SimpleNamespace(owner=None) ev = SimpleNamespace(uid="e1", calendar=cal) @@ -117,7 +107,7 @@ def test_calendar_event_gate_rejects_null_owner_calendar(): def test_calendar_event_gate_rejects_cross_owner(): - cal_mod = _import_calendar_helpers() + cal_mod = import_calendar_routes() db = MagicMock() cal = SimpleNamespace(owner="bob") ev = SimpleNamespace(uid="e1", calendar=cal)