feat(11-04): add the websockets health, VAPID key and test-push commands
- centrifugo.Client.Info probes the info API method; an error body fails - websockets:health ports CentrifugoHealthCheck: exits 1 without an API key or when the probe fails, prints the Setting/Value table otherwise - websockets:generate-vapid-keys prints a new P-256 pair, shows configured keys only truncated, and --update persists them to overrides.yaml - websockets:test-push reads subscriptions from an app-published SubscriptionSource, refuses to send while push is disabled and sends one encrypted push per subscription - no command prints a configured private key or the Centrifugo API key - flare and lighthouse READMEs document the CLI commands
This commit is contained in:
129
modules/lighthouse/centrifugo/commands_test.go
Normal file
129
modules/lighthouse/centrifugo/commands_test.go
Normal file
@@ -0,0 +1,129 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user