package centrifugo import ( "bytes" "io" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "sync/atomic" "testing" "git.golem15.com/golem15/summercms/modules/backpack" "git.golem15.com/golem15/summercms/modules/bonfire" "git.golem15.com/golem15/summercms/modules/compass" ) const testAPIKey = "test-api-key-7f3c9e" func healthApp(t *testing.T, realtimeYAML string) *backpack.App { t.Helper() dir := t.TempDir() if err := os.WriteFile(filepath.Join(dir, "realtime.yaml"), []byte(realtimeYAML), 0o600); err != nil { t.Fatal(err) } cfg, err := compass.Open(compass.Options{Dir: dir, Env: "development", Environ: []string{}}) if err != nil { t.Fatal(err) } return backpack.New(cfg) } func runHealth(t *testing.T, app *backpack.App) (string, error) { t.Helper() var buf bytes.Buffer root, err := bonfire.NewRootIO("acme", Commands(app), strings.NewReader(""), &buf, &buf) if err != nil { t.Fatal(err) } root.SetArgs([]string{"websockets:health"}) err = root.Execute() return buf.String(), err } func TestHealthCommand(t *testing.T) { var hits atomic.Int32 var gotAuth, gotBody atomic.Value status := atomic.Int32{} answer := atomic.Value{} srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { hits.Add(1) if r.URL.Path != "/api/info" || r.Method != http.MethodPost { w.WriteHeader(http.StatusNotFound) return } body, _ := io.ReadAll(r.Body) gotAuth.Store(r.Header.Get("Authorization")) gotBody.Store(string(body)) w.WriteHeader(int(status.Load())) _, _ = io.WriteString(w, answer.Load().(string)) })) defer srv.Close() configured := "centrifugo:\n api_url: " + srv.URL + "/api\n api_key: " + testAPIKey + "\n" t.Run("missing API key", func(t *testing.T) { out, err := runHealth(t, healthApp(t, "centrifugo:\n api_url: "+srv.URL+"/api\n")) if err == nil { t.Fatalf("want an error exit:\n%s", out) } if !strings.Contains(out, "Centrifugo not configured (API key missing)") || !strings.Contains(out, "SUMMER_REALTIME__CENTRIFUGO__API_KEY") { t.Fatalf("output:\n%s", out) } if hits.Load() != 0 { t.Fatal("a request was sent without an API key") } }) t.Run("Centrifugo answers info", func(t *testing.T) { status.Store(http.StatusOK) answer.Store(`{"result":{"nodes":[{"name":"node-1"}]}}`) out, err := runHealth(t, healthApp(t, configured)) if err != nil { t.Fatalf("err = %v\n%s", err, out) } for _, want := range []string{"Checking Centrifugo connection...", "API URL: " + srv.URL + "/api", "Configuration OK", "Setting\tValue", "API URL\t" + srv.URL + "/api", "Enabled\tYes", "API Key Set\tYes"} { if !strings.Contains(out, want) { t.Errorf("output lacks %q:\n%s", want, out) } } if gotAuth.Load() != "apikey "+testAPIKey || gotBody.Load() != "{}" { t.Fatalf("info request: auth %v body %v", gotAuth.Load(), gotBody.Load()) } if strings.Contains(out, testAPIKey) { t.Fatalf("output contains the API key:\n%s", out) } }) t.Run("Centrifugo answers 500", func(t *testing.T) { status.Store(http.StatusInternalServerError) answer.Store(`oops`) out, err := runHealth(t, healthApp(t, configured)) if err == nil || !strings.Contains(out, "Connection check failed:") || strings.Contains(out, "Configuration OK") { t.Fatalf("err = %v\n%s", err, out) } if strings.Contains(out, testAPIKey) { t.Fatalf("output contains the API key:\n%s", out) } }) t.Run("Centrifugo answers an error body", func(t *testing.T) { status.Store(http.StatusOK) answer.Store(`{"error":{"code":101,"message":"unauthorized"}}`) out, err := runHealth(t, healthApp(t, configured)) if err == nil || !strings.Contains(out, "Connection check failed:") || !strings.Contains(out, "unauthorized") { t.Fatalf("err = %v\n%s", err, out) } }) t.Run("Centrifugo unreachable", func(t *testing.T) { dead := httptest.NewServer(http.NotFoundHandler()) url := dead.URL dead.Close() out, err := runHealth(t, healthApp(t, "centrifugo:\n api_url: "+url+"/api\n api_key: "+testAPIKey+"\n")) if err == nil || !strings.Contains(out, "Connection check failed:") || strings.Contains(out, testAPIKey) { t.Fatalf("err = %v\n%s", err, out) } }) }