svcforge: reference implementation
ci / lint (push) Successful in 1m19s
ci / unit (push) Failing after 1m2s
ci / integration (push) Has been skipped
ci / types (push) Successful in 1m37s
ci / security (push) Failing after 38s
ci / dockerfile (push) Successful in 14s
ci / image (api) (push) Has been skipped
ci / image (reconciler) (push) Has been skipped
ci / image (worker) (push) Has been skipped
ci / bump (push) Has been skipped
ci / lint (push) Successful in 1m19s
ci / unit (push) Failing after 1m2s
ci / integration (push) Has been skipped
ci / types (push) Successful in 1m37s
ci / security (push) Failing after 38s
ci / dockerfile (push) Successful in 14s
ci / image (api) (push) Has been skipped
ci / image (reconciler) (push) Has been skipped
ci / image (worker) (push) Has been skipped
ci / bump (push) Has been skipped
Complete working build of the system learn-python/ teaches. 164 tests, mypy --strict clean, domain coverage 99%.
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
"""Unit tests for maintenance windows. Pure domain: no DB, no clock, no mocks.
|
||||
|
||||
`now` is a parameter everywhere in `windows.py`, which is why none of these tests
|
||||
monkeypatch `datetime.now` — there is nothing to patch. That is the point of the design.
|
||||
"""
|
||||
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import pytest
|
||||
|
||||
from svcforge_core.domain.windows import (
|
||||
BadWindow,
|
||||
MaintenanceWindow,
|
||||
next_window_open,
|
||||
parse_window,
|
||||
schedule_upgrade_at,
|
||||
)
|
||||
|
||||
HCM = MaintenanceWindow("0 3 * * 0", "Asia/Ho_Chi_Minh") # 03:00 every Sunday, Vietnam time
|
||||
|
||||
|
||||
def test_next_window_open_with_naive_now_raises_value_error() -> None:
|
||||
"""The one bug this module exists to prevent, caught at the boundary.
|
||||
|
||||
A naive datetime does not raise when you build it; it raises when you compare it,
|
||||
which is inside a worker at 03:00. mypy sees `datetime` either way.
|
||||
"""
|
||||
with pytest.raises(ValueError, match="aware"):
|
||||
next_window_open(HCM, datetime(2026, 7, 18, 20, 0)) # naive on purpose
|
||||
|
||||
|
||||
def test_next_window_open_returns_the_hcm_sunday_expressed_in_utc() -> None:
|
||||
"""Sunday 03:00 in Ho Chi Minh (UTC+7, no DST) is Saturday 20:00 UTC.
|
||||
|
||||
`now` here IS that instant, and the answer is that instant: the window is open right
|
||||
now, so the upgrade runs now. Strictly-greater semantics would push it a full week.
|
||||
"""
|
||||
now = datetime(2026, 7, 18, 20, 0, tzinfo=UTC)
|
||||
assert now.weekday() == 5 # a Saturday
|
||||
|
||||
opens = next_window_open(HCM, now)
|
||||
|
||||
assert opens.tzinfo is UTC
|
||||
assert opens == datetime(2026, 7, 18, 20, 0, tzinfo=UTC)
|
||||
assert opens.astimezone(ZoneInfo("Asia/Ho_Chi_Minh")) == datetime(
|
||||
2026, 7, 19, 3, 0, tzinfo=ZoneInfo("Asia/Ho_Chi_Minh")
|
||||
)
|
||||
|
||||
|
||||
def test_next_window_open_rolls_to_next_week_once_the_window_has_passed() -> None:
|
||||
"""A second past the open and you wait for the next one. Guards the -1s inclusivity trick."""
|
||||
opens = next_window_open(HCM, datetime(2026, 7, 18, 20, 0, 1, tzinfo=UTC))
|
||||
|
||||
assert opens == datetime(2026, 7, 25, 20, 0, tzinfo=UTC)
|
||||
assert opens.tzinfo is UTC
|
||||
|
||||
|
||||
def test_schedule_upgrade_at_with_security_returns_now_exactly() -> None:
|
||||
"""A CVE with a public exploit does not wait until Sunday."""
|
||||
now = datetime(2026, 7, 18, 20, 0, tzinfo=UTC)
|
||||
|
||||
assert schedule_upgrade_at(HCM, security=True, now=now) == now
|
||||
|
||||
|
||||
def test_schedule_upgrade_at_without_window_returns_now() -> None:
|
||||
"""maintenance_window is null -> upgrade any time."""
|
||||
now = datetime(2026, 7, 15, 9, 30, tzinfo=UTC)
|
||||
|
||||
assert schedule_upgrade_at(None, security=False, now=now) == now
|
||||
assert next_window_open(None, now).tzinfo is UTC
|
||||
|
||||
|
||||
def test_window_across_spring_forward_returns_one_aware_instant() -> None:
|
||||
"""DST spring-forward, asserting croniter's REAL behaviour rather than trusting docs.
|
||||
|
||||
On 2026-03-08 America/New_York jumps 02:00 EST -> 03:00 EDT, so a `30 2 * * *` window
|
||||
has no 02:30 that day. Observed: croniter does not skip the day and does not raise —
|
||||
it CLAMPS to the transition instant, yielding 03:00:00-04:00 (not 03:30). The window
|
||||
opens half an hour "late" in local terms, exactly once, and the following days resume
|
||||
at 02:30 EDT. One instant, aware, and the caller never sees a nonexistent local time.
|
||||
"""
|
||||
window = MaintenanceWindow("30 2 * * *", "America/New_York")
|
||||
now = datetime(2026, 3, 7, 17, 0, tzinfo=UTC) # Sat midday in New York, before the jump
|
||||
|
||||
opens = next_window_open(window, now)
|
||||
|
||||
assert opens.tzinfo is UTC
|
||||
assert opens == datetime(2026, 3, 8, 7, 0, tzinfo=UTC) # == 03:00 EDT, the clamp
|
||||
|
||||
local = opens.astimezone(ZoneInfo("America/New_York"))
|
||||
assert (local.hour, local.minute) == (3, 0)
|
||||
assert local.utcoffset() == timedelta(hours=-4) # EDT: the jump has happened
|
||||
|
||||
# The day after, the window is back where the tenant expects it.
|
||||
after = next_window_open(window, opens + timedelta(seconds=1))
|
||||
assert after == datetime(2026, 3, 9, 6, 30, tzinfo=UTC) # 02:30 EDT
|
||||
|
||||
|
||||
def test_window_across_fall_back_returns_the_first_of_the_two_local_times() -> None:
|
||||
"""Fall-back makes 01:30 happen twice. Observed: croniter yields BOTH, EDT then EST.
|
||||
|
||||
next_window_open returns the earlier one (fold=0, -04:00). Not a bug to fix here: a
|
||||
window that opens twice on one night is what the tenant's cron literally asked for.
|
||||
"""
|
||||
window = MaintenanceWindow("30 1 * * *", "America/New_York")
|
||||
now = datetime(2026, 10, 31, 16, 0, tzinfo=UTC)
|
||||
|
||||
first = next_window_open(window, now)
|
||||
second = next_window_open(window, first + timedelta(seconds=1))
|
||||
|
||||
assert first == datetime(2026, 11, 1, 5, 30, tzinfo=UTC) # 01:30 EDT
|
||||
assert second == datetime(2026, 11, 1, 6, 30, tzinfo=UTC) # 01:30 EST, one hour later
|
||||
assert first.tzinfo is UTC and second.tzinfo is UTC
|
||||
|
||||
|
||||
def test_parse_window_bad_cron_raises_bad_window() -> None:
|
||||
with pytest.raises(BadWindow, match="cron"):
|
||||
parse_window("not a cron|Asia/Ho_Chi_Minh")
|
||||
|
||||
|
||||
def test_parse_window_roundtrips_a_valid_spec() -> None:
|
||||
assert parse_window("0 3 * * 0|Asia/Ho_Chi_Minh") == HCM
|
||||
|
||||
|
||||
def test_parse_window_none_and_blank_mean_any_time() -> None:
|
||||
assert parse_window(None) is None
|
||||
assert parse_window(" ") is None
|
||||
|
||||
|
||||
def test_parse_window_unknown_zone_raises_bad_window() -> None:
|
||||
with pytest.raises(BadWindow, match="IANA"):
|
||||
parse_window("0 3 * * 0|Mars/Olympus_Mons")
|
||||
|
||||
|
||||
def test_parse_window_without_separator_raises_bad_window() -> None:
|
||||
with pytest.raises(BadWindow, match="CRON"):
|
||||
parse_window("0 3 * * 0")
|
||||
|
||||
|
||||
def test_parse_window_six_field_cron_raises_bad_window() -> None:
|
||||
"""croniter's is_valid() accepts a 6-field (seconds) form; the column is 5-field."""
|
||||
with pytest.raises(BadWindow, match="exactly 5 fields"):
|
||||
parse_window("0 0 3 * * 0|Asia/Ho_Chi_Minh")
|
||||
Reference in New Issue
Block a user