Coverage for app/backend/src/tests/test_event_recurrence.py: 100%
96 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-26 00:40 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-26 00:40 +0000
1from datetime import UTC, date, datetime, timedelta
3from couchers.event_recurrence import (
4 get_future_occurrences,
5 make_daily_rrule,
6 make_every_nth_week_rrule,
7 make_monthly_rrule,
8 schedule_occurrences,
9)
10from tests.fixtures.timewarp import FrozenTimewarp
13def test_make_every_nth_week_rrule_weekly() -> None:
14 start = date(2000, 1, 1)
15 rrule = make_every_nth_week_rrule(start, n=1)
17 occurrences = get_future_occurrences(start, rrule)
18 assert next(occurrences) == start
19 assert next(occurrences) == date(2000, 1, 8)
20 assert next(occurrences) == date(2000, 1, 15)
23def test_make_every_nth_week_rrule_biweekly() -> None:
24 start = date(2000, 1, 1)
25 rrule = make_every_nth_week_rrule(start, n=2)
27 occurrences = get_future_occurrences(start, rrule)
28 assert next(occurrences) == start
29 assert next(occurrences) == date(2000, 1, 15)
30 assert next(occurrences) == date(2000, 1, 29)
33def test_make_daily_rrule() -> None:
34 start = date(2000, 1, 1)
35 rrule = make_daily_rrule(start)
37 occurrences = get_future_occurrences(start, rrule)
38 assert next(occurrences) == start
39 assert next(occurrences) == date(2000, 1, 2)
40 assert next(occurrences) == date(2000, 1, 3)
43def test_make_daily_rrule_with_end_date() -> None:
44 start = date(2000, 1, 1)
45 rrule = make_daily_rrule(start, end_date=date(2000, 1, 2))
47 assert list(get_future_occurrences(start, rrule)) == [start, date(2000, 1, 2)]
50def test_make_monthly_rrule() -> None:
51 start = date(2000, 1, 1)
52 rrule = make_monthly_rrule(start)
54 occurrences = get_future_occurrences(start, rrule)
55 assert next(occurrences) == start
56 assert next(occurrences) == date(2000, 2, 1)
57 assert next(occurrences) == date(2000, 3, 1)
60def test_make_monthly_rrule_with_end_date() -> None:
61 start = date(2000, 1, 1)
62 rrule = make_monthly_rrule(start, end_date=date(2000, 2, 1))
64 assert list(get_future_occurrences(start, rrule)) == [start, date(2000, 2, 1)]
67def test_get_future_occurrences_boundaries() -> None:
68 start = date(2000, 1, 1)
69 rrule = make_every_nth_week_rrule(start, n=1)
71 # from_date a week before the first occurrence: shouldn't return anything from the week prior
72 occurrences = get_future_occurrences(start - timedelta(days=8), rrule)
73 assert next(occurrences) == start
74 assert next(occurrences) == date(2000, 1, 8)
75 assert next(occurrences) == date(2000, 1, 15)
77 # inclusive of from_date exactly on an occurrence
78 occurrences = get_future_occurrences(start, rrule)
79 assert next(occurrences) == start
80 assert next(occurrences) == date(2000, 1, 8)
81 assert next(occurrences) == date(2000, 1, 15)
83 # After the first occurrence
84 occurrences = get_future_occurrences(date(2000, 1, 2), rrule)
85 assert next(occurrences) == date(2000, 1, 8)
86 assert next(occurrences) == date(2000, 1, 15)
89def test_get_future_occurrences_bounded_rule_terminates() -> None:
90 start = date(2000, 1, 1)
91 rrule = make_every_nth_week_rrule(start, n=1, end_date=date(2000, 1, 8))
93 assert list(get_future_occurrences(start, rrule)) == [start, date(2000, 1, 8)]
96def test_get_future_occurrences_past_last_occurrence_is_empty() -> None:
97 start = date(2000, 1, 1)
98 rrule = make_every_nth_week_rrule(start, n=1, end_date=date(2000, 1, 8))
100 assert list(get_future_occurrences(date(2000, 2, 1), rrule)) == []
103def test_schedule_occurrences_frequent_rule_returns_more_than_minimum() -> None:
104 """
105 For frequent events, we should schedule all events within the schedule window,
106 which should exceed the minimum occurrence count (used for infrequent events).
107 """
108 reference_date = date(2000, 1, 1)
109 rrule = make_daily_rrule(reference_date)
111 result = schedule_occurrences(
112 rrule=rrule,
113 schedule_window=timedelta(weeks=2),
114 min_occurrences=2,
115 last_scheduled_date=reference_date - timedelta(days=1),
116 today=reference_date,
117 )
119 assert len(result) > 2
120 assert result[0] == reference_date
121 assert result[-1] <= reference_date + timedelta(weeks=2)
124def test_schedule_occurrences_infrequent_rule_returns_minimum_occurrences() -> None:
125 """
126 For infrequent events, we should schedule a fixed occurrence count.
127 """
128 reference_date = date(2000, 1, 1)
129 # monthly, well outside the 2-week schedule window between occurrences
130 rrule = make_monthly_rrule(reference_date)
132 result = schedule_occurrences(
133 rrule=rrule,
134 schedule_window=timedelta(weeks=2),
135 min_occurrences=2,
136 last_scheduled_date=reference_date - timedelta(days=1),
137 today=reference_date,
138 )
140 assert result == [reference_date, date(2000, 2, 1)]
143def test_schedule_occurrences_short_rule_returns_fewer_than_minimum() -> None:
144 """
145 When the event's recurrence is about to end, we might schedule fewer than
146 the minimum ahead-of-time scheduled occurrences.
147 """
148 reference_date = date(2000, 1, 1)
149 rrule = make_every_nth_week_rrule(reference_date, n=1, end_date=reference_date)
151 result = schedule_occurrences(
152 rrule=rrule,
153 schedule_window=timedelta(weeks=2),
154 min_occurrences=2,
155 last_scheduled_date=reference_date - timedelta(days=1),
156 today=reference_date,
157 )
159 assert result == [reference_date]
162def test_schedule_occurrences_filters_out_already_scheduled() -> None:
163 """
164 We should never schedule an occurrence on a date we previously scheduled.
165 """
167 reference_date = date(2000, 1, 1)
168 rrule = make_every_nth_week_rrule(reference_date, n=1)
170 # last_scheduled_date equal to an occurrence excludes it (strict >)
171 result = schedule_occurrences(
172 rrule=rrule,
173 schedule_window=timedelta(weeks=2),
174 min_occurrences=2,
175 last_scheduled_date=reference_date,
176 today=reference_date,
177 )
178 assert result == [date(2000, 1, 8), date(2000, 1, 15)]
180 # everything already scheduled
181 result = schedule_occurrences(
182 rrule=rrule,
183 schedule_window=timedelta(weeks=2),
184 min_occurrences=2,
185 last_scheduled_date=date(2000, 1, 15),
186 today=reference_date,
187 )
188 assert result == []
191def test_schedule_occurrences_defaults_last_scheduled_date_to_none() -> None:
192 """
193 When nothing has been scheduled yet, nothing should be filtered out.
194 """
195 reference_date = date(2000, 1, 1)
196 rrule = make_every_nth_week_rrule(reference_date, n=1)
198 result = schedule_occurrences(
199 rrule=rrule,
200 schedule_window=timedelta(weeks=2),
201 min_occurrences=2,
202 today=reference_date,
203 )
205 assert result == [reference_date, date(2000, 1, 8), date(2000, 1, 15)]
208def test_schedule_occurrences_defaults_today_to_now(frozen_timewarp: FrozenTimewarp) -> None:
209 reference_date = date(2000, 1, 1)
210 frozen_timewarp.freeze_at(datetime(2000, 1, 1, tzinfo=UTC))
212 rrule = make_every_nth_week_rrule(reference_date, n=1)
213 result = schedule_occurrences(
214 rrule=rrule,
215 schedule_window=timedelta(weeks=2),
216 min_occurrences=2,
217 last_scheduled_date=reference_date - timedelta(days=1),
218 )
220 assert result[0] == reference_date