MWSE/internal/services/notify_test.go

111 lines
3.4 KiB
Go

package services
import (
"sync"
"testing"
"git.saqut.com/saqut/mwse/internal/notify"
"git.saqut.com/saqut/mwse/internal/ws"
)
// recTrigger records suit replies pushed outward (the #44 3rd-party trigger).
type recTrigger struct {
mu sync.Mutex
got []notify.Notification
}
func (r *recTrigger) NotifyReplied(n notify.Notification) {
r.mu.Lock()
r.got = append(r.got, n)
r.mu.Unlock()
}
func (r *recTrigger) count() int {
r.mu.Lock()
defer r.mu.Unlock()
return len(r.got)
}
// TestNotifyOfflineThenDeliverOnConnect is the #43 store-and-forward core: a
// message left for an offline client is delivered when that client connects.
func TestNotifyOfflineThenDeliverOnConnect(t *testing.T) {
hub := newHub()
a, _ := connect(hub, "alice")
res := asMap(t, hub.Handle(a, msg("notify/send", "to", "bob", "pack", map[string]any{"text": "hi"})))
if res["status"] != "success" {
t.Fatalf("notify/send = %v", res)
}
trace, _ := res["trace"].(string)
if trace == "" {
t.Fatal("notify/send should return a trace id")
}
// bob was offline; now connects and must receive the queued notification.
_, fb := connect(hub, "bob")
sig := waitSignal(t, fb, "notify")
if sig["trace"] != trace {
t.Fatalf("delivered trace = %v, want %s", sig["trace"], trace)
}
if p, _ := sig["pack"].(map[string]any); p["text"] != "hi" {
t.Fatalf("delivered pack = %v", sig["pack"])
}
// Status reports delivered.
st := asMap(t, hub.Handle(a, msg("notify/status", "trace", trace)))
if st["delivered"] != true {
t.Fatalf("status = %v, want delivered", st)
}
}
// TestNotifyImmediateWhenOnline delivers without waiting when the target is up.
func TestNotifyImmediateWhenOnline(t *testing.T) {
hub := newHub()
a, _ := connect(hub, "alice")
_, fb := connect(hub, "bob")
hub.Handle(a, msg("notify/send", "to", "bob", "pack", map[string]any{"text": "now"}))
sig := waitSignal(t, fb, "notify")
if p, _ := sig["pack"].(map[string]any); p["text"] != "now" {
t.Fatalf("immediate delivery pack = %v", sig["pack"])
}
}
// TestNotifySuitReply is the #44 reply path: a suit notification's reply reaches
// the 3rd-party trigger and is signalled back to the origin client.
func TestNotifySuitReply(t *testing.T) {
trig := &recTrigger{}
hub := ws.NewHub()
Register(hub, WithNotifyTrigger(trig))
a, fa := connect(hub, "alice")
b, fb := connect(hub, "bob")
res := asMap(t, hub.Handle(a, msg("notify/send", "to", "bob", "suit", true, "pack", map[string]any{"q": "ok?"})))
trace, _ := res["trace"].(string)
sig := waitSignal(t, fb, "notify")
if sig["suit"] != true {
t.Fatalf("notify suit flag = %v, want true", sig["suit"])
}
// bob replies to the suit.
rep := asMap(t, hub.Handle(b, msg("notify/reply", "trace", trace, "pack", map[string]any{"a": "yes"})))
if rep["status"] != "success" {
t.Fatalf("notify/reply = %v", rep)
}
// The 3rd-party trigger fired, and the origin (alice) got the reply signal.
waitFor(t, func() bool { return trig.count() == 1 })
got := waitSignal(t, fa, "notify/reply")
if p, _ := got["pack"].(map[string]any); p["a"] != "yes" {
t.Fatalf("origin reply signal pack = %v", got["pack"])
}
// A non-suit reply must be rejected.
plain := asMap(t, hub.Handle(a, msg("notify/send", "to", "bob", "pack", map[string]any{})))
if r := asMap(t, hub.Handle(b, msg("notify/reply", "trace", plain["trace"], "pack", map[string]any{}))); r["status"] != "fail" {
t.Fatalf("reply to non-suit = %v, want fail", r)
}
}