Backend: add backups + authentik service types, absorb jellyseerr (Slice 1)
New service types: - backups: BackupsConfig(ingestion_label), no secrets, summary widget kind. Modeled as a service so it can be named/multi-instanced like others. - authentik: AuthentikConfig(base_url, timeout_seconds), api_token secret (required). Directory source for the upcoming Users tab. Jellyseerr absorption: - JellyfinConfig gains optional jellyseerr_url + jellyseerr_api_key fields. - integrations/jellyseerr.py deleted; registry entry removed. - clients/jellyseerr.py stays (JellyseerrClient still used by enrichment). - One-time idempotent migration in settings_store.ensure_defaults(): jellyseerr service rows merge into a paired Jellyfin (exactly-one merges; multiple picks first unpaired; none/all-paired drops with a logged warning). The api_key is decrypted from secrets before moving to config. Registry is now 8 types: alertmanager, authentik, backups, grafana, jellyfin, nextcloud, prometheus, ssh_tasks. Tests: registry count updated to 8, jellyseerr-absent assertion, new-type definition assertions, and migration tests (single-jellyfin merge, no- jellyfin drop, idempotency). 256 backend tests pass; ruff clean. Refs openspec/changes/services-as-hub-ia/ (spec R6, tasks slice 1).
This commit is contained in:
@@ -57,24 +57,55 @@ def client(tmp_path):
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_registry_contains_seven_service_types():
|
||||
def test_registry_contains_eight_service_types():
|
||||
assert set(SERVICE_DEFINITIONS) == {
|
||||
"grafana",
|
||||
"prometheus",
|
||||
"alertmanager",
|
||||
"jellyfin",
|
||||
"jellyseerr",
|
||||
"nextcloud",
|
||||
"ssh_tasks",
|
||||
"backups",
|
||||
"authentik",
|
||||
}
|
||||
|
||||
|
||||
def test_jellyseerr_absorbed_into_jellyfin():
|
||||
"""Jellyseerr is no longer its own service type (absorbed into Jellyfin)."""
|
||||
assert "jellyseerr" not in SERVICE_DEFINITIONS
|
||||
jellyfin_config = get_service_definition("jellyfin").config_schema["properties"]
|
||||
assert "jellyseerr_url" in jellyfin_config
|
||||
assert "jellyseerr_api_key" in jellyfin_config
|
||||
|
||||
|
||||
def test_backups_service_definition():
|
||||
definition = get_service_definition("backups")
|
||||
assert definition is not None
|
||||
assert definition.secret_fields == []
|
||||
assert {wk.kind for wk in definition.widget_kinds} == {"summary"}
|
||||
schema = definition.config_schema
|
||||
assert "ingestion_label" in schema["properties"]
|
||||
|
||||
|
||||
def test_authentik_service_definition():
|
||||
definition = get_service_definition("authentik")
|
||||
assert definition is not None
|
||||
assert {sf.key for sf in definition.secret_fields} == {"api_token"}
|
||||
assert definition.secret_fields[0].required is True
|
||||
assert definition.widget_kinds == []
|
||||
schema = definition.config_schema
|
||||
assert "base_url" in schema["properties"]
|
||||
assert "timeout_seconds" in schema["properties"]
|
||||
|
||||
|
||||
def test_definitions_declare_widget_kinds():
|
||||
assert {wk.kind for wk in get_service_definition("grafana").widget_kinds} == {"link"}
|
||||
assert {wk.kind for wk in get_service_definition("prometheus").widget_kinds} == {"metric"}
|
||||
assert {wk.kind for wk in get_service_definition("alertmanager").widget_kinds} == {"active_alerts"}
|
||||
assert {wk.kind for wk in get_service_definition("jellyfin").widget_kinds} == {"activity"}
|
||||
assert get_service_definition("nextcloud").widget_kinds == []
|
||||
assert get_service_definition("authentik").widget_kinds == []
|
||||
assert {wk.kind for wk in get_service_definition("backups").widget_kinds} == {"summary"}
|
||||
assert {wk.kind for wk in get_service_definition("ssh_tasks").widget_kinds} == {"task_output"}
|
||||
|
||||
|
||||
@@ -139,9 +170,10 @@ def test_list_service_types(client):
|
||||
types = {item["service_type"] for item in response.json()}
|
||||
assert types == {
|
||||
"alertmanager",
|
||||
"authentik",
|
||||
"backups",
|
||||
"grafana",
|
||||
"jellyfin",
|
||||
"jellyseerr",
|
||||
"nextcloud",
|
||||
"prometheus",
|
||||
"ssh_tasks",
|
||||
@@ -265,7 +297,7 @@ def test_service_base_url_requires_http_schema(bad_url):
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"service_type", ["grafana", "prometheus", "alertmanager", "jellyfin", "jellyseerr", "nextcloud"]
|
||||
"service_type", ["grafana", "prometheus", "alertmanager", "jellyfin", "authentik", "nextcloud"]
|
||||
)
|
||||
def test_service_base_url_accepts_absolute_urls(service_type):
|
||||
model = get_service_definition(service_type).config_model
|
||||
@@ -390,3 +422,99 @@ def test_record_and_list_service_task_runs(client):
|
||||
assert len(runs) == 1
|
||||
assert runs[0]["status"] == "success"
|
||||
assert runs[0]["stdout_tail"] == "ok"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Jellyseerr → Jellyfin migration (Slice 1.4 / 1.5)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_jellyseerr_migrates_into_single_jellyfin(tmp_path):
|
||||
"""A standalone jellyseerr service merges into the only jellyfin instance."""
|
||||
store = SettingsStore(tmp_path / "settings.sqlite")
|
||||
store.ensure_defaults()
|
||||
|
||||
jellyfin = store.upsert_service(
|
||||
{
|
||||
"service_type": "jellyfin",
|
||||
"name": "Main Jellyfin",
|
||||
"config": {"base_url": "https://jellyfin.example.com"},
|
||||
"enabled": True,
|
||||
},
|
||||
secret_values={"api_key": "jf-key"},
|
||||
)
|
||||
store.upsert_service(
|
||||
{
|
||||
"service_type": "jellyseerr",
|
||||
"name": "Main Jellyseerr",
|
||||
"config": {"base_url": "https://jellyseerr.example.com"},
|
||||
"enabled": True,
|
||||
},
|
||||
secret_values={"api_key": "js-key"},
|
||||
)
|
||||
|
||||
# Run migration via ensure_defaults (idempotent entry point).
|
||||
store.ensure_defaults()
|
||||
|
||||
# Jellyseerr row is gone.
|
||||
assert store.list_services("jellyseerr") == []
|
||||
|
||||
# Jellyfin config gained the absorbed fields.
|
||||
migrated = store.get_service(jellyfin["id"])
|
||||
assert migrated["config"]["jellyseerr_url"] == "https://jellyseerr.example.com"
|
||||
assert migrated["config"]["jellyseerr_api_key"] == "js-key"
|
||||
|
||||
|
||||
def test_jellyseerr_dropped_when_no_jellyfin(tmp_path):
|
||||
"""An unpaired jellyseerr (no jellyfin) is dropped with a warning, no crash."""
|
||||
store = SettingsStore(tmp_path / "settings.sqlite")
|
||||
store.ensure_defaults()
|
||||
|
||||
store.upsert_service(
|
||||
{
|
||||
"service_type": "jellyseerr",
|
||||
"name": "Orphan Jellyseerr",
|
||||
"config": {"base_url": "https://jellyseerr.example.com"},
|
||||
"enabled": True,
|
||||
},
|
||||
secret_values={"api_key": "js-key"},
|
||||
)
|
||||
|
||||
store.ensure_defaults()
|
||||
|
||||
assert store.list_services("jellyseerr") == []
|
||||
assert store.list_services("jellyfin") == []
|
||||
|
||||
|
||||
def test_jellyseerr_migration_is_idempotent(tmp_path):
|
||||
"""Running ensure_defaults twice does nothing the second time."""
|
||||
store = SettingsStore(tmp_path / "settings.sqlite")
|
||||
store.ensure_defaults()
|
||||
|
||||
store.upsert_service(
|
||||
{
|
||||
"service_type": "jellyfin",
|
||||
"name": "JF",
|
||||
"config": {"base_url": "https://jellyfin.example.com"},
|
||||
"enabled": True,
|
||||
},
|
||||
secret_values={"api_key": "k"},
|
||||
)
|
||||
store.upsert_service(
|
||||
{
|
||||
"service_type": "jellyseerr",
|
||||
"name": "JS",
|
||||
"config": {"base_url": "https://jellyseerr.example.com"},
|
||||
"enabled": True,
|
||||
},
|
||||
secret_values={"api_key": "k"},
|
||||
)
|
||||
|
||||
store.ensure_defaults()
|
||||
first_jellyfin = store.list_services("jellyfin")[0]
|
||||
first_url = first_jellyfin["config"]["jellyseerr_url"]
|
||||
|
||||
store.ensure_defaults() # second run
|
||||
second_jellyfin = store.list_services("jellyfin")[0]
|
||||
assert second_jellyfin["config"]["jellyseerr_url"] == first_url
|
||||
assert store.list_services("jellyseerr") == []
|
||||
|
||||
Reference in New Issue
Block a user