From cd505b999430047868939333336fb45929f9ee54 Mon Sep 17 00:00:00 2001 From: Fredrick Amnehagen Date: Fri, 6 Feb 2026 00:51:51 +0100 Subject: [PATCH] test: add end-to-end agent deployment lifecycle test --- tests/test_e2e_flow.py | 104 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 tests/test_e2e_flow.py diff --git a/tests/test_e2e_flow.py b/tests/test_e2e_flow.py new file mode 100644 index 0000000..93c938f --- /dev/null +++ b/tests/test_e2e_flow.py @@ -0,0 +1,104 @@ +import pytest +import subprocess +import os +import uuid +import testinfra +import time + +# Use the bin/infra wrapper for testing +CLI_BIN = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "bin", "infra")) +CONFIG_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "config.yaml")) +SSH_KEY = os.path.expanduser("~/.ssh/id_ed25519_no_pass") +ROUTER_PASS = "kpvoh58zhq2sq6ms" + +def run_infra(cmd, env=None): + full_env = os.environ.copy() + if env: + full_env.update(env) + full_env["ROUTER_PASS"] = ROUTER_PASS + full_cmd = [CLI_BIN, "--config", CONFIG_PATH] + cmd + return subprocess.run(full_cmd, capture_output=True, text=True, env=full_env) + +def get_testinfra_host(host_str): + return testinfra.get_host( + f"ssh://root@{host_str}", + ssh_identity_file=SSH_KEY, + ssh_extra_args="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" + ) + +@pytest.fixture +def agent_context(): + """Provides a unique context for an agent service deployment""" + uid = str(uuid.uuid4())[:8] + return { + "id": uid, + "name": f"agent-svc-{uid}", + "domain": f"agent-{uid}.loopaware.com", + "mac": f"aa:bb:cc:ee:ff:{uid[:2]}", + "port": 8080 + } + +def test_e2e_agent_deployment_lifecycle(agent_context): + """ + Tests the complete flow: + IPAM -> DB -> DNS -> Ingress -> Cloudflare -> Decommission + """ + ctx = agent_context + print(f"\n[E2E] Starting deployment for {ctx['name']}") + + # 1. IPAM: Find next free IP + res = run_infra(["ip", "next-free"]) + assert res.returncode == 0 + ip = res.stdout.strip() + print(f"[E2E] Allocated IP: {ip}") + + # 2. Database: Provision PostgreSQL + res = run_infra(["db", "provision", ctx['name']]) + assert res.returncode == 0 + assert ctx['name'].replace("-", "_") in res.stdout + print("[E2E] Database provisioned") + + # 3. DNS: Register DHCP Host + res = run_infra(["dns", "add-host", ctx['mac'], ip, ctx['name']]) + assert res.returncode == 0 + print("[E2E] DNS/DHCP host registered") + + # 4. Ingress: Configure HAProxy + res = run_infra(["ingress", "add", ctx['domain'], ip, str(ctx['port'])]) + assert res.returncode == 0 + print(f"[E2E] Ingress configured for {ctx['domain']}") + + # 5. Cloudflare: Add to DDNS list + res = run_infra(["cloudflare", "add-ddns", ctx['domain']]) + assert res.returncode == 0 + print("[E2E] Cloudflare DDNS registered") + + # --- VERIFICATION PHASE --- + print("[E2E] Verifying infrastructure state...") + + # Verify HAProxy points to the CORRECT allocated IP + pve = get_testinfra_host("10.32.2.1") + # Wrap in single quotes to protect from local shell, double for remote + be_name = f"be_{ctx['domain'].replace('.', '_')}" + res = pve.run(f"pct exec 11220 -- grep -A 5 '{be_name}' /etc/haproxy/conf.d/99-dynamic.cfg") + assert res.rc == 0 + assert ip in res.stdout + assert str(ctx['port']) in res.stdout + + # Verify Database exists + db_host = get_testinfra_host("10.32.70.54") + db_name = ctx['name'].lower().replace("-", "_") + # Safer check: use psql -t -c to get raw list + res = db_host.run(f"su - postgres -c \"psql -t -c '\\l' | grep -qw {db_name}\"") + assert res.rc == 0 + + # --- CLEANUP / DECOMMISSION PHASE --- + print("[E2E] Starting decommissioning...") + res = run_infra(["decommission", "--domain", ctx['domain'], "--mac", ctx['mac']]) + assert res.returncode == 0 + + # Final state check: should be gone in sites.json + res = pve.run(f"pct exec 11220 -- grep '{ctx['domain']}' /etc/haproxy/dynamic/sites.json") + assert res.rc != 0 + + print("[E2E] Flow completed successfully.") \ No newline at end of file