All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 1m16s
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
142 lines
5.4 KiB
Python
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"] == []
|