server-up/tests/test_containers.py
Ramon 6f549357b1
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 5m44s
fix(unifi): herstel MongoDB-gebruiker
2026-08-06 09:36:37 +02:00

259 lines
11 KiB
Python

"""Containers op de host: wat is beheerd en wat niet.
Het dashboard telt alles wat de Docker-daemon kent, terwijl de stacklijst alleen
toont wat in LIBRARY_DIR staat. Containers die daarbuiten vallen waren zichtbaar
in de cijfers maar nergens terug te vinden.
"""
import pytest
from conftest import login
PS_UITVOER = "\n".join([
"abc123\tvaultwarden\tvaultwarden/server:latest\trunning\tUp 2 hours\t0.0.0.0:8222->80/tcp\tvaultwarden\t/opt/serverup/stacks/vaultwarden",
"def456\tforgejo\tcodeberg.org/forgejo/forgejo:1.21\trunning\tUp 3 days\t0.0.0.0:3000->3000/tcp\tforgejo\t/opt/docker/forgejo",
"ghi789\tact_runner\tgitea/act_runner:latest\trunning\tUp 3 days\t\t\t",
"jkl012\tserver-up\tserver-up:0.5.46-beta\trunning\tUp 5 minutes\t127.0.0.1:5000->5000/tcp\tserver-up\t/opt/docker/server-up",
"mno345\toud-testje\tnginx:latest\texited\tExited (0) 4 days ago\t\t\t",
])
@pytest.fixture
def host(env, monkeypatch):
from core import docker
monkeypatch.setattr(docker, "_run", lambda *a, **k: type(
"R", (), {"returncode": 0, "stdout": PS_UITVOER, "stderr": ""})())
return docker
def test_list_containers_leest_alle_velden(host):
c = host.list_containers()
assert len(c) == 5
vw = next(x for x in c if x["name"] == "vaultwarden")
assert vw["image"] == "vaultwarden/server:latest"
assert vw["running"] is True
assert vw["project"] == "vaultwarden"
assert vw["working_dir"] == "/opt/serverup/stacks/vaultwarden"
def test_list_containers_zonder_labels(host):
"""Een container zonder compose-labels heeft lege project-/werkmapvelden."""
c = next(x for x in host.list_containers() if x["name"] == "act_runner")
assert c["project"] == "" and c["working_dir"] == ""
assert c["running"] is True
def test_gestopte_container_wordt_herkend(host):
c = next(x for x in host.list_containers() if x["name"] == "oud-testje")
assert c["running"] is False
def test_endpoint_scheidt_beheerd_van_de_rest(client, env, monkeypatch):
from core import docker
login(client)
# Eén stack in de library: vaultwarden.
stack = env["lib"] / "vaultwarden"
stack.mkdir(parents=True)
(stack / "docker-compose.yml").write_text("services:\n app:\n image: nginx\n")
monkeypatch.setattr(docker, "_run", lambda *a, **k: type(
"R", (), {"returncode": 0,
"stdout": PS_UITVOER.replace("/opt/serverup/stacks/vaultwarden",
str(stack)),
"stderr": ""})())
d = client.get("/api/containers").get_json()
beheerd = [c["name"] for c in d["containers"] if c["managed"]]
overig = [c["name"] for c in d["containers"] if not c["managed"]]
assert beheerd == ["vaultwarden"]
assert set(overig) == {"forgejo", "act_runner", "server-up", "oud-testje"}
assert d["managed"] == 1
assert d["total"] == 5
assert d["running"] == 4
def test_eigen_container_wordt_gemarkeerd(client, env, monkeypatch):
from core import docker
import app as app_module
login(client)
monkeypatch.setattr(app_module, "CONTAINER_NAME", "server-up")
monkeypatch.setattr(docker, "_run", lambda *a, **k: type(
"R", (), {"returncode": 0, "stdout": PS_UITVOER, "stderr": ""})())
d = client.get("/api/containers").get_json()
assert next(c for c in d["containers"] if c["name"] == "server-up")["self"] is True
def test_eigen_container_kan_niet_gestopt_worden(client, monkeypatch):
"""Zichzelf stoppen midden in een verzoek levert een halve actie op; daar
is de aparte herstartknop voor."""
import app as app_module
csrf = login(client)
monkeypatch.setattr(app_module, "CONTAINER_NAME", "server-up")
r = client.post("/api/containers/server-up/stop", headers={"X-CSRF-Token": csrf})
assert r.status_code == 400
assert "herstarten" in r.get_json()["msg"]
def test_onbekende_container(client, host):
csrf = login(client)
r = client.post("/api/containers/bestaatniet/stop", headers={"X-CSRF-Token": csrf})
assert r.status_code == 404
def test_onbekende_actie(client, host):
csrf = login(client)
r = client.post("/api/containers/forgejo/verwijder", headers={"X-CSRF-Token": csrf})
assert r.status_code == 400
def test_viewer_mag_geen_containers_bedienen(client, host):
csrf = login(client)
client.post("/api/auth/users",
json={"username": "kijker", "password": "eenlangwachtwoord",
"role": "viewer"},
headers={"X-CSRF-Token": csrf})
r = client.post("/api/auth/login",
json={"username": "kijker", "password": "eenlangwachtwoord"})
kijker_csrf = r.get_json()["csrf_token"]
assert client.get("/api/containers").status_code == 200
assert client.post("/api/containers/forgejo/stop",
headers={"X-CSRF-Token": kijker_csrf}).status_code == 403
# ── Een map zonder compose-bestand ───────────────────────────────────────────
# Die werd stilzwijgend overgeslagen: niet in de lijst, en de containers die
# erbij hoorden belandden onder "handmatig gestart". Je zag een leeg scherm en
# nergens stond waarom.
def test_map_zonder_compose_verdwijnt_niet_uit_de_lijst(client, env):
from conftest import login
login(client)
(env["lib"] / "kapotte-app").mkdir(parents=True, exist_ok=True)
d = client.get("/api/stacks").get_json()
namen = [s["name"] for s in d]
assert "kapotte-app" in namen, f"de map is niet zichtbaar: {namen}"
s = next(s for s in d if s["name"] == "kapotte-app")
assert s["has_compose"] is False, "hij doet alsof er compose is"
assert s["containers"] == []
assert s["running"] is False
def test_een_gewone_app_blijft_gewoon_staan(client, env, monkeypatch):
from conftest import login
from core import docker
monkeypatch.setattr(docker, "compose_ps", lambda *a, **k: [])
login(client)
d = env["lib"] / "echte-app"
d.mkdir(parents=True, exist_ok=True)
(d / "docker-compose.yml").write_text("services:\n web:\n image: nginx\n")
uit = client.get("/api/stacks").get_json()
s = next(s for s in uit if s["name"] == "echte-app")
assert s["has_compose"] is True
# ── Overnemen ────────────────────────────────────────────────────────────────
# Verwijderen laat een handmatig gestarte compose-app niet verdwijnen: hij staat
# er weer zodra dat project ergens anders start. Overnemen wél.
def _nep_container(monkeypatch, **velden):
from core import docker
c = {"id": "abc", "name": "losse-app", "image": "nginx", "state": "running",
"status": "Up", "ports": "", "project": "losse-app",
"working_dir": "", "running": True}
c.update(velden)
monkeypatch.setattr(docker, "list_containers", lambda *a, **k: [c])
return c
def test_overnemen_maakt_een_verwijzing_en_verplaatst_niets(client, env, monkeypatch, tmp_path):
from conftest import login
csrf = login(client)
bron = tmp_path / "eigen-map"
bron.mkdir()
(bron / "docker-compose.yml").write_text("services:\n web:\n image: nginx\n")
_nep_container(monkeypatch, working_dir=str(bron))
r = client.post("/api/containers/losse-app/adopt", headers={"X-CSRF-Token": csrf})
assert r.status_code == 200, r.get_json()
doel = env["lib"] / "losse-app"
assert doel.is_symlink(), "er is geen verwijzing gemaakt"
assert doel.resolve() == bron.resolve()
assert (bron / "docker-compose.yml").is_file(), "de bron is aangeraakt"
# En hij duikt op in de appslijst.
from core import docker
monkeypatch.setattr(docker, "compose_ps", lambda *a, **k: [])
namen = [s["name"] for s in client.get("/api/stacks").get_json()]
assert "losse-app" in namen, namen
def test_overnemen_van_een_map_waar_we_niet_bij_kunnen(client, env, monkeypatch):
"""De container draait op de host; wij zien alleen wat gekoppeld is."""
from conftest import login
csrf = login(client)
_nep_container(monkeypatch, working_dir="/ergens/op/de/host")
r = client.post("/api/containers/losse-app/adopt", headers={"X-CSRF-Token": csrf})
assert r.status_code == 400
assert "niet in de container gekoppeld" in r.get_json()["msg"]
def test_overnemen_zonder_compose_labels(client, env, monkeypatch):
from conftest import login
csrf = login(client)
_nep_container(monkeypatch, project="", working_dir="")
r = client.post("/api/containers/losse-app/adopt", headers={"X-CSRF-Token": csrf})
assert r.status_code == 400
assert "niet met docker compose gestart" in r.get_json()["msg"]
def test_overnemen_botst_niet_met_een_bestaande_app(client, env, monkeypatch, tmp_path):
from conftest import login
csrf = login(client)
(env["lib"] / "losse-app").mkdir(parents=True)
bron = tmp_path / "eigen-map"
bron.mkdir()
(bron / "docker-compose.yml").write_text("services: {}\n")
_nep_container(monkeypatch, working_dir=str(bron))
r = client.post("/api/containers/losse-app/adopt", headers={"X-CSRF-Token": csrf})
assert r.status_code == 409
assert "al een app" in r.get_json()["msg"]
def test_een_losse_container_mag_verwijderd_worden(client, env, monkeypatch):
from conftest import login
from core import docker
csrf = login(client)
_nep_container(monkeypatch)
gedaan = []
monkeypatch.setattr(docker, "container_action",
lambda n, a: (gedaan.append((n, a)), (True, "weg"))[1])
r = client.post("/api/containers/losse-app/remove", headers={"X-CSRF-Token": csrf})
assert r.status_code == 200, r.get_json()
assert gedaan == [("losse-app", "remove")]
def test_server_up_verwijdert_zichzelf_niet(client, env, monkeypatch):
from conftest import login
import app as app_mod
csrf = login(client)
_nep_container(monkeypatch, name=app_mod.CONTAINER_NAME)
r = client.post(f"/api/containers/{app_mod.CONTAINER_NAME}/remove",
headers={"X-CSRF-Token": csrf})
assert r.status_code == 400
def test_compose_ps_toont_alleen_werkelijk_gepubliceerde_poorten(host):
container = {
"Ports": "8443/tcp, 8843/tcp",
"Publishers": [
{"TargetPort": 8443, "PublishedPort": 8443, "Protocol": "tcp"},
{"TargetPort": 8443, "PublishedPort": 8443, "Protocol": "tcp"},
{"TargetPort": 8843, "PublishedPort": 0, "Protocol": "tcp"},
],
}
assert host._published_ports(container) == "8443->8443/tcp"
def test_compose_ps_valt_bij_oude_compose_terug_op_ports(host):
assert host._published_ports(
{"Ports": "0.0.0.0:8080->80/tcp"}) == "0.0.0.0:8080->80/tcp"