test: localize calendar recurrence helper import (#4944)

* test: localize calendar recurrence helper import

* test: share calendar route import helper
This commit is contained in:
Alexandre Teixeira
2026-06-28 18:04:15 +01:00
committed by GitHub
parent 927b1f7ecf
commit bad9ec2f9c
6 changed files with 47 additions and 49 deletions
+8
View File
@@ -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
+4 -4
View File
@@ -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)
+22 -22
View File
@@ -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),
+2 -2
View File
@@ -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))
+4 -4
View File
@@ -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"
+7 -17
View File
@@ -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)