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) } }