package opnsense import ( "context" "encoding/json" "net/http" "net/http/httptest" "strings" "sync" "testing" "time" "git.dern.ovh/infrastructure/caddy-opnsense-blocker/internal/config" ) func TestClientCreatesAliasAndBlocksAndUnblocksIPs(t *testing.T) { t.Parallel() type state struct { mu sync.Mutex aliasUUID string aliasExists bool ips map[string]struct{} } backendState := &state{ips: map[string]struct{}{}} server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { username, password, ok := r.BasicAuth() if !ok || username != "key" || password != "secret" { http.Error(w, "unauthorized", http.StatusUnauthorized) return } w.Header().Set("Content-Type", "application/json") backendState.mu.Lock() defer backendState.mu.Unlock() switch { case r.Method == http.MethodGet && r.URL.Path == "/api/firewall/alias/get_alias_u_u_i_d/blocked-ips": if backendState.aliasExists { _ = json.NewEncoder(w).Encode(map[string]any{"uuid": backendState.aliasUUID}) } else { _ = json.NewEncoder(w).Encode(map[string]any{"uuid": ""}) } case r.Method == http.MethodPost && r.URL.Path == "/api/firewall/alias/add_item": backendState.aliasExists = true backendState.aliasUUID = "uuid-1" _ = json.NewEncoder(w).Encode(map[string]any{"status": "ok"}) case r.Method == http.MethodPost && r.URL.Path == "/api/firewall/alias/set_item/uuid-1": _ = json.NewEncoder(w).Encode(map[string]any{"status": "ok"}) case r.Method == http.MethodPost && r.URL.Path == "/api/firewall/alias/reconfigure": _ = json.NewEncoder(w).Encode(map[string]any{"status": "ok"}) case r.Method == http.MethodGet && r.URL.Path == "/api/firewall/alias_util/list/blocked-ips": rows := make([]map[string]string, 0, len(backendState.ips)) for ip := range backendState.ips { rows = append(rows, map[string]string{"ip": ip}) } _ = json.NewEncoder(w).Encode(map[string]any{"rows": rows}) case r.Method == http.MethodPost && r.URL.Path == "/api/firewall/alias_util/add/blocked-ips": var payload map[string]string if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } backendState.ips[payload["address"]] = struct{}{} _ = json.NewEncoder(w).Encode(map[string]any{"status": "done"}) case r.Method == http.MethodPost && r.URL.Path == "/api/firewall/alias_util/delete/blocked-ips": var payload map[string]string if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } delete(backendState.ips, payload["address"]) _ = json.NewEncoder(w).Encode(map[string]any{"status": "done"}) default: http.Error(w, "not found", http.StatusNotFound) } })) defer server.Close() client := NewClient(config.OPNsenseConfig{ Enabled: true, BaseURL: server.URL, APIKey: "key", APISecret: "secret", EnsureAlias: true, Timeout: config.Duration{Duration: time.Second}, Alias: config.AliasConfig{ Name: "blocked-ips", Type: "host", }, APIPaths: config.APIPathsConfig{ AliasGetUUID: "/api/firewall/alias/get_alias_u_u_i_d/{alias}", AliasAddItem: "/api/firewall/alias/add_item", AliasSetItem: "/api/firewall/alias/set_item/{uuid}", AliasReconfig: "/api/firewall/alias/reconfigure", AliasUtilList: "/api/firewall/alias_util/list/{alias}", AliasUtilAdd: "/api/firewall/alias_util/add/{alias}", AliasUtilDelete: "/api/firewall/alias_util/delete/{alias}", }, }) ctx := context.Background() if result, err := client.AddIPIfMissing(ctx, "203.0.113.10"); err != nil || result != "added" { t.Fatalf("unexpected add result: result=%q err=%v", result, err) } if result, err := client.AddIPIfMissing(ctx, "203.0.113.10"); err != nil || result != "already_present" { t.Fatalf("unexpected add replay result: result=%q err=%v", result, err) } present, err := client.IsIPPresent(ctx, "203.0.113.10") if err != nil { t.Fatalf("is ip present: %v", err) } if !present { t.Fatalf("expected IP to be present in alias") } if result, err := client.RemoveIPIfPresent(ctx, "203.0.113.10"); err != nil || result != "removed" { t.Fatalf("unexpected remove result: result=%q err=%v", result, err) } if result, err := client.RemoveIPIfPresent(ctx, "203.0.113.10"); err != nil || result != "already_absent" { t.Fatalf("unexpected remove replay result: result=%q err=%v", result, err) } backendState.mu.Lock() defer backendState.mu.Unlock() if !backendState.aliasExists || backendState.aliasUUID == "" { t.Fatalf("expected alias to exist after first add") } if len(backendState.ips) != 0 { t.Fatalf("expected alias to be empty after remove, got %v", backendState.ips) } if strings.TrimSpace(backendState.aliasUUID) == "" { t.Fatalf("expected alias uuid to be populated") } }