"""Live smoke test of the §15 host network settings page. Runs the real server + web UI with BOTH gates forced on and an in-process fake agent (the Windows-dev path), then probes the page in a real browser: tab visibility, interface cards, an add-address mutation, the audit view, and the whitelist refusal banner. """ from __future__ import annotations import os os.environ["WAGO_SIM_HOSTNET"] = "1" os.environ["WAGO_SIM_IN_DOCKER"] = "1" os.environ["WAGO_SIM_HOSTNET_SOCKET"] = "unused" # we inject in-process import asyncio from wago_sim.hostnet import (FakeBackend, HostNetAgent, HostNetService, InProcessClient) from wago_sim.webui.app import build_webui async def main() -> None: agent = HostNetAgent( backend=FakeBackend({ "eth0": ["192.168.1.10/24", "192.168.1.11/24"], "eth1": ["10.0.0.5/8"], "lo": ["127.0.0.1/8"], }), whitelist=["eth0", "eth1"], ) service = HostNetService(InProcessClient(agent)) from wago_sim.app import build_app from wago_sim.modules import DI_8, DI_16, DO_8, DO_16, AI_2, AO_2 sim = build_app(modules=[DI_8, DI_16, DO_8, DO_16, AI_2, AO_2]) webui = build_webui(sim, user="admin", password="simu", hostnet_service=service) import uvicorn config = uvicorn.Config(webui, host="127.0.0.1", port=8099, log_level="warning") server = uvicorn.Server(config) task = asyncio.create_task(server.serve()) for _ in range(100): if server.started or task.done(): break await asyncio.sleep(0.02) assert server.started, "web UI failed to start" # --- API probe (no browser needed for the data layer) ------------- import httpx async with httpx.AsyncClient(base_url="http://127.0.0.1:8099") as c: await c.post("/login", json={"user": "admin", "password": "simu"}) r = await c.get("/api/hostnet") assert r.status_code == 200, r.text data = r.json() assert data["status"]["available"] is True assert data["status"]["agent"]["whitelist"] == ["eth0", "eth1"] names = {i["name"] for i in data["interfaces"]} print("interfaces:", sorted(names)) r = await c.post("/api/hostnet/interfaces/eth0/addresses", json={"address": "192.168.1.33", "prefix": 24}) assert r.status_code == 200, r.text print("added 192.168.1.33 to eth0") r = await c.get("/api/hostnet/audit") assert "192.168.1.33" in "\n".join(r.json()["lines"]) print("audit:", r.json()["lines"][-2:]) # whitelist refusal surfaces as 403 with a clean message r = await c.post("/api/hostnet/interfaces/lo/addresses", json={"address": "127.0.0.2", "prefix": 8}) assert r.status_code == 403, r.text print("refused lo mutation:", r.json()["detail"][:60], "…") # restore walks the add back r = await c.post("/api/hostnet/interfaces/eth0/restore") assert r.status_code == 200 r = await c.get("/api/hostnet") eth0 = next(i for i in r.json()["interfaces"] if i["name"] == "eth0") addrs = [a["address"] for a in eth0["addresses"]] assert "192.168.1.33" not in addrs print("restored eth0 to:", addrs) print("ALL SMOKE CHECKS PASSED") if os.environ.get("HOSTNET_SMOKE_PERSIST"): print("PERSIST mode: serving on http://127.0.0.1:8099 until Ctrl+C") await asyncio.Event().wait() server.should_exit = True await asyncio.wait_for(task, timeout=5) if __name__ == "__main__": asyncio.run(main())