package v1 import ( "encoding/json" "net/http" "net/http/httptest" "path/filepath" "testing" ) func setupTestNftables(t *testing.T) (*API, string) { tmpDir := t.TempDir() t.Setenv("WILD_CENTRAL_HAPROXY_CONFIG_PATH", filepath.Join(tmpDir, "haproxy.cfg")) t.Setenv("WILD_CENTRAL_NFTABLES_RULES_PATH", filepath.Join(tmpDir, "wild-central.nft")) api, err := NewAPI(tmpDir, "", nil) if err != nil { t.Fatalf("failed to create test API: %v", err) } return api, tmpDir } // TestNftablesStatus_ReturnsOK verifies the status endpoint returns 200. // GetStatus() runs `nft list table inet wild-cloud` and silently returns an // empty string if nft is not installed or the table doesn't exist, so this // endpoint always returns 200. func TestNftablesStatus_ReturnsOK(t *testing.T) { api, tmpDir := setupTestNftables(t) req := httptest.NewRequest("GET", "/api/v1/nftables/status", nil) w := httptest.NewRecorder() api.NftablesStatus(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) } var resp map[string]any if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("invalid JSON: %v", err) } if _, ok := resp["rulesFile"]; !ok { t.Error("expected 'rulesFile' field in response") } if _, ok := resp["rules"]; !ok { t.Error("expected 'rules' field in response") } rulesFile, _ := resp["rulesFile"].(string) expected := filepath.Join(tmpDir, "wild-central.nft") if rulesFile != expected { t.Errorf("expected rulesFile=%q, got %q", expected, rulesFile) } } // TestNftablesGetInterfaces_ReturnsOK verifies the interfaces endpoint returns 200 // with a valid interfaces list. Uses the real net.Interfaces() so we just check // structure — the test host will always have at least one interface (lo is excluded, // but any other up interface counts; if only loopback exists the list can be empty). func TestNftablesGetInterfaces_ReturnsOK(t *testing.T) { api, _ := setupTestNftables(t) req := httptest.NewRequest("GET", "/api/v1/nftables/interfaces", nil) w := httptest.NewRecorder() api.NftablesGetInterfaces(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) } var resp map[string]any if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("invalid JSON: %v", err) } ifaces, ok := resp["interfaces"] if !ok { t.Fatal("expected 'interfaces' field in response") } // Each entry must have a name and addresses array for _, entry := range ifaces.([]any) { iface := entry.(map[string]any) name, hasName := iface["name"].(string) if !hasName || name == "" { t.Errorf("interface entry missing non-empty name: %v", iface) } if _, hasAddrs := iface["addresses"]; !hasAddrs { t.Errorf("interface %q missing addresses field", name) } // Loopback must not appear if name == "lo" { t.Errorf("loopback interface 'lo' should be excluded") } } } // TestNftablesApply_ServiceUnavailable verifies the handler returns 500 when // the nftables reload service is not available (expected in test environments). func TestNftablesApply_ServiceUnavailable(t *testing.T) { api, _ := setupTestNftables(t) req := httptest.NewRequest("POST", "/api/v1/nftables/apply", nil) w := httptest.NewRecorder() api.NftablesApply(w, req) // ApplyRules calls `systemctl start wild-cloud-nftables-reload.service` // which requires polkit/root — always fails in test environments. if w.Code != http.StatusInternalServerError { // If it happened to succeed (running on the actual Wild Central device // with the right polkit rules), that's also valid. if w.Code != http.StatusOK { t.Errorf("unexpected status %d: %s", w.Code, w.Body.String()) } return } var resp map[string]any if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("invalid JSON: %v", err) } if resp["error"] == nil { t.Error("expected 'error' field in 500 response") } }