"""Tests for the tracking-config write-time guards. Covers the cross-field rule that an *enabled* Immich dispatch slot (periodic / scheduled / memory) must carry at least one fire time — otherwise the config saves but the scheduler creates zero cron jobs and the slot is silently dead. """ from __future__ import annotations import pytest from fastapi import HTTPException from notify_bridge_server.api.tracking_configs import ( _normalize_time_fields, _validate_enabled_have_times, ) from notify_bridge_server.database.models import TrackingConfig class TestNormalizeTimeFields: def test_normalizes_each_field_in_place(self): values = { "periodic_times": "18:30, 09:00", "scheduled_times": "9:0", "memory_times": "", } _normalize_time_fields(values) assert values["periodic_times"] == "09:00,18:30" assert values["scheduled_times"] == "09:00" assert values["memory_times"] == "" def test_ignores_absent_fields(self): values = {"name": "x"} _normalize_time_fields(values) # no KeyError, no-op assert values == {"name": "x"} def test_malformed_raises_422(self): with pytest.raises(HTTPException) as exc: _normalize_time_fields({"scheduled_times": "25:00"}) assert exc.value.status_code == 422 assert "scheduled_times" in str(exc.value.detail) class TestValidateEnabledHaveTimes: # --- create path (existing=None) --- def test_create_enabled_with_times_ok(self): _validate_enabled_have_times( {"scheduled_enabled": True, "scheduled_times": "09:00"}, None ) def test_create_enabled_without_times_raises(self): with pytest.raises(HTTPException) as exc: _validate_enabled_have_times( {"scheduled_enabled": True, "scheduled_times": ""}, None ) assert exc.value.status_code == 422 assert "scheduled_times" in str(exc.value.detail) def test_create_disabled_without_times_ok(self): _validate_enabled_have_times( {"memory_enabled": False, "memory_times": ""}, None ) def test_all_three_kinds_checked(self): for kind in ("periodic", "scheduled", "memory"): with pytest.raises(HTTPException): _validate_enabled_have_times( {f"{kind}_enabled": True, f"{kind}_times": ""}, None ) # --- update path (effective state = values merged over existing) --- def test_update_enable_without_providing_times_uses_existing_empty(self): existing = TrackingConfig(provider_type="immich", name="t", scheduled_times="") with pytest.raises(HTTPException): _validate_enabled_have_times({"scheduled_enabled": True}, existing) def test_update_enable_with_existing_times_ok(self): existing = TrackingConfig( provider_type="immich", name="t", scheduled_times="09:00" ) _validate_enabled_have_times({"scheduled_enabled": True}, existing) def test_update_clear_times_while_enabled_in_existing_raises(self): existing = TrackingConfig( provider_type="immich", name="t", scheduled_enabled=True, scheduled_times="09:00", ) with pytest.raises(HTTPException): _validate_enabled_have_times({"scheduled_times": ""}, existing) def test_update_unrelated_field_does_not_trigger_check(self): # A pre-existing enabled-but-empty config must not block edits that don't # touch the slot's enabled flag or times (only touched kinds are checked). existing = TrackingConfig( provider_type="immich", name="t", scheduled_enabled=True, scheduled_times="", ) _validate_enabled_have_times({"name": "renamed"}, existing) def test_update_disabling_slot_clears_requirement(self): existing = TrackingConfig( provider_type="immich", name="t", scheduled_enabled=True, scheduled_times="09:00", ) _validate_enabled_have_times( {"scheduled_enabled": False, "scheduled_times": ""}, existing )