package edgeconfig import ( "context" "errors" "net/http" "net/http/httptest" "os" "path/filepath" "sync" "testing" ) // --- Fragment / Writer --- type testFrag struct { name string data string err error } func (f testFrag) Name() string { return f.name } func (f testFrag) Render() ([]byte, error) { if f.err != nil { return nil, f.err } return []byte(f.data), nil } func TestWriterAtomicAndOrdering(t *testing.T) { dir := t.TempDir() w := NewWriter(dir) changed, err := w.Write( testFrag{name: "20-routes.conf", data: "# routes"}, testFrag{name: "10-auth.conf", data: "# auth"}, ) if err != nil { t.Fatal(err) } if len(changed) != 2 || changed[0] != "10-auth.conf" || changed[1] != "20-routes.conf" { t.Fatalf("changed = %v, want sorted [10-auth.conf 20-routes.conf]", changed) } for _, name := range []string{"10-auth.conf", "20-routes.conf"} { data, err := os.ReadFile(filepath.Join(dir, name)) if err != nil { t.Fatal(err) } if len(data) == 0 { t.Fatalf("%s: empty", name) } } // no tmp leftovers entries, _ := os.ReadDir(dir) if len(entries) != 2 { t.Fatalf("dir has %d entries, want exactly the 2 fragments", len(entries)) } } func TestWriterRenderErrorAbortsFlush(t *testing.T) { dir := t.TempDir() w := NewWriter(dir) _, err := w.Write( testFrag{name: "10-auth.conf", data: "# ok"}, testFrag{name: "20-bad.conf", err: errors.New("boom")}, ) if err == nil { t.Fatal("want render error") } // nothing written — previous complete set keeps serving if _, statErr := os.Stat(filepath.Join(dir, "10-auth.conf")); !os.IsNotExist(statErr) { t.Fatal("flush must be all-or-nothing on render error") } } func TestWriterRejectsBadNames(t *testing.T) { dir := t.TempDir() w := NewWriter(dir) for _, bad := range []string{"../evil", "sub/dir.conf", "", ".."} { if _, err := w.Write(testFrag{name: bad, data: "x"}); err == nil { t.Fatalf("name %q accepted", bad) } } } // --- HTTPReloader + PostReload against a fake handler --- func TestHTTPReloaderFanOut(t *testing.T) { var mu sync.Mutex var hits int srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { mu.Lock() hits++ mu.Unlock() w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"reloaded":true,"checksum":42}`)) })) defer srv.Close() host := srv.Listener.Addr().String() r, err := NewHTTPReloader(host + "," + host) if err != nil { t.Fatal(err) } res := r.Reload(context.Background()) if !res.OK() { t.Fatalf("want OK, errs=%v", res.Errs()) } if hits != 2 { t.Fatalf("hits=%d, want 2 (one per target)", hits) } } func TestHTTPReloaderPartialFailure(t *testing.T) { ok := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`{"reloaded":true}`)) })) defer ok.Close() bad := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, `{"message":"nginx -t failed"}`, http.StatusInternalServerError) })) defer bad.Close() r, err := NewHTTPReloader(ok.Listener.Addr().String() + "," + bad.Listener.Addr().String()) if err != nil { t.Fatal(err) } res := r.Reload(context.Background()) if res.OK() { t.Fatal("want failure when one target fails") } errs := res.Errs() if len(errs) != 1 { t.Fatalf("errs=%v, want exactly one (the failing target)", errs) } } func TestNewHTTPReloaderValidation(t *testing.T) { if _, err := NewHTTPReloader(""); err == nil { t.Fatal("empty targets accepted") } if _, err := NewHTTPReloader("no-port-here"); err == nil { t.Fatal("target without port accepted") } if _, err := NewHTTPReloader(" a:1 , b:2 "); err != nil { t.Fatalf("whitespace-separated list rejected: %v", err) } } // --- KubeReloader with injected lister/post (no k8s client in tests) --- func TestKubeReloaderFansOutToAllPods(t *testing.T) { var mu sync.Mutex got := map[string]bool{} k, err := NewKubeReloader(func(ctx context.Context) ([]string, error) { return []string{"10.0.0.4", "10.0.0.5"}, nil }, "8081") if err != nil { t.Fatal(err) } k.post = func(ctx context.Context, target string) (HandlerResult, error) { mu.Lock() got[target] = true mu.Unlock() return HandlerResult{Reloaded: true}, nil } res := k.Reload(context.Background()) if !res.OK() { t.Fatalf("want OK, errs=%v", res.Errs()) } for _, want := range []string{"10.0.0.4:8081", "10.0.0.5:8081"} { if !got[want] { t.Fatalf("pod %s not reloaded; got %v", want, got) } } } func TestKubeReloaderPodListFailure(t *testing.T) { k, err := NewKubeReloader(func(ctx context.Context) ([]string, error) { return nil, errors.New("api server down") }, "") if err != nil { t.Fatal(err) } res := k.Reload(context.Background()) if res.OK() { t.Fatal("pod-list failure must surface as error") } } func TestKubeReloaderNoPods(t *testing.T) { k, _ := NewKubeReloader(func(ctx context.Context) ([]string, error) { return nil, nil }, "") if res := k.Reload(context.Background()); res.OK() { t.Fatal("zero pods must be an error, not silent success") } } func TestNullReloader(t *testing.T) { if res := (NullReloader{}).Reload(context.Background()); !res.OK() { t.Fatal("null reloader must succeed") } }