Files
vacuum-wall/tests/test_api.py
T
mteehan 332d14e37d ws: migrate push stream to data streaming
- daemon: send full snapshot on connect; versions/tick now carry the
  full state of one subsystem (subsystem + data); no legacy
  updated/subsystems payloads; refresh_state and POST /status/refresh
  broadcast per-subsystem versions with data
- client: modelSet() patches models in place; onMessage/topic refresh
  retired; 3s initial-load fallback via new POST /api/status/refresh
- schema: lib/schema.py TypedDicts + hoover/schema.js defaults +
  docs/state-model.md as single source of truth for state shapes
- system: poll at 1s, volatile metrics registered, dashboard uses a
  dedicated system model (status model removed)
- firewall: refuse to strip both https and ssh from the default zone
  (409, force override via UI confirm); set_zone_services persists
  services to the declarative config; collector exposes default_zone
- UI: pages migrate to flat state shapes; post-mutation modelFetch
  refreshes removed (WS delta covers it)
- tests: ws snapshot/delta/broadcast, refresh-state, schema types,
  model-set/js ws handler and reconnect fallback
2026-08-20 01:38:00 +00:00

943 lines
30 KiB
Python

"""
API integration tests — all blueprints tested via a single Flask app fixture.
Mocks daemon.client in each blueprint's module namespace to avoid needing
a running daemon.
"""
from unittest.mock import patch as _patch
import pytest
def _fw(func, **kw):
"""Patch daemon.client.{func} in the firewall blueprint namespace."""
return _patch(f"webui.api.firewall.{func}", **kw)
def _dh(func, **kw):
"""Patch daemon.client.{func} in the dhcp blueprint namespace."""
return _patch(f"webui.api.dhcp.{func}", **kw)
def _px(func, **kw):
"""Patch daemon.client.{func} in the proxy blueprint namespace."""
return _patch(f"webui.api.proxy.{func}", **kw)
def _ce(func, **kw):
"""Patch daemon.client.{func} in the certs blueprint namespace."""
return _patch(f"webui.api.certs.{func}", **kw)
def _wg(func, **kw):
"""Patch daemon.client.{func} in the wireguard blueprint namespace."""
return _patch(f"webui.api.wireguard.{func}", **kw)
def _ne(func, **kw):
"""Patch daemon.client.{func} in the network blueprint namespace."""
return _patch(f"webui.api.network.{func}", **kw)
def _st(func, **kw):
"""Patch daemon.client.{func} in the status blueprint namespace."""
return _patch(f"webui.api.status.{func}", **kw)
@pytest.fixture
def client():
from flask import Flask
from webui.api.certs import bp as certs_bp
from webui.api.dhcp import bp as dhcp_bp
from webui.api.firewall import bp as firewall_bp
from webui.api.network import bp as network_bp
from webui.api.proxy import bp as proxy_bp
from webui.api.wireguard import bp as wg_bp
app = Flask(__name__)
app.register_blueprint(firewall_bp, url_prefix="/api/firewall")
app.register_blueprint(network_bp, url_prefix="/api/network")
app.register_blueprint(dhcp_bp, url_prefix="/api/dhcp")
app.register_blueprint(proxy_bp, url_prefix="/api/proxy")
app.register_blueprint(certs_bp, url_prefix="/api/certs")
app.register_blueprint(wg_bp, url_prefix="/api/wireguard")
return app.test_client()
# ============================================================================
# Firewall
# ============================================================================
class TestFirewallListZones:
@_fw("get")
def test_success(self, mock_get, client):
mock_get.return_value = {
"active": {"public": ["eth0"]},
"available": ["public", "internal"],
}
resp = client.get("/api/firewall/zones")
assert resp.status_code == 200
data = resp.get_json()
assert data["ok"] is True
assert "public" in data["data"]["active"]
@_fw("get")
def test_runtime_error(self, mock_get, client):
mock_get.side_effect = RuntimeError("no sudo")
resp = client.get("/api/firewall/zones")
assert resp.status_code == 500
data = resp.get_json()
assert data["ok"] is False
class TestFirewallZoneDetails:
@_fw("get")
def test_success(self, mock_get, client):
mock_get.return_value = {"name": "public", "services": ["ssh"]}
resp = client.get("/api/firewall/zones/public")
assert resp.status_code == 200
data = resp.get_json()
assert data["data"]["name"] == "public"
class TestFirewallCreateZone:
@_fw("post")
@_fw("get")
def test_success(self, mock_get, mock_post, client):
mock_get.return_value = {"active": {}, "available": ["public", "internal"]}
mock_post.return_value = {"zone": "dmz"}
resp = client.post(
"/api/firewall/zones",
json={"name": "dmz", "target": "default"},
)
assert resp.status_code == 200
data = resp.get_json()
assert data["ok"] is True
def test_missing_name(self, client):
resp = client.post(
"/api/firewall/zones",
json={"target": "default"},
)
assert resp.status_code == 400
data = resp.get_json()
assert data["ok"] is False
class TestFirewallDeleteZone:
@_fw("delete")
def test_success(self, mock_delete, client):
mock_delete.return_value = {"zone": "dmz"}
resp = client.delete("/api/firewall/zones/dmz")
assert resp.status_code == 200
@_fw("delete")
def test_not_found(self, mock_delete, client):
from daemon.client import NotFound
mock_delete.side_effect = NotFound("Zone does not exist")
resp = client.delete("/api/firewall/zones/dmz")
assert resp.status_code == 404
class TestFirewallRichRules:
@_fw("post")
def test_add(self, mock_post, client):
mock_post.return_value = {
"id": "abc123",
"rule": "rule accept",
"zone": "public",
}
resp = client.post(
"/api/firewall/rich-rules",
json={
"zone": "public",
"rule": 'rule family="ipv4" port protocol="tcp" port="443" accept',
},
)
assert resp.status_code == 200
data = resp.get_json()
assert data["ok"] is True
assert data["data"]["id"] == "abc123"
def test_missing_fields(self, client):
resp = client.post("/api/firewall/rich-rules", json={})
assert resp.status_code == 400
@_fw("get")
def test_list(self, mock_get, client):
mock_get.return_value = [{"id": "a1", "rule": "rule1"}]
resp = client.get("/api/firewall/rich-rules/public")
assert resp.status_code == 200
data = resp.get_json()
assert isinstance(data["data"], list)
@_fw("delete")
def test_remove_by_id(self, mock_delete, client):
mock_delete.return_value = {"zone": "public", "id": "abc123"}
resp = client.delete("/api/firewall/rich-rules/public/abc123")
assert resp.status_code == 200
data = resp.get_json()
assert data["ok"] is True
@_fw("delete")
def test_remove_not_found(self, mock_delete, client):
from daemon.client import NotFound
mock_delete.side_effect = NotFound("not found")
resp = client.delete("/api/firewall/rich-rules/public/abc123")
assert resp.status_code == 404
class TestFirewallServices:
@_fw("get")
def test_list(self, mock_get, client):
mock_get.return_value = ["ssh", "http", "dns"]
resp = client.get("/api/firewall/services")
assert resp.status_code == 200
assert resp.get_json()["data"] == ["ssh", "http", "dns"]
class TestFirewallInterfaces:
@_fw("get")
def test_list(self, mock_get, client):
mock_get.return_value = [
{
"name": "eth0",
"mac": "aa:bb:cc:dd:ee:00",
"state": "UP",
"mtu": 1500,
"ips": ["192.168.1.1/24"],
"ipv6": [],
"zone": "internal",
},
{
"name": "eth1",
"mac": "aa:bb:cc:dd:ee:01",
"state": "UP",
"mtu": 1500,
"ips": ["10.0.0.1/24"],
"ipv6": [],
"zone": "public",
},
]
resp = client.get("/api/firewall/interfaces")
assert resp.status_code == 200
data = resp.get_json()["data"]
assert len(data) == 2
assert data[0]["name"] == "eth0"
class TestFirewallMasquerade:
@_fw("post")
def test_enable(self, mock_post, client):
mock_post.return_value = {"zone": "internal", "masquerade": True}
resp = client.post(
"/api/firewall/masquerade",
json={"zone": "internal", "enable": True},
)
assert resp.status_code == 200
def test_missing_fields(self, client):
resp = client.post("/api/firewall/masquerade", json={})
assert resp.status_code == 400
class TestFirewallForwardPort:
@_fw("post")
def test_add(self, mock_post, client):
mock_post.return_value = {
"id": "fp1",
"port": 443,
"proto": "tcp",
"zone": "public",
}
resp = client.post(
"/api/firewall/forward-port",
json={"zone": "public", "port": 443, "proto": "tcp"},
)
assert resp.status_code == 200
data = resp.get_json()
assert data["data"]["id"] == "fp1"
def test_missing_fields(self, client):
resp = client.post(
"/api/firewall/forward-port",
json={"zone": "public"},
)
assert resp.status_code == 400
@_fw("delete")
def test_remove_by_id(self, mock_delete, client):
mock_delete.return_value = {"zone": "public", "port": 443, "proto": "tcp"}
resp = client.delete("/api/firewall/forward-port/public/443/tcp")
assert resp.status_code == 200
data = resp.get_json()
assert data["ok"] is True
@_fw("delete")
def test_remove_not_found(self, mock_delete, client):
from daemon.client import NotFound
mock_delete.side_effect = NotFound("not found")
resp = client.delete("/api/firewall/forward-port/public/999/tcp")
assert resp.status_code == 404
def test_add_value_error(self, client):
resp = client.post(
"/api/firewall/forward-port",
json={"zone": "public", "port": "not_a_number", "proto": "tcp"},
)
assert resp.status_code == 400
class TestFirewallConfigApply:
@_fw("post")
def test_config_apply_success(self, mock_post, client):
mock_post.return_value = {
"applied_zones": ["public"],
"backup": "/tmp/rules.json",
}
resp = client.post("/api/firewall/config/apply")
assert resp.status_code == 200
data = resp.get_json()
assert data["ok"] is True
assert "public" in data["data"]["applied_zones"]
@_fw("post")
def test_config_apply_error(self, mock_post, client):
mock_post.side_effect = RuntimeError("apply failed")
resp = client.post("/api/firewall/config/apply")
assert resp.status_code == 500
data = resp.get_json()
assert data["ok"] is False
assert data["error"] == "apply failed"
# ============================================================================
# DHCP
# ============================================================================
class TestDhcpConfig:
@_dh("get")
def test_get(self, mock_get, client):
mock_get.return_value = {"dhcp": {}, "dns": {}}
resp = client.get("/api/dhcp/config")
assert resp.status_code == 200
assert resp.get_json()["ok"] is True
@_dh("post")
def test_post_invalid_body(self, mock_post, client):
resp = client.post(
"/api/dhcp/config", data="not json", content_type="text/plain"
)
data = resp.get_json()
assert data is not None
class TestDhcpApply:
@_dh("post")
def test_apply(self, mock_post, client):
mock_post.return_value = {"applied": True}
resp = client.post("/api/dhcp/apply")
assert resp.status_code == 200
data = resp.get_json()
assert data["ok"] is True
class TestDhcpStatus:
@_dh("get")
def test_success(self, mock_get, client):
mock_get.return_value = {"service_active": True}
resp = client.get("/api/dhcp/status")
assert resp.status_code == 200
data = resp.get_json()
assert data["data"]["service_active"] is True
class TestDhcpRanges:
@_dh("post")
def test_add_range(self, mock_post, client):
mock_post.return_value = {
"interface": "eth0",
"start": "192.168.1.100",
"end": "192.168.1.200",
}
resp = client.post(
"/api/dhcp/ranges",
json={
"interface": "eth0",
"start": "192.168.1.100",
"end": "192.168.1.200",
"lease_time": "2h",
},
)
assert resp.status_code == 200
data = resp.get_json()
assert data["ok"] is True
def test_add_range_missing_fields(self, client):
resp = client.post("/api/dhcp/ranges", json={"start": "192.168.1.100"})
assert resp.status_code == 400
@_dh("delete")
def test_remove_range(self, mock_delete, client):
mock_delete.return_value = {
"interface": "eth0",
"start": "192.168.1.100",
"end": "192.168.1.200",
}
resp = client.delete(
"/api/dhcp/ranges",
json={
"interface": "eth0",
"start": "192.168.1.100",
"end": "192.168.1.200",
},
)
assert resp.status_code == 200
data = resp.get_json()
assert data["ok"] is True
def test_remove_range_missing_fields(self, client):
resp = client.delete("/api/dhcp/ranges", json={})
assert resp.status_code == 400
class TestDhcpStaticLease:
@_dh("post")
def test_add(self, mock_post, client):
mock_post.return_value = {"mac": "AA:BB:CC", "ip": "10.0.0.5"}
resp = client.post(
"/api/dhcp/static-lease",
json={"mac": "AA:BB:CC", "ip": "10.0.0.5"},
)
assert resp.status_code == 200
def test_missing_mac(self, client):
resp = client.post("/api/dhcp/static-lease", json={"ip": "10.0.0.5"})
assert resp.status_code == 400
@_dh("delete")
def test_remove(self, mock_delete, client):
mock_delete.return_value = {"mac": "AA:BB:CC"}
resp = client.delete("/api/dhcp/static-lease/AA:BB:CC")
assert resp.status_code == 200
class TestDhcpDnsRecord:
@_dh("post")
def test_add(self, mock_post, client):
mock_post.return_value = {"name": "host.local", "address": "10.0.0.10"}
resp = client.post(
"/api/dhcp/dns-record",
json={"name": "host.local", "address": "10.0.0.10"},
)
assert resp.status_code == 200
def test_missing_fields(self, client):
resp = client.post("/api/dhcp/dns-record", json={})
assert resp.status_code == 400
@_dh("delete")
def test_remove(self, mock_delete, client):
mock_delete.return_value = {"name": "host.local"}
resp = client.delete("/api/dhcp/dns-record/host.local")
assert resp.status_code == 200
# ============================================================================
# Proxy
# ============================================================================
class TestProxyDomains:
@_px("get")
def test_list(self, mock_get, client):
mock_get.return_value = []
resp = client.get("/api/proxy/domains")
assert resp.status_code == 200
@_px("post")
def test_add(self, mock_post, client):
mock_post.return_value = {"domain": "ex.com"}
resp = client.post(
"/api/proxy/domains",
json={"domain": "ex.com", "backend": "webui"},
)
assert resp.status_code == 200
def test_add_missing_domain(self, client):
resp = client.post("/api/proxy/domains", json={})
assert resp.status_code == 400
class TestProxyApply:
@_px("post")
def test_apply(self, mock_post, client):
mock_post.return_value = {"applied": True}
resp = client.post("/api/proxy/apply")
assert resp.status_code == 200
class TestProxyTest:
@_px("post")
def test_valid(self, mock_post, client):
mock_post.return_value = {"valid": True, "output": "syntax ok"}
resp = client.post("/api/proxy/test")
assert resp.status_code == 200
data = resp.get_json()
assert data["data"]["valid"] is True
@_px("post")
def test_invalid(self, mock_post, client):
mock_post.return_value = {"valid": False, "output": "error msg"}
resp = client.post("/api/proxy/test")
assert resp.status_code == 400
data = resp.get_json()
assert data["ok"] is False
assert data["error"] == "error msg"
# ============================================================================
# Certs
# ============================================================================
class TestCertsList:
@_ce("get")
def test_list(self, mock_get, client):
mock_get.return_value = []
resp = client.get("/api/certs/list")
assert resp.status_code == 200
@_ce("get")
def test_details_not_found(self, mock_get, client):
from daemon.client import NotFound
mock_get.side_effect = NotFound("not found")
resp = client.get("/api/certs/example.com")
assert resp.status_code == 404
class TestCertsIssue:
def test_missing_domain(self, client):
resp = client.post("/api/certs/issue/start", json={})
assert resp.status_code == 400
class TestCertsEmail:
def test_missing_email(self, client):
resp = client.post("/api/certs/email", json={})
assert resp.status_code == 400
# ============================================================================
# WireGuard
# ============================================================================
class TestWireguardConfig:
@_wg("get")
def test_get(self, mock_get, client):
mock_get.return_value = {
"interface": {"name": "wg0"},
"peers": {},
}
resp = client.get("/api/wireguard/config")
data = resp.get_json()
assert data["ok"] is True
assert "private_key" not in data["data"]["interface"]
@_wg("post")
def test_post(self, mock_post, client):
mock_post.return_value = {"config_saved": True}
resp = client.post("/api/wireguard/config", json={"peers": {}})
assert resp.status_code == 200
@_wg("patch")
def test_patch(self, mock_patch, client):
mock_patch.return_value = {"config_saved": True}
resp = client.patch(
"/api/wireguard/config",
json={"interface": {"name": "wg0"}},
)
assert resp.status_code == 200
class TestWireguardPeers:
@_wg("get")
def test_list(self, mock_get, client):
mock_get.return_value = []
resp = client.get("/api/wireguard/peers")
assert resp.status_code == 200
@_wg("post")
def test_add(self, mock_post, client):
mock_post.return_value = {
"name": "client1",
"public_key": "pub",
}
resp = client.post(
"/api/wireguard/peers",
json={"name": "client1"},
)
data = resp.get_json()
assert data["ok"] is True
assert "private_key" not in data["data"]
def test_add_missing_name(self, client):
resp = client.post("/api/wireguard/peers", json={})
assert resp.status_code == 400
@_wg("delete")
def test_remove_by_name(self, mock_delete, client):
mock_delete.return_value = {"name": "client1"}
resp = client.delete("/api/wireguard/peers/client1")
assert resp.status_code == 200
class TestWireguardInitialize:
@_wg("post")
def test_initialize(self, mock_post, client):
mock_post.return_value = {"initialized": True}
resp = client.post("/api/wireguard/initialize")
data = resp.get_json()
assert data["ok"] is True
class TestWireguardGenerateClient:
def test_missing_name(self, client):
resp = client.post("/api/wireguard/generate-client", json={})
assert resp.status_code == 400
class TestWireguardStatus:
@_wg("get")
def test_get(self, mock_get, client):
mock_get.return_value = {"up": True, "interface": {}, "peers": []}
resp = client.get("/api/wireguard/status")
assert resp.status_code == 200
class TestWireguardUp:
@_wg("post")
def test_up_starts_tunnel(self, mock_post, client):
mock_post.return_value = {"applied": True}
resp = client.post("/api/wireguard/up")
assert resp.status_code == 200
@_wg("post")
def test_up_error(self, mock_post, client):
mock_post.side_effect = RuntimeError("interface down")
resp = client.post("/api/wireguard/up")
assert resp.status_code == 500
class TestWireguardDown:
@_wg("post")
def test_down_stops_tunnel(self, mock_post, client):
mock_post.return_value = {"down": True}
resp = client.post("/api/wireguard/down")
assert resp.status_code == 200
class TestWireguardApply:
@_wg("post")
def test_apply(self, mock_post, client):
mock_post.return_value = {"applied": True}
resp = client.post("/api/wireguard/apply")
assert resp.status_code == 200
# ============================================================================
# Helpers
# ============================================================================
class TestResponseHelpers:
@_fw("get")
def test_error_response_format(self, mock_get, client):
mock_get.side_effect = RuntimeError("fail")
resp = client.get("/api/firewall/zones")
data = resp.get_json()
assert "error" in data
assert "ok" in data
assert data["ok"] is False
# ============================================================================
# Firewall Config CRUD (daemon-backed)
# ============================================================================
class TestFirewallConfig:
@_fw("get")
def test_config_get(self, mock_get, client):
mock_get.return_value = {"zones": {"public": {"interfaces": ["eth0"]}}}
resp = client.get("/api/firewall/config")
assert resp.status_code == 200
@_fw("post")
@_fw("get")
def test_config_save(self, mock_get, mock_post, client):
mock_post.return_value = {"config_saved": True}
mock_get.return_value = {
"pending": [],
"needs_apply": False,
"unmanaged_zones": {},
}
resp = client.post(
"/api/firewall/config", json={"zones": {"public": {"interfaces": ["eth0"]}}}
)
assert resp.status_code == 200
def test_config_save_missing_zones(self, client):
resp = client.post("/api/firewall/config", json={})
assert resp.status_code == 400
@_fw("patch")
@_fw("get")
def test_config_patch(self, mock_get, mock_patch, client):
mock_patch.return_value = {"config_saved": True}
mock_get.return_value = {
"pending": [],
"needs_apply": False,
"unmanaged_zones": {},
}
resp = client.patch("/api/firewall/config", json={"zones": {}})
assert resp.status_code == 200
@_fw("post")
def test_config_apply(self, mock_post, client):
mock_post.return_value = {
"applied_zones": ["public"],
"backup": "/tmp/rules.json",
}
resp = client.post("/api/firewall/config/apply")
assert resp.status_code == 200
@_fw("get")
def test_config_pending(self, mock_get, client):
mock_get.return_value = {"pending": [], "needs_apply": False}
resp = client.get("/api/firewall/config/pending")
assert resp.status_code == 200
# ============================================================================
# Proxy config
# ============================================================================
class TestProxyConfig:
@_px("get")
def test_get(self, mock_get, client):
mock_get.return_value = {"domains": {}, "ssl": {}}
resp = client.get("/api/proxy/config")
assert resp.status_code == 200
@_px("post")
def test_post(self, mock_post, client):
mock_post.return_value = {"config_saved": True}
resp = client.post("/api/proxy/config", json={"domains": {}})
assert resp.status_code == 200
@_px("patch")
def test_patch(self, mock_patch, client):
mock_patch.return_value = {"config_saved": True}
resp = client.patch("/api/proxy/config", json={})
assert resp.status_code == 200
# ============================================================================
# DHCP config
# ============================================================================
class TestDhcpConfigCrud:
@_dh("patch")
def test_patch(self, mock_patch, client):
mock_patch.return_value = {"config_saved": True}
resp = client.patch("/api/dhcp/config", json={"dhcp": {}})
assert resp.status_code == 200
# ============================================================================
# Proxy management
# ============================================================================
# ============================================================================
# Proxy domain update
# ============================================================================
class TestProxyDomainUpdate:
@_px("post")
def test_update(self, mock_post, client):
mock_post.return_value = {"domain": "ex.com"}
resp = client.put(
"/api/proxy/domains/ex.com",
json={"backend_host": "10.0.0.2"},
)
assert resp.status_code == 200
# ============================================================================
# Network
# ============================================================================
class TestNetworkListInterfaces:
@_ne("get")
def test_success(self, mock_get, client):
mock_get.return_value = [
{"name": "eth0", "config": {"addresses": ["10.0.0.1/24"]}}
]
resp = client.get("/api/network/interfaces")
assert resp.status_code == 200
data = resp.get_json()
assert data["ok"] is True
@_ne("get")
def test_runtime_error(self, mock_get, client):
mock_get.side_effect = RuntimeError("networkctl not found")
resp = client.get("/api/network/interfaces")
assert resp.status_code == 500
assert resp.get_json()["ok"] is False
class TestNetworkGetInterface:
@_ne("get")
def test_success(self, mock_get, client):
mock_get.return_value = {
"name": "eth0",
"config": {"addresses": ["10.0.0.1/24"]},
}
resp = client.get("/api/network/interfaces/eth0")
assert resp.status_code == 200
data = resp.get_json()
assert data["ok"] is True
@_ne("get")
def test_not_found(self, mock_get, client):
from daemon.client import NotFound
mock_get.side_effect = NotFound("interface not found")
resp = client.get("/api/network/interfaces/nonexist")
assert resp.status_code == 404
class TestNetworkSaveInterface:
@_ne("post")
def test_success(self, mock_post, client):
mock_post.return_value = {"name": "eth0", "applied": True}
resp = client.post(
"/api/network/interfaces/eth0",
json={
"addresses": ["10.0.0.1/24"],
"gateway": "10.0.0.254",
"dns": ["8.8.8.8"],
"routes": [],
},
)
assert resp.status_code == 200
data = resp.get_json()
assert data["ok"] is True
@_ne("post")
def test_not_found(self, mock_post, client):
from daemon.client import NotFound
mock_post.side_effect = NotFound("interface not found")
resp = client.post("/api/network/interfaces/missing", json={})
assert resp.status_code == 404
class TestNetworkReloadInterface:
@_ne("post")
def test_success(self, mock_post, client):
mock_post.return_value = {"name": "eth0", "reloaded": True}
resp = client.post("/api/network/interfaces/eth0/reload")
assert resp.status_code == 200
assert resp.get_json()["ok"] is True
@_ne("post")
def test_runtime_error(self, mock_post, client):
mock_post.side_effect = RuntimeError("reload failed")
resp = client.post("/api/network/interfaces/eth0/reload")
assert resp.status_code == 500
class TestNetworkApplyAll:
@_ne("post")
def test_success(self, mock_post, client):
mock_post.return_value = {"applied": 2, "interfaces": ["eth0", "eth1"]}
resp = client.post("/api/network/apply")
assert resp.status_code == 200
data = resp.get_json()
assert data["ok"] is True
assert data["data"]["applied"] == 2
@_ne("post")
def test_runtime_error(self, mock_post, client):
mock_post.side_effect = RuntimeError("apply failed")
resp = client.post("/api/network/apply")
assert resp.status_code == 500
# ============================================================================
# Status
# ============================================================================
@pytest.fixture
def status_client():
from flask import Flask
from webui.api.status import bp as status_bp
app = Flask(__name__)
app.register_blueprint(status_bp, url_prefix="/api/status")
return app.test_client()
class TestStatusRefresh:
def test_filtered_subsystems_passed_through(self, status_client):
"""The subsystem body is forwarded to the daemon POST endpoint."""
from daemon.iface import POST_STATUS_REFRESH
with _st("post") as mock_post:
mock_post.return_value = {"firewall": {"zones": {}}}
resp = status_client.post(
"/api/status/refresh", json={"subsystems": ["firewall"]}
)
assert resp.status_code == 200
data = resp.get_json()
assert data["ok"] is True
assert data["data"] == {"firewall": {"zones": {}}}
mock_post.assert_called_once_with(
POST_STATUS_REFRESH, {"subsystems": ["firewall"]}
)
def test_empty_body_forwards_empty_dict(self, status_client):
"""An empty body becomes {} (daemon-side 'all subsystems' default)."""
from daemon.iface import POST_STATUS_REFRESH
with _st("post") as mock_post:
mock_post.return_value = {}
resp = status_client.post("/api/status/refresh")
assert resp.status_code == 200
mock_post.assert_called_once_with(POST_STATUS_REFRESH, {})
@_st("post")
def test_runtime_error(self, mock_post, status_client):
mock_post.side_effect = RuntimeError("no daemon")
resp = status_client.post("/api/status/refresh", json={})
assert resp.status_code == 500
assert resp.get_json()["ok"] is False