fix htmx refactor route mismatches and remaining TODO items
- wireguard: POST /peers with JSON encoding (was /add-peer) - rules: delete by rule_id in URL path (was JSON body); pass rule objects with id from server; add hx-disable to initial render - nat: port forward delete uses URL path params to match blueprint - nat: masquerade toggle uses native hx-post/hx-vals (was inline fetch) - app.js renderers updated to use URL path deletes for rules and forwards - remove TODO.md
This commit is contained in:
+219
-20
@@ -1,3 +1,7 @@
|
||||
"""
|
||||
API integration tests — all blueprints tested via a single Flask app fixture.
|
||||
"""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
@@ -24,6 +28,11 @@ def client():
|
||||
return app.test_client()
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Firewall
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class TestFirewallListZones:
|
||||
@patch("webui.api.firewall.get_active_zones")
|
||||
@patch("webui.api.firewall.get_available_zones")
|
||||
@@ -100,7 +109,7 @@ class TestFirewallDeleteZone:
|
||||
class TestFirewallRichRules:
|
||||
@patch("webui.api.firewall.add_rich_rule")
|
||||
def test_add(self, mock_add, client):
|
||||
mock_add.return_value = None
|
||||
mock_add.return_value = {"id": "abc123", "rule": "rule accept"}
|
||||
resp = client.post(
|
||||
"/api/firewall/rich-rules",
|
||||
json={
|
||||
@@ -111,18 +120,35 @@ class TestFirewallRichRules:
|
||||
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
|
||||
|
||||
@patch("webui.api.firewall.get_rich_rules")
|
||||
def test_list(self, mock_list, client):
|
||||
mock_list.return_value = ["rule1", "rule2"]
|
||||
@patch("webui.api.firewall.config_get")
|
||||
def test_list(self, mock_cfg, mock_list, client):
|
||||
mock_list.return_value = ["rule1"]
|
||||
mock_cfg.return_value = {"zones": {"public": {"rich_rules": [{"id": "a1", "rule": "rule1"}]}}}
|
||||
resp = client.get("/api/firewall/rich-rules/public")
|
||||
assert resp.status_code == 200
|
||||
data = resp.get_json()
|
||||
assert data["data"] == ["rule1", "rule2"]
|
||||
assert isinstance(data["data"], list)
|
||||
|
||||
@patch("webui.api.firewall.remove_rich_rule_by_id")
|
||||
def test_remove_by_id(self, mock_remove, client):
|
||||
mock_remove.return_value = None
|
||||
resp = client.delete("/api/firewall/rich-rules/public/abc123")
|
||||
assert resp.status_code == 200
|
||||
data = resp.get_json()
|
||||
assert data["ok"] is True
|
||||
|
||||
@patch("webui.api.firewall.remove_rich_rule_by_id")
|
||||
def test_remove_not_found(self, mock_remove, client):
|
||||
mock_remove.side_effect = ValueError("not found")
|
||||
resp = client.delete("/api/firewall/rich-rules/public/abc123")
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
class TestFirewallServices:
|
||||
@@ -161,12 +187,14 @@ class TestFirewallMasquerade:
|
||||
class TestFirewallForwardPort:
|
||||
@patch("webui.api.firewall.add_forward_port")
|
||||
def test_add(self, mock_add, client):
|
||||
mock_add.return_value = None
|
||||
mock_add.return_value = {"id": "fp1", "port": 443, "proto": "tcp"}
|
||||
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(
|
||||
@@ -175,6 +203,25 @@ class TestFirewallForwardPort:
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
|
||||
@patch("webui.api.firewall.remove_forward_port_by_id")
|
||||
def test_remove_by_id(self, mock_remove, client):
|
||||
mock_remove.return_value = None
|
||||
resp = client.delete("/api/firewall/forward-port/public/443/tcp")
|
||||
assert resp.status_code == 200
|
||||
data = resp.get_json()
|
||||
assert data["ok"] is True
|
||||
|
||||
@patch("webui.api.firewall.remove_forward_port_by_id")
|
||||
def test_remove_not_found(self, mock_remove, client):
|
||||
mock_remove.side_effect = ValueError("not found")
|
||||
resp = client.delete("/api/firewall/forward-port/public/999/tcp")
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# DHCP
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class TestDhcpConfig:
|
||||
@patch("webui.api.dhcp.get_config")
|
||||
@@ -192,6 +239,67 @@ class TestDhcpConfig:
|
||||
assert data is not None
|
||||
|
||||
|
||||
class TestDhcpApply:
|
||||
@patch("webui.api.dhcp.apply_config")
|
||||
def test_apply(self, mock_apply, client):
|
||||
mock_apply.return_value = None
|
||||
resp = client.post("/api/dhcp/apply")
|
||||
assert resp.status_code == 200
|
||||
data = resp.get_json()
|
||||
assert data["ok"] is True
|
||||
|
||||
|
||||
class TestDhcpStatus:
|
||||
@patch("lib.dnsmasq.get_status")
|
||||
def test_success(self, mock_status, client):
|
||||
mock_status.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:
|
||||
@patch("webui.api.dhcp.set_dhcp_range")
|
||||
def test_add_range(self, mock_set, client):
|
||||
mock_set.return_value = None
|
||||
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
|
||||
|
||||
@patch("webui.api.dhcp.remove_dhcp_range")
|
||||
def test_remove_range(self, mock_remove, client):
|
||||
mock_remove.return_value = None
|
||||
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:
|
||||
@patch("webui.api.dhcp.add_static_lease")
|
||||
def test_add(self, mock_add, client):
|
||||
@@ -213,19 +321,15 @@ class TestDhcpStaticLease:
|
||||
"dhcp": {"static_leases": [{"mac": "AA:BB:CC", "ip": "10.0.0.5"}]}
|
||||
}
|
||||
mock_remove.return_value = None
|
||||
resp = client.delete("/api/dhcp/static-lease?mac=AA:BB:CC")
|
||||
resp = client.delete("/api/dhcp/static-lease/AA:BB:CC")
|
||||
assert resp.status_code == 200
|
||||
|
||||
@patch("webui.api.dhcp.get_config")
|
||||
def test_remove_not_found(self, mock_get, client):
|
||||
mock_get.return_value = {"dhcp": {"static_leases": []}}
|
||||
resp = client.delete("/api/dhcp/static-lease?mac=AA:BB:CC")
|
||||
resp = client.delete("/api/dhcp/static-lease/AA:BB:CC")
|
||||
assert resp.status_code == 404
|
||||
|
||||
def test_remove_missing_mac(self, client):
|
||||
resp = client.delete("/api/dhcp/static-lease")
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
class TestDhcpDnsRecord:
|
||||
@patch("webui.api.dhcp.add_dns_record")
|
||||
@@ -241,6 +345,27 @@ class TestDhcpDnsRecord:
|
||||
resp = client.post("/api/dhcp/dns-record", json={})
|
||||
assert resp.status_code == 400
|
||||
|
||||
@patch("webui.api.dhcp.remove_dns_record")
|
||||
@patch("webui.api.dhcp.get_config")
|
||||
def test_remove(self, mock_get, mock_remove, client):
|
||||
mock_get.return_value = {
|
||||
"dns": {"custom_records": [{"name": "host.local", "address": "10.0.0.10"}]}
|
||||
}
|
||||
mock_remove.return_value = None
|
||||
resp = client.delete("/api/dhcp/dns-record/host.local")
|
||||
assert resp.status_code == 200
|
||||
|
||||
@patch("webui.api.dhcp.get_config")
|
||||
def test_remove_not_found(self, mock_get, client):
|
||||
mock_get.return_value = {"dns": {"custom_records": []}}
|
||||
resp = client.delete("/api/dhcp/dns-record/host.local")
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Proxy
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class TestProxyDomains:
|
||||
@patch("webui.api.proxy.get_domains")
|
||||
@@ -271,6 +396,30 @@ class TestProxyApply:
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
class TestProxyTest:
|
||||
@patch("webui.api.proxy.test_config")
|
||||
def test_valid(self, mock_test, client):
|
||||
mock_test.return_value = (True, "syntax ok")
|
||||
resp = client.post("/api/proxy/test")
|
||||
assert resp.status_code == 200
|
||||
data = resp.get_json()
|
||||
assert data["data"]["valid"] is True
|
||||
|
||||
@patch("webui.api.proxy.test_config")
|
||||
def test_invalid(self, mock_test, client):
|
||||
mock_test.return_value = (False, "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:
|
||||
@patch("webui.api.certs.list_certs")
|
||||
def test_list(self, mock_list, client):
|
||||
@@ -297,6 +446,11 @@ class TestCertsEmail:
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# WireGuard
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class TestWireguardConfig:
|
||||
@patch("webui.api.wireguard.get_config")
|
||||
def test_get(self, mock_get, client):
|
||||
@@ -325,9 +479,9 @@ class TestWireguardPeers:
|
||||
|
||||
@patch("webui.api.wireguard.add_peer")
|
||||
def test_add(self, mock_add, client):
|
||||
mock_add.return_value = {"public_key": "pub", "private_key": "priv"}
|
||||
mock_add.return_value = {"name": "client1", "public_key": "pub", "private_key": "priv"}
|
||||
resp = client.post(
|
||||
"/api/wireguard/add-peer",
|
||||
"/api/wireguard/peers",
|
||||
json={"name": "client1"},
|
||||
)
|
||||
data = resp.get_json()
|
||||
@@ -335,21 +489,31 @@ class TestWireguardPeers:
|
||||
assert "private_key" not in data["data"]
|
||||
|
||||
def test_add_missing_name(self, client):
|
||||
resp = client.post("/api/wireguard/add-peer", json={})
|
||||
resp = client.post("/api/wireguard/peers", json={})
|
||||
assert resp.status_code == 400
|
||||
|
||||
@patch("webui.api.wireguard.remove_peer")
|
||||
@patch("webui.api.wireguard.get_config")
|
||||
def test_remove_by_name(self, mock_get, mock_remove, client):
|
||||
mock_get.return_value = {"peers": {"client1": {}}}
|
||||
mock_remove.return_value = None
|
||||
resp = client.delete("/api/wireguard/peers/client1")
|
||||
assert resp.status_code == 200
|
||||
|
||||
@patch("webui.api.wireguard.get_config")
|
||||
def test_remove_not_found(self, mock_get, client):
|
||||
mock_get.return_value = {"peers": {}}
|
||||
resp = client.delete("/api/wireguard/peers/unknown")
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
class TestWireguardInitialize:
|
||||
@patch("webui.api.wireguard.initialize")
|
||||
def test_initialize(self, mock_init, client):
|
||||
mock_init.return_value = {
|
||||
"interface": {"name": "wg0", "private_key": "priv"},
|
||||
"peers": {},
|
||||
}
|
||||
mock_init.return_value = None
|
||||
resp = client.post("/api/wireguard/initialize")
|
||||
data = resp.get_json()
|
||||
assert data["ok"] is True
|
||||
assert data["data"] is None
|
||||
|
||||
|
||||
class TestWireguardGenerateClient:
|
||||
@@ -366,6 +530,41 @@ class TestWireguardStatus:
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
class TestWireguardUp:
|
||||
@patch("webui.api.wireguard.apply")
|
||||
def test_up_starts_tunnel(self, mock_apply, client):
|
||||
mock_apply.return_value = None
|
||||
resp = client.post("/api/wireguard/up")
|
||||
assert resp.status_code == 200
|
||||
|
||||
@patch("webui.api.wireguard.apply")
|
||||
def test_up_error(self, mock_apply, client):
|
||||
mock_apply.side_effect = RuntimeError("interface down")
|
||||
resp = client.post("/api/wireguard/up")
|
||||
assert resp.status_code == 500
|
||||
|
||||
|
||||
class TestWireguardDown:
|
||||
@patch("webui.api.wireguard.down")
|
||||
def test_down_stops_tunnel(self, mock_down, client):
|
||||
mock_down.return_value = None
|
||||
resp = client.post("/api/wireguard/down")
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
class TestWireguardApply:
|
||||
@patch("webui.api.wireguard.apply")
|
||||
def test_apply(self, mock_apply, client):
|
||||
mock_apply.return_value = None
|
||||
resp = client.post("/api/wireguard/apply")
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Helpers
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class TestResponseHelpers:
|
||||
@patch("webui.api.firewall.get_active_zones")
|
||||
@patch("webui.api.firewall.get_available_zones")
|
||||
@@ -375,4 +574,4 @@ class TestResponseHelpers:
|
||||
data = resp.get_json()
|
||||
assert "error" in data
|
||||
assert "ok" in data
|
||||
assert data["ok"] is False
|
||||
assert data["ok"] is False
|
||||
Reference in New Issue
Block a user