Files
unkinben 45cb378022 Replace gitea-release plugin with curl-based release upload (#3)
Uses basic auth (droneci user) and Gitea API directly to create
the release and upload the binary asset.

Reviewed-on: #3
2026-03-25 19:45:34 +11:00

432 lines
12 KiB
Go

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])
}
}