package puppet import ( "encoding/json" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" ) // ---- config ----------------------------------------------------------------- func TestLoad_Defaults(t *testing.T) { t.Setenv("XDG_CONFIG_HOME", t.TempDir()) t.Setenv("NODE_LOOKUP_URL", "") t.Setenv("NODE_LOOKUP_ROLE_FACT", "") t.Setenv("NODE_LOOKUP_PUPPETBOARD_URL", "") cfg, err := Load() if err != nil { t.Fatal(err) } if cfg.PuppetDBURL != DefaultPuppetDBURL { t.Fatalf("expected default puppetdb url, got %s", cfg.PuppetDBURL) } if cfg.PuppetboardURL != DefaultPuppetboardURL { t.Fatalf("expected default puppetboard url, got %s", cfg.PuppetboardURL) } } func TestLoad_FileAndEnvOverride(t *testing.T) { dir := t.TempDir() t.Setenv("XDG_CONFIG_HOME", dir) t.Setenv("NODE_LOOKUP_URL", "") t.Setenv("NODE_LOOKUP_ROLE_FACT", "") t.Setenv("NODE_LOOKUP_PUPPETBOARD_URL", "https://env.example.net") cfgDir := filepath.Join(dir, appName) if err := os.MkdirAll(cfgDir, 0o755); err != nil { t.Fatal(err) } body := "puppetdb_url: http://file:8080/pdb/query/v4/facts\npuppetboard_url: https://file.example.net\n" if err := os.WriteFile(filepath.Join(cfgDir, configFileName), []byte(body), 0o644); err != nil { t.Fatal(err) } cfg, err := Load() if err != nil { t.Fatal(err) } if cfg.PuppetDBURL != "http://file:8080/pdb/query/v4/facts" { t.Fatalf("file override failed: %s", cfg.PuppetDBURL) } // env beats the file for puppetboard_url if cfg.PuppetboardURL != "https://env.example.net" { t.Fatalf("env should beat file: %s", cfg.PuppetboardURL) } } // ---- NodesEndpoint ---------------------------------------------------------- func TestNodesEndpoint(t *testing.T) { cases := map[string]string{ "http://puppetdbapi.service.consul:8080/pdb/query/v4/facts": "http://puppetdbapi.service.consul:8080/pdb/query/v4/nodes", "http://h:8080/pdb/query/v4/facts/": "http://h:8080/pdb/query/v4/nodes", "https://h/facts": "https://h/nodes", } for in, want := range cases { if got := NodesEndpoint(in); got != want { t.Errorf("NodesEndpoint(%q) = %q, want %q", in, got, want) } } } // ---- HostPageURL ------------------------------------------------------------ func TestHostPageURL(t *testing.T) { if got := HostPageURL("https://pb.example.net", "h1.example.net"); got != "https://pb.example.net/node/h1.example.net" { t.Fatalf("unexpected url: %s", got) } // trailing slash on the base is trimmed if got := HostPageURL("https://pb.example.net/", "h1"); got != "https://pb.example.net/node/h1" { t.Fatalf("trailing slash not handled: %s", got) } } // ---- LookupNode ------------------------------------------------------------- func TestLookupNode_Found(t *testing.T) { var gotQuery string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotQuery = r.URL.Query().Get("query") _ = json.NewEncoder(w).Encode([]Node{{ Certname: "h1", ReportTimestamp: "2026-07-15T04:05:06.000Z", LatestReportStatus: "changed", }}) })) defer srv.Close() node, err := LookupNode(srv.URL, "h1") if err != nil { t.Fatal(err) } if node == nil || node.ReportTimestamp != "2026-07-15T04:05:06.000Z" { t.Fatalf("unexpected node: %+v", node) } if !strings.Contains(gotQuery, "certname") || !strings.Contains(gotQuery, "h1") { t.Fatalf("query missing certname filter: %s", gotQuery) } } func TestLookupNode_Unknown(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _ = json.NewEncoder(w).Encode([]Node{}) })) defer srv.Close() node, err := LookupNode(srv.URL, "nope") if err != nil { t.Fatal(err) } if node != nil { t.Fatalf("expected nil node for unknown certname, got %+v", node) } } func TestLookupNode_HTTPError(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "boom", http.StatusInternalServerError) })) defer srv.Close() if _, err := LookupNode(srv.URL, "h1"); err == nil { t.Fatal("expected error for HTTP 500") } } // ---- ReadHosts -------------------------------------------------------------- func TestReadHosts_ArgsWin(t *testing.T) { // A real pipe with data present, but explicit args should take precedence. r, w, _ := os.Pipe() go func() { _, _ = w.WriteString("piped\n"); _ = w.Close() }() defer func() { _ = r.Close() }() got := ReadHosts(r, []string{"a", "b", "a"}) if strings.Join(got, ",") != "a,b" { t.Fatalf("expected deduped args, got %v", got) } } func TestReadHosts_StdinFirstField(t *testing.T) { r, w, _ := os.Pipe() go func() { // node-lookup default output: "host value"; also a bare host and a dup. _, _ = w.WriteString("host1 roles::web\nhost2 roles::db\nhost1 roles::web\nhost3\n") _ = w.Close() }() defer func() { _ = r.Close() }() got := ReadHosts(r, nil) if strings.Join(got, ",") != "host1,host2,host3" { t.Fatalf("expected first-field hosts deduped, got %v", got) } } func TestReadHosts_NoInput(t *testing.T) { // /dev/null is a char device: no args, no pipe data -> nil. f, _ := os.Open(os.DevNull) defer func() { _ = f.Close() }() if got := ReadHosts(f, nil); got != nil { t.Fatalf("expected nil for no input, got %v", got) } }