server-up/tests/test_connect.py
Ramon 4450b3c171
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 1m16s
v0.7.00-beta - apps koppelen, eenvoudiger instellen, geavanceerde opties verborgen
Apps onderling verbinden:
- Elke stack draaide als eigen compose-project met een eigen netwerk, waardoor
  stacks elkaar niet op naam konden bereiken. Bij installeren staat nu
  "Verbinden met andere apps" aan: de stack komt op een gedeeld bridge-netwerk
  (SHARED_NETWORK, standaard "serverup") dat op aanvraag wordt aangemaakt.
- compose_transform.add_shared_network() hangt elke service eraan en laat
  bestaande netwerken staan, dus het werkt samen met een macvlan-adres.
- Nieuw /api/apps/connectable geeft de geinstalleerde apps met hun
  containernamen, zodat de installatiemodal kan laten kiezen.

Kiezen in plaats van typen:
- Een veld met "connect": {"app": "mosquitto", "scheme": "mqtt", "port": 1883}
  toont een keuzelijst met wat er al draait, met handmatig invullen als
  terugval. Toegepast op zigbee2mqtt -> mosquitto en open-webui -> ollama.
- Is de doel-app nog niet geinstalleerd, dan zegt het veld dat.

Uitleg per instelling:
- Info-icoon per veld opent een blok met de volledige uitleg plus de
  standaardwaarde. Uitgeschreven voor de instellingen die de meeste vragen
  oproepen (downloadmap vs mediamap, shm_size bij frigate, poort 80 bij caddy,
  PUID bij de arr-stack, GPU bij ollama, publiek adres bij wg-easy).

Geavanceerde opties:
- advanced: true op een veld of groep zet het achter een uitklapblok met
  teller. Toegepast op wachtwoorden, PUID/PGID, USB-paden en bewaartermijnen
  van 28 apps.
- Test bewaakt dat elke app zichtbare velden overhoudt en dat koppelvelden naar
  een bestaande app verwijzen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C7oLCRYzY5ixJ5Sv8Y8EFb
2026-07-27 13:52:17 +02:00

142 lines
5.4 KiB
Python

"""Apps onderling koppelen, geavanceerde velden en de uitleg per veld."""
import sys
import tempfile
from pathlib import Path
import pytest
import yaml
WORTEL = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(WORTEL / "server-up"))
from core import boilerplates as bp
from core import compose_transform as ct
from conftest import login
BASIS = """\
services:
web:
image: nginx
ports:
- "8080:80"
db:
image: postgres
"""
# ── Gedeeld netwerk ──────────────────────────────────────────────────────────
def test_gedeeld_netwerk_komt_bij_elke_service():
"""Zonder gedeeld netwerk zit elke stack in zijn eigen compose-netwerk en
kan geen enkele app een andere op naam bereiken."""
doc = yaml.safe_load(ct.add_shared_network(BASIS, "serverup"))
for naam, svc in doc["services"].items():
assert "serverup" in svc["networks"], naam
assert doc["networks"]["serverup"] == {"external": True}
def test_gedeeld_netwerk_naast_een_eigen_ip():
"""Een stack met een macvlan-adres moet het gedeelde netwerk erbij krijgen,
niet in plaats daarvan."""
met_ip = ct.to_dedicated_ip(BASIS, "lan", "192.168.1.240")
doc = yaml.safe_load(ct.add_shared_network(met_ip, "serverup"))
web = doc["services"]["web"]["networks"]
assert "lan" in web and "serverup" in web
assert web["lan"] == {"ipv4_address": "192.168.1.240"}
assert doc["networks"]["lan"]["external"] is True
assert doc["networks"]["serverup"]["external"] is True
def test_gedeeld_netwerk_is_idempotent():
een = ct.add_shared_network(BASIS, "serverup")
twee = ct.add_shared_network(een, "serverup")
doc = yaml.safe_load(twee)
assert doc["services"]["web"]["networks"].count("serverup") == 1
def test_gedeeld_netwerk_bij_ongeldige_yaml():
with pytest.raises(ct.TransformError):
ct.add_shared_network("dit: [is geen: yaml", "serverup")
# ── Veldopties ───────────────────────────────────────────────────────────────
def _veld(app: str, naam: str) -> dict:
return next(f for f in bp.fields(WORTEL / "apps" / app) if f["name"] == naam)
def test_koppelveld_wordt_doorgegeven():
f = _veld("zigbee2mqtt", "mqtt_server")
assert f["connect"] == {"app": "mosquitto", "scheme": "mqtt", "port": 1883}
assert f["help"], "koppelveld hoort uitleg te hebben"
def test_open_webui_wijst_naar_ollama():
f = _veld("open-webui", "ollama_url")
assert f["connect"]["app"] == "ollama"
assert f["connect"]["port"] == 11434
def test_geavanceerd_veld_wordt_gemarkeerd():
assert _veld("zigbee2mqtt", "usb_device")["advanced"] is True
assert _veld("zigbee2mqtt", "mqtt_server")["advanced"] is False
def test_gewone_velden_blijven_zichtbaar():
"""De basisvelden mogen niet per ongeluk achter het uitklapmenu verdwijnen."""
for app in ("zigbee2mqtt", "immich", "frigate", "ollama"):
velden = {f["name"]: f for f in bp.fields(WORTEL / "apps" / app)}
assert velden["service_name"]["advanced"] is False
if "port" in velden:
assert velden["port"]["advanced"] is False
def test_elke_app_houdt_zichtbare_velden_over():
"""Een app waarvan alles geavanceerd is, zou een leeg formulier tonen."""
for d in sorted((WORTEL / "apps").iterdir()):
if not d.is_dir():
continue
velden = [f for f in bp.fields(d) if not f.get("is_group_toggle")]
zichtbaar = [f for f in velden if not f["advanced"]]
assert zichtbaar, f"{d.name}: alle velden staan als geavanceerd gemarkeerd"
def test_koppelvelden_verwijzen_naar_bestaande_apps():
"""Een connect-veld dat naar een app verwijst die niet bestaat, levert een
lege keuzelijst op zonder dat iemand snapt waarom."""
apps = {d.name for d in (WORTEL / "apps").iterdir() if d.is_dir()}
for d in sorted((WORTEL / "apps").iterdir()):
if not d.is_dir():
continue
for f in bp.fields(d):
doel = (f.get("connect") or {}).get("app")
if doel:
assert doel in apps, f"{d.name}/{f['name']} verwijst naar '{doel}'"
# ── Endpoint ─────────────────────────────────────────────────────────────────
def test_connectable_endpoint(client, env, monkeypatch):
from core import docker
import json as _json
login(client)
stack = env["lib"] / "mosquitto"
stack.mkdir(parents=True)
(stack / "docker-compose.yml").write_text("services:\n a:\n image: x\n")
(stack / ".serverup.json").write_text(_json.dumps(
{"source": "mosquitto", "name": "Mosquitto MQTT"}))
monkeypatch.setattr(docker, "compose_ps",
lambda d, name=None: [{"name": "mosquitto", "service": "a",
"ports": "1883/tcp", "running": True}])
d = client.get("/api/apps/connectable").get_json()
assert d["shared_network"] == "serverup"
app = next(a for a in d["apps"] if a["stack"] == "mosquitto")
assert app["source"] == "mosquitto"
assert app["containers"][0]["name"] == "mosquitto"
def test_connectable_zonder_stacks(client):
login(client)
assert client.get("/api/apps/connectable").get_json()["apps"] == []