344 lines
12 KiB
Python
344 lines
12 KiB
Python
from pathlib import Path
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
from sqlmodel import SQLModel, create_engine
|
|
|
|
|
|
@pytest.fixture
|
|
def api_client(tmp_path, monkeypatch):
|
|
db_path = tmp_path / "setup_test.db"
|
|
monkeypatch.setenv("APPOPS_DB_PATH", str(db_path))
|
|
monkeypatch.setenv("APP_OPS_VOICE_TOKEN", "test-voice-token")
|
|
for name in [
|
|
"PORTAINER_URL",
|
|
"PORTAINER_API_KEY",
|
|
"PORTAINER_ENDPOINT_ID",
|
|
"UNRAID_URL",
|
|
"UNRAID_API_KEY",
|
|
"AUTHENTIK_URL",
|
|
"AUTHENTIK_TOKEN",
|
|
"AUTHENTIK_OUTPOST_URL",
|
|
"RADARR_URL",
|
|
"RADARR_API_KEY",
|
|
"PROWLARR_URL",
|
|
"PROWLARR_API_KEY",
|
|
]:
|
|
monkeypatch.delenv(name, raising=False)
|
|
|
|
from backend.app.main import app
|
|
from backend.app.core.config import settings
|
|
from backend.app.db import session
|
|
from backend.app.db.init_db import init_db
|
|
|
|
for name in [
|
|
"PORTAINER_URL",
|
|
"PORTAINER_API_KEY",
|
|
"PORTAINER_ENDPOINT_ID",
|
|
"UNRAID_URL",
|
|
"UNRAID_API_KEY",
|
|
"AUTHENTIK_URL",
|
|
"AUTHENTIK_TOKEN",
|
|
"AUTHENTIK_OUTPOST_URL",
|
|
"RADARR_URL",
|
|
"RADARR_API_KEY",
|
|
"PROWLARR_URL",
|
|
"PROWLARR_API_KEY",
|
|
]:
|
|
settings.apis[name] = None
|
|
settings.verify_tls = False
|
|
|
|
engine = create_engine(f"sqlite:///{Path(db_path)}", connect_args={"check_same_thread": False})
|
|
session.engine = engine
|
|
SQLModel.metadata.drop_all(engine)
|
|
SQLModel.metadata.create_all(engine)
|
|
init_db()
|
|
|
|
with TestClient(app) as client:
|
|
yield client
|
|
|
|
|
|
def test_setup_status_initial_is_pending_and_secret_safe(api_client):
|
|
payload = api_client.get("/api/setup/status").json()
|
|
|
|
assert payload["firstRunCompleted"] is False
|
|
assert payload["summary"]["containersDiscovered"] == 0
|
|
assert payload["summary"]["appsManaged"] >= 0
|
|
assert payload["summary"]["connectorsMissingConfig"] >= 1
|
|
assert {step["id"]: step["status"] for step in payload["steps"]}["portainer"] == "pending"
|
|
assert {step["id"]: step["status"] for step in payload["steps"]}["discovery"] == "pending"
|
|
assert "test-voice-token" not in str(payload)
|
|
|
|
|
|
def test_portainer_config_save_never_returns_secret(api_client):
|
|
secret = "PORTAINER_SUPER_SECRET"
|
|
|
|
payload = api_client.patch(
|
|
"/api/settings/connectors/portainer",
|
|
json={
|
|
"url": "http://198.18.10.150:9443",
|
|
"endpointId": "1",
|
|
"verifyTls": False,
|
|
"apiKey": secret,
|
|
},
|
|
).json()
|
|
|
|
assert payload["connectorId"] == "portainer"
|
|
assert payload["configured"] is True
|
|
assert payload["hasSecret"] is True
|
|
assert payload["missing"] == []
|
|
assert payload["safeConfig"] == {
|
|
"url": "http://198.18.10.150:9443",
|
|
"endpointId": "1",
|
|
"verifyTls": False,
|
|
}
|
|
assert secret not in str(payload)
|
|
|
|
all_settings = api_client.get("/api/settings/connectors").json()
|
|
assert secret not in str(all_settings)
|
|
assert next(item for item in all_settings if item["connectorId"] == "portainer")["hasSecret"] is True
|
|
|
|
|
|
def test_portainer_config_save_trims_safe_fields(api_client):
|
|
payload = api_client.patch(
|
|
"/api/settings/connectors/portainer",
|
|
json={
|
|
"url": " http://198.18.10.150:9200/ ",
|
|
"endpointId": " 1 ",
|
|
"verifyTls": False,
|
|
"apiKey": "PORTAINER_SUPER_SECRET",
|
|
},
|
|
).json()
|
|
|
|
assert payload["safeConfig"]["url"] == "http://198.18.10.150:9200"
|
|
assert payload["safeConfig"]["endpointId"] == "1"
|
|
|
|
|
|
def test_portainer_config_save_defaults_endpoint_id(api_client):
|
|
payload = api_client.patch(
|
|
"/api/settings/connectors/portainer",
|
|
json={
|
|
"url": "http://198.18.10.150:9200",
|
|
"endpointId": "",
|
|
"verifyTls": False,
|
|
"apiKey": "PORTAINER_SUPER_SECRET",
|
|
},
|
|
).json()
|
|
|
|
assert payload["safeConfig"]["endpointId"] == "1"
|
|
assert "PORTAINER_ENDPOINT_ID" not in payload["missing"]
|
|
|
|
|
|
def test_setup_test_portainer_missing_config_and_redaction(api_client):
|
|
response = api_client.post("/api/setup/test/portainer")
|
|
|
|
assert response.status_code == 200
|
|
payload = response.json()
|
|
assert payload["success"] is False
|
|
assert payload["status"] == "missing_config"
|
|
assert "PORTAINER_API_KEY" in payload["requiredFields"]
|
|
|
|
|
|
def test_setup_test_portainer_success_summary(api_client, monkeypatch):
|
|
secret = "PORTAINER_SUPER_SECRET"
|
|
api_client.patch(
|
|
"/api/settings/connectors/portainer",
|
|
json={"url": "http://portainer.local", "endpointId": "1", "verifyTls": False, "apiKey": secret},
|
|
)
|
|
|
|
monkeypatch.setattr("backend.app.connectors.portainer.PortainerConnector.test_connection", lambda self: {"status": "connected"}, raising=True)
|
|
monkeypatch.setattr("backend.app.connectors.portainer.PortainerConnector.list_containers", lambda self: {"status": "ok", "containers": [{"id": "one"}]}, raising=True)
|
|
monkeypatch.setattr("backend.app.connectors.portainer.PortainerConnector.list_stacks", lambda self: {"status": "ok", "stacks": [{"id": "stack"}]}, raising=True)
|
|
|
|
payload = api_client.post("/api/setup/test/portainer").json()
|
|
|
|
assert payload["success"] is True
|
|
assert payload["status"] == "connected"
|
|
assert payload["summary"]["containers"] == 1
|
|
assert payload["summary"]["stacks"] == 1
|
|
assert secret not in str(payload)
|
|
|
|
|
|
def test_setup_test_portainer_endpoint_error_is_actionable(api_client, monkeypatch):
|
|
secret = "PORTAINER_SUPER_SECRET"
|
|
api_client.patch(
|
|
"/api/settings/connectors/portainer",
|
|
json={"url": "http://portainer.local", "endpointId": "99", "verifyTls": False, "apiKey": secret},
|
|
)
|
|
|
|
monkeypatch.setattr(
|
|
"backend.app.connectors.portainer.PortainerConnector.test_connection",
|
|
lambda self: {"status": "endpoint_error", "reason": "Portainer API error: HTTP 404 endpoint not found"},
|
|
raising=True,
|
|
)
|
|
|
|
payload = api_client.post("/api/setup/test/portainer").json()
|
|
|
|
assert payload["success"] is False
|
|
assert payload["status"] == "endpoint_error"
|
|
assert payload["requiredFields"] == ["PORTAINER_ENDPOINT_ID"]
|
|
assert payload["safeConfig"]["endpointId"] == "99"
|
|
assert secret not in str(payload)
|
|
|
|
|
|
def test_setup_test_portainer_container_list_error_marks_endpoint(api_client, monkeypatch):
|
|
secret = "PORTAINER_SUPER_SECRET"
|
|
api_client.patch(
|
|
"/api/settings/connectors/portainer",
|
|
json={"url": "http://portainer.local", "endpointId": "1", "verifyTls": False, "apiKey": secret},
|
|
)
|
|
|
|
monkeypatch.setattr("backend.app.connectors.portainer.PortainerConnector.test_connection", lambda self: {"status": "connected"}, raising=True)
|
|
monkeypatch.setattr(
|
|
"backend.app.connectors.portainer.PortainerConnector.list_containers",
|
|
lambda self: {"status": "error", "reason": "Portainer API error: HTTP 404 endpoint not found", "containers": []},
|
|
raising=True,
|
|
)
|
|
monkeypatch.setattr("backend.app.connectors.portainer.PortainerConnector.list_stacks", lambda self: {"status": "ok", "stacks": []}, raising=True)
|
|
|
|
payload = api_client.post("/api/setup/test/portainer").json()
|
|
|
|
assert payload["success"] is False
|
|
assert payload["status"] == "endpoint_error"
|
|
assert payload["summary"]["endpointReachable"] is False
|
|
assert payload["requiredFields"] == ["PORTAINER_ENDPOINT_ID"]
|
|
assert secret not in str(payload)
|
|
|
|
|
|
def test_unraid_config_save_never_returns_secret(api_client):
|
|
secret = "UNRAID_SUPER_SECRET"
|
|
|
|
payload = api_client.patch(
|
|
"/api/settings/connectors/unraid",
|
|
json={"url": "http://unraid.local", "verifyTls": False, "apiKey": secret},
|
|
).json()
|
|
|
|
assert payload["connectorId"] == "unraid"
|
|
assert payload["configured"] is True
|
|
assert payload["hasSecret"] is True
|
|
assert payload["safeConfig"] == {"url": "http://unraid.local", "verifyTls": False}
|
|
assert secret not in str(payload)
|
|
|
|
|
|
def test_setup_test_authentik_404_marks_url_error(api_client, monkeypatch):
|
|
secret = "AUTHENTIK_SUPER_SECRET"
|
|
api_client.patch(
|
|
"/api/settings/connectors/authentik",
|
|
json={"url": "https://auth.example.local/if/flow/default-authentication-flow", "verifyTls": False, "apiKey": secret},
|
|
)
|
|
|
|
monkeypatch.setattr(
|
|
"backend.app.connectors.authentik.AuthentikConnector.test_connection",
|
|
lambda self: {"status": "unreachable", "reason": "Authentik API error: HTTP 404 <!DOCTYPE html>"},
|
|
raising=True,
|
|
)
|
|
|
|
payload = api_client.post("/api/setup/test/connector/authentik").json()
|
|
|
|
assert payload["success"] is False
|
|
assert payload["status"] == "url_error"
|
|
assert payload["requiredFields"] == ["AUTHENTIK_URL"]
|
|
assert "root/base URL" in payload["message"]
|
|
assert secret not in str(payload)
|
|
|
|
|
|
def test_setup_test_authentik_auth_failed_marks_token(api_client, monkeypatch):
|
|
secret = "AUTHENTIK_SUPER_SECRET"
|
|
api_client.patch(
|
|
"/api/settings/connectors/authentik",
|
|
json={"url": "https://auth.example.local", "verifyTls": False, "apiKey": secret},
|
|
)
|
|
|
|
monkeypatch.setattr(
|
|
"backend.app.connectors.authentik.AuthentikConnector.test_connection",
|
|
lambda self: {"status": "auth_failed", "reason": "auth_failed"},
|
|
raising=True,
|
|
)
|
|
|
|
payload = api_client.post("/api/setup/test/connector/authentik").json()
|
|
|
|
assert payload["success"] is False
|
|
assert payload["status"] == "auth_failed"
|
|
assert payload["requiredFields"] == ["AUTHENTIK_TOKEN"]
|
|
assert "token ongeldig of verlopen" in payload["message"]
|
|
assert "API Token" in payload["message"]
|
|
assert secret not in str(payload)
|
|
|
|
|
|
def test_unraid_partial_config_update_preserves_url_and_secret(api_client):
|
|
secret = "UNRAID_SUPER_SECRET"
|
|
api_client.patch(
|
|
"/api/settings/connectors/unraid",
|
|
json={"url": "https://unraid.local", "verifyTls": True, "apiKey": secret},
|
|
)
|
|
|
|
payload = api_client.patch(
|
|
"/api/settings/connectors/unraid",
|
|
json={"verifyTls": False},
|
|
).json()
|
|
|
|
assert payload["configured"] is True
|
|
assert payload["hasSecret"] is True
|
|
assert payload["safeConfig"] == {"url": "https://unraid.local", "verifyTls": False}
|
|
assert secret not in str(payload)
|
|
|
|
|
|
def test_setup_test_prowlarr_uses_connector_test(api_client, monkeypatch):
|
|
secret = "PROWLARR_SUPER_SECRET"
|
|
api_client.patch(
|
|
"/api/settings/connectors/prowlarr",
|
|
json={"url": "http://prowlarr.local:9696", "verifyTls": False, "apiKey": secret},
|
|
)
|
|
|
|
monkeypatch.setattr(
|
|
"backend.app.connectors.prowlarr.ProwlarrConnector.test_connection",
|
|
lambda self: {"status": "connected", "version": "1.2.3"},
|
|
raising=True,
|
|
)
|
|
|
|
payload = api_client.post("/api/setup/test/connector/prowlarr").json()
|
|
|
|
assert payload["success"] is True
|
|
assert payload["status"] == "connected"
|
|
assert payload["connected"] is True
|
|
assert payload["settings"]["safeConfig"]["url"] == "http://prowlarr.local:9696"
|
|
assert secret not in str(payload)
|
|
|
|
|
|
def test_setup_test_prowlarr_duplicate_radarr_url_is_actionable(api_client, monkeypatch):
|
|
prowlarr_secret = "PROWLARR_SUPER_SECRET"
|
|
radarr_secret = "RADARR_SUPER_SECRET"
|
|
shared_url = "http://media.local:7878"
|
|
api_client.patch(
|
|
"/api/settings/connectors/radarr",
|
|
json={"url": shared_url, "verifyTls": False, "apiKey": radarr_secret},
|
|
)
|
|
api_client.patch(
|
|
"/api/settings/connectors/prowlarr",
|
|
json={"url": shared_url, "verifyTls": False, "apiKey": prowlarr_secret},
|
|
)
|
|
|
|
monkeypatch.setattr(
|
|
"backend.app.connectors.prowlarr.ProwlarrConnector.test_connection",
|
|
lambda self: {"status": "error", "reason": "unauthorized"},
|
|
raising=True,
|
|
)
|
|
|
|
payload = api_client.post("/api/setup/test/connector/prowlarr").json()
|
|
|
|
assert payload["success"] is False
|
|
assert payload["status"] == "url_mismatch"
|
|
assert payload["requiredFields"] == ["PROWLARR_URL", "PROWLARR_API_KEY"]
|
|
assert "Radarr" in payload["message"]
|
|
assert "Prowlarr" in payload["message"]
|
|
assert prowlarr_secret not in str(payload)
|
|
assert radarr_secret not in str(payload)
|
|
|
|
|
|
def test_complete_setup_sets_first_run_completed(api_client):
|
|
payload = api_client.post("/api/setup/complete").json()
|
|
|
|
assert payload["success"] is True
|
|
assert payload["status"]["firstRunCompleted"] is True
|
|
assert payload["status"]["setupCompletedAt"]
|