test(11-07): cover lighthouse realtime and the Centrifugo driver
- lighthouse: TestSuppression (Widget silenced, Gadget not, nesting, stale outer ctx), TestBulkEmitsOnce, TestBroadcastEdges (zero-key batch, update actor, id-only delete, method contract, multi-channel, savepoint), TestBroadcastPublishFailure, TestFromSelectsDriver, TestMountSurfaces, TestRegistry under -race, drivers, args JSON, Bind (coverage 91.7%) - centrifugo: TestTokenClaims, TestTokenHandler, TestClientRequests, TestClientLoadConfig and a TestProxy table porting the WinterCMS WS-005, WS-007 and WS-013 cases (coverage 92.4%)
This commit is contained in:
248
modules/lighthouse/lighthouse_test.go
Normal file
248
modules/lighthouse/lighthouse_test.go
Normal file
@@ -0,0 +1,248 @@
|
||||
package lighthouse
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/backpack"
|
||||
"git.golem15.com/golem15/summercms/modules/compass"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func configApp(t *testing.T, kv map[string]any) *backpack.App {
|
||||
t.Helper()
|
||||
cfg, err := compass.Open(compass.Options{Dir: t.TempDir(), Env: "testing", Environ: []string{}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for k, v := range kv {
|
||||
if err := cfg.Set(k, v); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
return backpack.New(cfg)
|
||||
}
|
||||
|
||||
// TestFromSelectsDriver covers D-11: realtime.driver picks a registered
|
||||
// driver (null by default, case-insensitive), an unknown name fails and
|
||||
// lists the registered drivers, and the realtime.* settings are read once.
|
||||
func TestFromSelectsDriver(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
driver any
|
||||
want string
|
||||
}{
|
||||
{"default_null", nil, "null"},
|
||||
{"memory", "memory", "memory"},
|
||||
{"log_case_insensitive", " LOG ", "log"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
kv := map[string]any{}
|
||||
if c.driver != nil {
|
||||
kv["realtime.driver"] = c.driver
|
||||
}
|
||||
svc, err := From(configApp(t, kv))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := svc.Driver().Name(); got != c.want {
|
||||
t.Fatalf("driver = %s, want %s", got, c.want)
|
||||
}
|
||||
if svc.Driver().Routes() != nil {
|
||||
t.Fatalf("%s driver declares routes", c.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
t.Run("unknown_lists_registered", func(t *testing.T) {
|
||||
_, err := From(configApp(t, map[string]any{"realtime.driver": "nope"}))
|
||||
if err == nil || !strings.Contains(err.Error(), `unknown realtime.driver "nope"`) || !strings.Contains(err.Error(), "acme-nil-driver, log, memory, null") {
|
||||
t.Fatalf("err = %v", err)
|
||||
}
|
||||
})
|
||||
t.Run("settings_and_idempotence", func(t *testing.T) {
|
||||
app := configApp(t, map[string]any{
|
||||
"realtime.broadcast_namespace": " acme ",
|
||||
"realtime.broadcast_queue": "rt",
|
||||
"realtime.broadcast_timeout": 7,
|
||||
})
|
||||
svc, err := From(app)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if svc.Namespace() != "acme" || svc.Queue() != "rt" || svc.Timeout() != 7*time.Second || svc.Registry() == nil {
|
||||
t.Fatalf("settings = %q %q %s", svc.Namespace(), svc.Queue(), svc.Timeout())
|
||||
}
|
||||
again, err := From(app)
|
||||
if err != nil || again != svc {
|
||||
t.Fatalf("From twice = %p, %v", again, err)
|
||||
}
|
||||
if _, err := From(nil); err == nil {
|
||||
t.Fatal("From(nil) succeeded")
|
||||
}
|
||||
def, err := From(backpack.New(nil))
|
||||
if err != nil || def.Queue() != DefaultQueue || def.Timeout() != DefaultTimeout || def.Namespace() != "" {
|
||||
t.Fatalf("config-less service = %+v, %v", def, err)
|
||||
}
|
||||
})
|
||||
t.Run("nil_service_accessors", func(t *testing.T) {
|
||||
var s *Service
|
||||
if s.Driver() != nil || s.Registry() != nil || s.Namespace() != "" || s.Queue() != DefaultQueue || s.Timeout() != DefaultTimeout || s.Logger() == nil {
|
||||
t.Fatal("nil service accessors")
|
||||
}
|
||||
s.SetUserLookup(nil)
|
||||
if u, found, err := s.User(context.Background(), 4); err != nil || !found || u.ID != 4 {
|
||||
t.Fatalf("nil service User = %+v %v %v", u, found, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestDurationSetting covers the seconds-or-duration setting parser.
|
||||
func TestDurationSetting(t *testing.T) {
|
||||
app := configApp(t, map[string]any{"a": 5, "b": "250ms", "c": "soon", "d": -3, "e": " "})
|
||||
for key, want := range map[string]time.Duration{"a": 5 * time.Second, "b": 250 * time.Millisecond, "c": 0, "d": 0, "e": 0, "missing": 0} {
|
||||
if got := DurationSetting(app.Config, key); got != want {
|
||||
t.Errorf("DurationSetting(%s) = %s, want %s", key, got, want)
|
||||
}
|
||||
}
|
||||
if DurationSetting(nil, "a") != 0 {
|
||||
t.Fatal("nil config")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDrivers covers the built-in drivers: the log driver logs channels and
|
||||
// event but never the payload, the memory driver records copies, and the
|
||||
// registry panics on misuse.
|
||||
func TestDrivers(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
d := &logDriver{log: slog.New(slog.NewTextHandler(&buf, nil))}
|
||||
ctx := context.Background()
|
||||
if err := d.Publish(ctx, "room:1", "acme.ping", json.RawMessage(`{"secret":"payload-value"}`)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := d.Broadcast(ctx, []string{"room:1", "room:2"}, "acme.ping", json.RawMessage(`{"secret":"payload-value"}`)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out := buf.String()
|
||||
if !strings.Contains(out, "channel=room:1") || !strings.Contains(out, "event=acme.ping") || strings.Contains(out, "payload-value") {
|
||||
t.Fatalf("log driver output = %s", out)
|
||||
}
|
||||
if (nullDriver{}).Publish(ctx, "", "", nil) != nil || (nullDriver{}).Broadcast(ctx, nil, "", nil) != nil {
|
||||
t.Fatal("null driver returned an error")
|
||||
}
|
||||
|
||||
m := NewMemoryDriver()
|
||||
payload := json.RawMessage(`{"a":1}`)
|
||||
channels := []string{"x:1", "x:2"}
|
||||
if err := m.Broadcast(ctx, channels, "e", payload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
channels[0] = "mutated"
|
||||
payload[2] = 'Z'
|
||||
pubs := m.Publications()
|
||||
if len(pubs) != 1 || pubs[0].Method != "broadcast" || pubs[0].Channels[0] != "x:1" || string(pubs[0].Payload) != `{"a":1}` || m.Name() != "memory" || m.Routes() != nil {
|
||||
t.Fatalf("memory driver kept references: %+v", pubs)
|
||||
}
|
||||
pubs[0].Channels[0] = "changed"
|
||||
if m.Publications()[0].Channels[0] != "x:1" {
|
||||
t.Fatal("Publications returned shared slices")
|
||||
}
|
||||
var nilMem *MemoryDriver
|
||||
if nilMem.Publish(ctx, "a", "b", nil) == nil || nilMem.Publications() != nil {
|
||||
t.Fatal("nil memory driver")
|
||||
}
|
||||
|
||||
for name, fn := range map[string]func(){
|
||||
"empty_name": func() { RegisterDriver("", func(*backpack.App, *Service) (Driver, error) { return nullDriver{}, nil }) },
|
||||
"nil_factory": func() { RegisterDriver("acme-nil", nil) },
|
||||
"duplicate": func() {
|
||||
RegisterDriver("null", func(*backpack.App, *Service) (Driver, error) { return nullDriver{}, nil })
|
||||
},
|
||||
} {
|
||||
func() {
|
||||
defer func() {
|
||||
if recover() == nil {
|
||||
t.Errorf("RegisterDriver %s did not panic", name)
|
||||
}
|
||||
}()
|
||||
fn()
|
||||
}()
|
||||
}
|
||||
if _, err := From(configApp(t, map[string]any{"realtime.driver": "acme-nil-driver"})); err == nil || !strings.Contains(err.Error(), "returned nil") {
|
||||
t.Fatalf("nil driver: %v", err)
|
||||
}
|
||||
if _, err := From(configApp(t, map[string]any{"realtime.driver": "acme-broken"})); err == nil || !strings.Contains(err.Error(), "driver acme-broken") {
|
||||
t.Fatalf("failing factory: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterDriver("acme-nil-driver", func(*backpack.App, *Service) (Driver, error) { return nil, nil })
|
||||
RegisterDriver("acme-broken", func(*backpack.App, *Service) (Driver, error) { return nil, context.Canceled })
|
||||
}
|
||||
|
||||
// TestBroadcastArgsJSON covers the stored job args: the payload travels as
|
||||
// a JSON string so JSONB cannot reorder its keys.
|
||||
func TestBroadcastArgsJSON(t *testing.T) {
|
||||
a := BroadcastArgs{Channels: []string{"room:1"}, Event: "e", Payload: json.RawMessage(`{"z":1,"a":2}`)}
|
||||
raw, err := json.Marshal(a)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(raw) != `{"channels":["room:1"],"event":"e","payload":"{\"z\":1,\"a\":2}"}` {
|
||||
t.Fatalf("stored args = %s", raw)
|
||||
}
|
||||
var back BroadcastArgs
|
||||
if err := json.Unmarshal(raw, &back); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(back.Payload) != `{"z":1,"a":2}` || back.Event != "e" || back.Kind() != "summer.broadcast" {
|
||||
t.Fatalf("round trip = %+v", back)
|
||||
}
|
||||
if err := json.Unmarshal([]byte(`{"channels":[],"event":"e","payload":""}`), &back); err != nil || back.Payload != nil {
|
||||
t.Fatalf("empty payload = %s, %v", back.Payload, err)
|
||||
}
|
||||
if err := back.UnmarshalJSON([]byte(`not json`)); err == nil {
|
||||
t.Fatal("malformed args accepted")
|
||||
}
|
||||
}
|
||||
|
||||
// TestBind covers the Binding rules.
|
||||
func TestBind(t *testing.T) {
|
||||
svc, err := From(backpack.New(nil))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
channels := func(context.Context, *gorm.DB, *Gadget) ([]string, error) { return nil, nil }
|
||||
if err := Bind[Gadget](nil, Binding[Gadget]{Channels: channels}); err == nil {
|
||||
t.Fatal("nil service accepted")
|
||||
}
|
||||
if err := Bind[string](svc, Binding[string]{Channels: func(context.Context, *gorm.DB, *string) ([]string, error) { return nil, nil }}); err == nil {
|
||||
t.Fatal("non-struct type accepted")
|
||||
}
|
||||
if err := Bind[Gadget](svc, Binding[Gadget]{}); err == nil {
|
||||
t.Fatal("nil Channels accepted")
|
||||
}
|
||||
if err := Bind[Gadget](svc, Binding[Gadget]{Channels: channels}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := Bind[Gadget](svc, Binding[Gadget]{Channels: channels}); err == nil {
|
||||
t.Fatal("second binding accepted")
|
||||
}
|
||||
h := svc.handlerFor(reflect.TypeFor[Gadget]())
|
||||
if h == nil || h.alias != "lighthouse.gadget" || h.ttl != DefaultTTL || h.eventName(ActionDeleted) != "deleted.lighthouse.gadget" || !h.allows(nil, ActionUpdated) {
|
||||
t.Fatalf("binding defaults = %+v", h)
|
||||
}
|
||||
if svc.handlerFor(reflect.TypeFor[User]()) != nil {
|
||||
t.Fatal("a type without a binding or Broadcastable got a handler")
|
||||
}
|
||||
if got := defaultAlias(reflect.TypeFor[Widget]()); got != "lighthouse.widget" {
|
||||
t.Fatalf("defaultAlias = %s", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user