package api import ( "bytes" "io" "log" "net/http" "net/http/httptest" "os" "path/filepath" "testing" ) // newRestartTestRouter builds a minimal Router with a recording restart seam (so the // process is never actually killed) and a config path seeded with `prior`. func newRestartTestRouter(t *testing.T, prior []byte) (*Router, *int, string) { t.Helper() dir := t.TempDir() path := filepath.Join(dir, "controller.yaml") if prior != nil { if err := os.WriteFile(path, prior, 0o600); err != nil { t.Fatal(err) } } calls := 0 r := &Router{configPath: path, logger: log.New(io.Discard, "", 0)} r.restart = func() { calls++ } return r, &calls, path } // a minimal config that passes config.LoadFromBytes (customer.id + customer.domain required). var validConfig = []byte("customer:\n id: test\n domain: test.example\n") func TestConfigApply_ChangedConfig_Restarts(t *testing.T) { prior := []byte("customer:\n id: old\n domain: old.example\n") r, calls, path := newRestartTestRouter(t, prior) req := httptest.NewRequest(http.MethodPost, "/api/config/apply", bytes.NewReader(validConfig)) rec := httptest.NewRecorder() r.configApply(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String()) } if *calls != 1 { t.Fatalf("restart called %d times, want 1 (config changed)", *calls) } got, _ := os.ReadFile(path) if !bytes.Equal(got, validConfig) { t.Fatalf("config not written: got %q", got) } } // COMPANION: an identical re-push must NOT restart (the old handler always restarted / // always ran its post-apply hook regardless of whether anything changed). func TestConfigApply_IdenticalConfig_NoRestart(t *testing.T) { r, calls, _ := newRestartTestRouter(t, validConfig) // prior == body req := httptest.NewRequest(http.MethodPost, "/api/config/apply", bytes.NewReader(validConfig)) rec := httptest.NewRecorder() r.configApply(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String()) } if *calls != 0 { t.Fatalf("restart called %d times, want 0 (config byte-identical)", *calls) } } func TestConfigApply_InvalidYAML_NoRestart(t *testing.T) { r, calls, _ := newRestartTestRouter(t, validConfig) req := httptest.NewRequest(http.MethodPost, "/api/config/apply", bytes.NewReader([]byte("customer:\n id: only-id-no-domain\n"))) rec := httptest.NewRecorder() r.configApply(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("status = %d, want 400 (missing customer.domain)", rec.Code) } if *calls != 0 { t.Fatalf("restart called %d times, want 0 (validation failed)", *calls) } } func TestSelfRestart_InvokesRestarter(t *testing.T) { r, calls, _ := newRestartTestRouter(t, nil) req := httptest.NewRequest(http.MethodPost, "/api/selfrestart", nil) rec := httptest.NewRecorder() r.selfRestart(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want 200", rec.Code) } if *calls != 1 { t.Fatalf("restart called %d times, want 1", *calls) } }