Files
notify-bridge/packages/server/tests/test_tracking_config_validation.py
alexei.dolgolyov 8065e6effa feat(immich): multi-time-point scheduling for scheduled/periodic/memory
Replace the single comma-separated text box with an add/remove list of
native HH:MM pickers for the Immich scheduled-assets, periodic-summary,
and memory slots. The backend already stored comma-separated *_times and
scheduled one cron job per time; this makes entering several times per
day discoverable and hardens the read/write path.

Backend:
- services/time_list.py: normalize_time_list (validate / dedup / sort /
  cap at 24, raising TimeListError) + lenient parse_hhmm_list; the
  scheduler now uses the shared parser (drops its private copy).
- tracking_configs API normalizes *_times on every write (422 on bad
  input) and rejects enabling a slot whose times list is empty.
- scheduler warns when an enabled slot has zero or dropped fire times,
  restoring the observability lost with the old per-call warning.

Frontend:
- TimeListEditor.svelte: add/remove native time rows, dedupe + sort on
  emit, per-day cap, collapses on-screen duplicates, aria-labelled rows;
  syncs from the value prop only on external changes (untrack guard) so
  keyboard entry isn't clobbered mid-edit.
- Descriptor-driven save guard: an enabled feature section must have at
  least one time.
- i18n (en/ru) keys; refreshed help text; removed dead invalidTimeList.

Tests: time_list normalization/parsing (incl. non-ASCII/odd shapes) and
the enabled-implies-times validation.
2026-05-29 14:57:41 +03:00

115 lines
4.2 KiB
Python

"""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
)