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:
@@ -7,15 +7,47 @@ password), and three widget kinds (totals, active, speed). Models on
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from media_library_viewer_api.clients.qbittorrent import QbittorrentClient
|
||||
from media_library_viewer_api.integrations.base import (
|
||||
SecretField,
|
||||
ServiceBaseUrl,
|
||||
ServiceConfigBase,
|
||||
ServiceDefinition,
|
||||
TestResult,
|
||||
WidgetConfigBase,
|
||||
translate_connection_error,
|
||||
widget_kind,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from media_library_viewer_api.services.settings_store import SettingsStore
|
||||
|
||||
|
||||
def test_connection(
|
||||
config: dict[str, Any],
|
||||
secrets: dict[str, str],
|
||||
store: SettingsStore,
|
||||
) -> TestResult:
|
||||
"""Login + probe maindata; surface auth failures specifically."""
|
||||
try:
|
||||
base_url = str(config.get("base_url") or "")
|
||||
username = str(secrets.get("username") or "")
|
||||
password = str(secrets.get("password") or "")
|
||||
timeout = int(config.get("timeout_seconds") or 10)
|
||||
client = QbittorrentClient(base_url, username, password, timeout=timeout)
|
||||
data = client.maindata()
|
||||
version = str(data.get("server_state", {}).get("qbittorrent_version", "") or "connected")
|
||||
return TestResult(ok=True, detail="Connected to qBittorrent.", evidence=version)
|
||||
except RuntimeError as exc:
|
||||
lowered = str(exc).lower()
|
||||
if "login failed" in lowered:
|
||||
return TestResult(ok=False, detail="Authentication failed — qBittorrent rejected the credentials.")
|
||||
return translate_connection_error(exc, context="qBittorrent")
|
||||
except Exception as exc:
|
||||
return translate_connection_error(exc, context="qBittorrent")
|
||||
|
||||
|
||||
class QbittorrentConfig(ServiceConfigBase):
|
||||
"""Non-secret qBittorrent connection config."""
|
||||
@@ -65,4 +97,5 @@ DEFINITION = ServiceDefinition(
|
||||
refresh_interval_ms=5_000,
|
||||
),
|
||||
],
|
||||
test_callable=test_connection,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user