feat(service-credential-tester): slice 1 — backend test endpoint + per-type routines
TestResult dataclass + translate_connection_error shared helper in base.py. test_callable field on ServiceDefinition (default None). 7 per-type test_connection routines (qbittorrent, prometheus via Grafana gateway, alertmanager, jellyfin, authentik, ssh_tasks via build_ssh_client, nextcloud). POST /api/services/test endpoint: validation-first (422 on malformed config), dispatch, no-persistence, no-secret-logs. backups has test_callable=None. qBit 'Fails.' → specific auth message (resolves #3 at API layer). Backend: 362 pytest pass (+31 new), ruff clean. Frontend: build green.
This commit is contained in:
@@ -12,7 +12,7 @@ from typing import Any
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
|
||||
from media_library_viewer_api.dependencies import get_settings_store
|
||||
from media_library_viewer_api.integrations.base import validate_config
|
||||
from media_library_viewer_api.integrations.base import TestResult, validate_config
|
||||
from media_library_viewer_api.integrations.registry import (
|
||||
SERVICE_DEFINITIONS,
|
||||
get_service_definition,
|
||||
@@ -180,3 +180,30 @@ def delete_instance(
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Service not found")
|
||||
store.delete_service(service_id)
|
||||
return {"status": "deleted"}
|
||||
|
||||
|
||||
@router.post("/test")
|
||||
def test_instance(
|
||||
body: ServiceInstanceInput,
|
||||
store: SettingsStore = Depends(get_settings_store),
|
||||
) -> dict[str, Any]:
|
||||
"""Test connectivity + credentials for unsaved service input.
|
||||
|
||||
Validates first (422 on malformed config), dispatches to the per-type
|
||||
test_callable, and returns ``{ok, detail, evidence}``. Does NOT persist.
|
||||
"""
|
||||
_validate_input(body) # raises HTTPException(422) on bad config/type/secrets
|
||||
definition = require_service_definition(body.service_type)
|
||||
|
||||
if definition.test_callable is None:
|
||||
logger.info("test requested type=%s ok=true (no test_callable)", body.service_type)
|
||||
return {"ok": True, "detail": "No connection test for this service type", "evidence": None}
|
||||
|
||||
try:
|
||||
result: TestResult = definition.test_callable(body.config, body.secrets, store)
|
||||
except Exception as exc:
|
||||
logger.exception("test_callable raised for type=%s", body.service_type)
|
||||
result = TestResult(ok=False, detail=f"Test failed unexpectedly: {exc}")
|
||||
|
||||
logger.info("test requested type=%s ok=%s", body.service_type, result.ok)
|
||||
return {"ok": result.ok, "detail": result.detail, "evidence": result.evidence}
|
||||
|
||||
Reference in New Issue
Block a user