package main import ( "encoding/json" "io" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" ) // ---- helpers ---------------------------------------------------------------- func newTestServer(t *testing.T, facts []fact) *httptest.Server { t.Helper() return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(facts) })) } func rawJSON(v interface{}) json.RawMessage { b, _ := json.Marshal(v) return json.RawMessage(b) } // ---- buildQuery ------------------------------------------------------------- func TestBuildQuery_NoFilters(t *testing.T) { q := buildQuery("", "", "", "", "enc_role", false) if !strings.Contains(q, "enc_role") { t.Fatalf("expected default role query, got %s", q) } } func TestBuildQuery_Node(t *testing.T) { q := buildQuery("host1", "", "", "", "enc_role", false) if !strings.Contains(q, "certname") || !strings.Contains(q, "host1") { t.Fatalf("unexpected query: %s", q) } } func TestBuildQuery_FactAndMatch(t *testing.T) { q := buildQuery("", "region", "syd1", "", "enc_role", false) if !strings.Contains(q, "region") || !strings.Contains(q, "syd1") { t.Fatalf("unexpected query: %s", q) } } func TestBuildQuery_PartialMatch(t *testing.T) { q := buildQuery("", "enc_role", "", "dns", "enc_role", false) if !strings.Contains(q, "~") || !strings.Contains(q, "dns") { t.Fatalf("expected partial match query, got %s", q) } } func TestBuildQuery_ShowRole(t *testing.T) { q := buildQuery("", "", "", "", "my_role_fact", true) if !strings.Contains(q, "my_role_fact") { t.Fatalf("expected role fact in query, got %s", q) } } func TestBuildQuery_CustomRoleFact(t *testing.T) { q := buildQuery("", "", "", "", "custom_role", true) if !strings.Contains(q, "custom_role") { t.Fatalf("expected custom role fact, got %s", q) } } // ---- valueString ------------------------------------------------------------ func TestValueString_String(t *testing.T) { raw := rawJSON("hello") if got := valueString(raw); got != "hello" { t.Fatalf("expected hello, got %s", got) } } func TestValueString_Number(t *testing.T) { raw := rawJSON(42) if got := valueString(raw); got != "42" { t.Fatalf("expected 42, got %s", got) } } func TestValueString_Object(t *testing.T) { raw := json.RawMessage(`{"a":1}`) got := valueString(raw) if got != `{"a":1}` { t.Fatalf("expected compact JSON, got %s", got) } } // ---- processResults --------------------------------------------------------- func TestProcessResults_Sorted(t *testing.T) { facts := []fact{ {Certname: "z-host", Value: rawJSON("val1")}, {Certname: "a-host", Value: rawJSON("val2")}, } out := processResults(facts) if out[0] != "a-host val2" || out[1] != "z-host val1" { t.Fatalf("unexpected order: %v", out) } } func TestProcessResults_Empty(t *testing.T) { out := processResults(nil) if len(out) != 0 { t.Fatalf("expected empty, got %v", out) } } // ---- countResults ----------------------------------------------------------- func TestCountResults_Basic(t *testing.T) { lines := []string{ "host1 syd1", "host2 syd1", "host3 mel1", } out := countResults(lines) found := map[string]bool{} for _, l := range out { found[l] = true } if !found["2: syd1"] { t.Fatalf("expected '2: syd1' in %v", out) } if !found["1: mel1"] { t.Fatalf("expected '1: mel1' in %v", out) } } func TestCountResults_Sorted(t *testing.T) { lines := []string{"h1 z", "h2 a", "h3 a"} out := countResults(lines) // lexicographic sort: "1: z" < "2: a" if out[0] != "1: z" || out[1] != "2: a" { t.Fatalf("unexpected order: %v", out) } } // ---- queryPuppetDB ---------------------------------------------------------- func TestQueryPuppetDB_Success(t *testing.T) { want := []fact{ {Certname: "node1", Name: "enc_role", Value: rawJSON("roles::dns")}, } srv := newTestServer(t, want) defer srv.Close() got, err := queryPuppetDB(srv.URL+"/pdb/query/v4/facts", `["=","name","enc_role"]`) if err != nil { t.Fatal(err) } if len(got) != 1 || got[0].Certname != "node1" { t.Fatalf("unexpected result: %v", got) } } func TestQueryPuppetDB_HTTPError(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "not found", http.StatusNotFound) })) defer srv.Close() _, err := queryPuppetDB(srv.URL+"/pdb/query/v4/facts", `["=","name","enc_role"]`) if err == nil { t.Fatal("expected error for 404") } } func TestQueryPuppetDB_BadJSON(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("not json")) })) defer srv.Close() _, err := queryPuppetDB(srv.URL+"/pdb/query/v4/facts", `["=","name","enc_role"]`) if err == nil { t.Fatal("expected decode error") } } func TestQueryPuppetDB_ConnectionRefused(t *testing.T) { _, err := queryPuppetDB("http://127.0.0.1:1/facts", `["=","name","enc_role"]`) if err == nil { t.Fatal("expected connection error") } } // ---- config ----------------------------------------------------------------- func TestLoadConfig_Defaults(t *testing.T) { t.Setenv("XDG_CONFIG_HOME", t.TempDir()) t.Setenv("NODE_LOOKUP_URL", "") t.Setenv("NODE_LOOKUP_ROLE_FACT", "") cfg, err := loadConfig() if err != nil { t.Fatal(err) } if cfg.PuppetDBURL != defaultPuppetDBURL { t.Fatalf("expected default URL, got %s", cfg.PuppetDBURL) } if cfg.RoleFact != defaultRoleFact { t.Fatalf("expected default role fact, got %s", cfg.RoleFact) } } func TestLoadConfig_EnvOverride(t *testing.T) { t.Setenv("XDG_CONFIG_HOME", t.TempDir()) t.Setenv("NODE_LOOKUP_URL", "http://custom:9090/facts") t.Setenv("NODE_LOOKUP_ROLE_FACT", "my_role") cfg, err := loadConfig() if err != nil { t.Fatal(err) } if cfg.PuppetDBURL != "http://custom:9090/facts" { t.Fatalf("env override failed: %s", cfg.PuppetDBURL) } if cfg.RoleFact != "my_role" { t.Fatalf("env override failed: %s", cfg.RoleFact) } } func TestLoadConfig_FileOverride(t *testing.T) { dir := t.TempDir() t.Setenv("XDG_CONFIG_HOME", dir) t.Setenv("NODE_LOOKUP_URL", "") t.Setenv("NODE_LOOKUP_ROLE_FACT", "") cfgDir := filepath.Join(dir, appName) if err := os.MkdirAll(cfgDir, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(cfgDir, configFileName), []byte("puppetdb_url: http://file:8080/facts\nrole_fact: file_role\n"), 0o644); err != nil { t.Fatal(err) } cfg, err := loadConfig() if err != nil { t.Fatal(err) } if cfg.PuppetDBURL != "http://file:8080/facts" { t.Fatalf("file override failed: %s", cfg.PuppetDBURL) } if cfg.RoleFact != "file_role" { t.Fatalf("file override failed: %s", cfg.RoleFact) } } func TestLoadConfig_EnvOverridesFile(t *testing.T) { dir := t.TempDir() t.Setenv("XDG_CONFIG_HOME", dir) t.Setenv("NODE_LOOKUP_URL", "http://env:9999/facts") t.Setenv("NODE_LOOKUP_ROLE_FACT", "") cfgDir := filepath.Join(dir, appName) if err := os.MkdirAll(cfgDir, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(cfgDir, configFileName), []byte("puppetdb_url: http://file:8080/facts\n"), 0o644); err != nil { t.Fatal(err) } cfg, err := loadConfig() if err != nil { t.Fatal(err) } if cfg.PuppetDBURL != "http://env:9999/facts" { t.Fatalf("env should beat file, got %s", cfg.PuppetDBURL) } } func TestLoadConfig_InvalidYAML(t *testing.T) { dir := t.TempDir() t.Setenv("XDG_CONFIG_HOME", dir) t.Setenv("NODE_LOOKUP_URL", "") t.Setenv("NODE_LOOKUP_ROLE_FACT", "") cfgDir := filepath.Join(dir, appName) if err := os.MkdirAll(cfgDir, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(cfgDir, configFileName), []byte(":\tinvalid: yaml:\n"), 0o644); err != nil { t.Fatal(err) } _, err := loadConfig() if err == nil { t.Fatal("expected error for invalid YAML") } } func TestWriteDefaultConfig(t *testing.T) { dir := t.TempDir() t.Setenv("XDG_CONFIG_HOME", dir) if err := writeDefaultConfig(); err != nil { t.Fatal(err) } path := filepath.Join(dir, appName, configFileName) data, err := os.ReadFile(path) if err != nil { t.Fatal(err) } if !strings.Contains(string(data), "puppetdb_url") { t.Fatalf("config file missing puppetdb_url: %s", data) } } func TestWriteDefaultConfig_AlreadyExists(t *testing.T) { dir := t.TempDir() t.Setenv("XDG_CONFIG_HOME", dir) if err := writeDefaultConfig(); err != nil { t.Fatal(err) } err := writeDefaultConfig() if err == nil { t.Fatal("expected error when config already exists") } } // ---- allFactsForNode -------------------------------------------------------- func TestAllFactsForNode_ReturnsSortedFacts(t *testing.T) { facts := []fact{ {Certname: "node1", Name: "zebra", Value: rawJSON("z-val")}, {Certname: "node1", Name: "alpha", Value: rawJSON("a-val")}, {Certname: "node1", Name: "middle", Value: rawJSON(42)}, } srv := newTestServer(t, facts) defer srv.Close() got, err := allFactsForNode(srv.URL+"/pdb/query/v4/facts", "node1") if err != nil { t.Fatal(err) } if len(got) != 3 { t.Fatalf("expected 3 facts, got %d", len(got)) } } func TestAllFactsForNode_QueryContainsCertname(t *testing.T) { var receivedQuery string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { receivedQuery = r.URL.Query().Get("query") w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode([]fact{}) })) defer srv.Close() _, _ = allFactsForNode(srv.URL+"/pdb/query/v4/facts", "mynode.example.com") if !strings.Contains(receivedQuery, "mynode.example.com") { t.Fatalf("expected certname in query, got: %s", receivedQuery) } if !strings.Contains(receivedQuery, "certname") { t.Fatalf("expected 'certname' in query, got: %s", receivedQuery) } } func TestAllFactsForNode_HTTPError(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "internal error", http.StatusInternalServerError) })) defer srv.Close() _, err := allFactsForNode(srv.URL+"/pdb/query/v4/facts", "node1") if err == nil { t.Fatal("expected error for HTTP 500") } } // ---- run with -a flag ------------------------------------------------------- func TestRun_AllFacts_RequiresNode(t *testing.T) { cfg := config{PuppetDBURL: "http://unused", RoleFact: "enc_role"} err := run(cfg, "", "", "", "", false, false, false, false, false, false, true) if err == nil || !strings.Contains(err.Error(), "-a requires -n") { t.Fatalf("expected -a requires -n error, got: %v", err) } } func TestRun_AllFacts_PrintsSortedByName(t *testing.T) { facts := []fact{ {Certname: "node1", Name: "zzz_fact", Value: rawJSON("last")}, {Certname: "node1", Name: "aaa_fact", Value: rawJSON("first")}, {Certname: "node1", Name: "mmm_fact", Value: rawJSON(true)}, } srv := newTestServer(t, facts) defer srv.Close() old := os.Stdout r, w, _ := os.Pipe() os.Stdout = w cfg := config{PuppetDBURL: srv.URL + "/pdb/query/v4/facts", RoleFact: "enc_role"} err := run(cfg, "node1", "", "", "", false, false, false, false, false, false, true) _ = w.Close() os.Stdout = old var buf strings.Builder _, _ = io.Copy(&buf, r) out := buf.String() if err != nil { t.Fatal(err) } lines := strings.Split(strings.TrimSpace(out), "\n") if len(lines) != 3 { t.Fatalf("expected 3 lines, got %d: %q", len(lines), out) } if !strings.HasPrefix(lines[0], "aaa_fact") { t.Errorf("first line should be aaa_fact, got: %s", lines[0]) } if !strings.HasPrefix(lines[1], "mmm_fact") { t.Errorf("second line should be mmm_fact, got: %s", lines[1]) } if !strings.HasPrefix(lines[2], "zzz_fact") { t.Errorf("third line should be zzz_fact, got: %s", lines[2]) } }