package notify_test import ( "testing" "time" "github.com/google/uuid" "scrabble/backend/internal/notify" fb "scrabble/pkg/fbs/scrabblefb" ) func TestHubDeliversToSubscriber(t *testing.T) { h := notify.NewHub(4) ch, cancel := h.Subscribe() defer cancel() want := notify.Intent{UserID: uuid.New(), Kind: notify.KindYourTurn, Payload: []byte{1, 2, 3}} h.Publish(want) select { case got := <-ch: if got.Kind != want.Kind || got.UserID != want.UserID { t.Fatalf("delivered %+v, want %+v", got, want) } case <-time.After(time.Second): t.Fatal("no delivery within timeout") } } func TestHubDropsWhenSubscriberBufferFull(t *testing.T) { h := notify.NewHub(1) ch, cancel := h.Subscribe() defer cancel() in := notify.Intent{UserID: uuid.New(), Kind: notify.KindNudge} // Buffer holds one; the second and third are dropped, and Publish must not block. h.Publish(in, in, in) if got := len(ch); got != 1 { t.Fatalf("buffered %d intents, want 1 (rest dropped)", got) } } func TestHubUnsubscribeClosesChannel(t *testing.T) { h := notify.NewHub(2) ch, cancel := h.Subscribe() cancel() if _, ok := <-ch; ok { t.Fatal("channel should be closed after unsubscribe") } // Publishing after unsubscribe must be safe (no panic, no delivery). h.Publish(notify.Intent{Kind: notify.KindMatchFound}) } func TestNopPublisherDiscards(t *testing.T) { var p notify.Publisher = notify.Nop{} p.Publish(notify.Intent{Kind: notify.KindYourTurn}) // must not panic } func TestYourTurnPayloadRoundTrips(t *testing.T) { uid, gid := uuid.New(), uuid.New() in := notify.YourTurn(uid, gid, time.Unix(1717000000, 0)) if in.UserID != uid || in.Kind != notify.KindYourTurn || in.EventID == "" { t.Fatalf("intent metadata wrong: %+v", in) } ev := fb.GetRootAsYourTurnEvent(in.Payload, 0) if got := string(ev.GameId()); got != gid.String() { t.Fatalf("game id = %q, want %q", got, gid) } if got := ev.DeadlineUnix(); got != 1717000000 { t.Fatalf("deadline = %d, want 1717000000", got) } } func TestOpponentMovedPayloadRoundTrips(t *testing.T) { uid, gid := uuid.New(), uuid.New() in := notify.OpponentMoved(uid, gid, 1, "play", 24, 130) if in.Kind != notify.KindOpponentMoved { t.Fatalf("kind = %q", in.Kind) } ev := fb.GetRootAsOpponentMovedEvent(in.Payload, 0) if string(ev.GameId()) != gid.String() || ev.Seat() != 1 || string(ev.Action()) != "play" || ev.Score() != 24 || ev.Total() != 130 { t.Fatalf("decoded wrong: game=%q seat=%d action=%q score=%d total=%d", ev.GameId(), ev.Seat(), ev.Action(), ev.Score(), ev.Total()) } } func TestChatMessagePayloadRoundTrips(t *testing.T) { uid, gid, sid := uuid.New(), uuid.New(), uuid.New() in := notify.ChatMessage(uid, gid, sid, "msg-1", "message", "hi", time.Unix(1717000001, 0)) if in.Kind != notify.KindChatMessage { t.Fatalf("kind = %q", in.Kind) } ev := fb.GetRootAsChatMessage(in.Payload, 0) if string(ev.Id()) != "msg-1" || string(ev.SenderId()) != sid.String() || string(ev.Body()) != "hi" || ev.CreatedAtUnix() != 1717000001 { t.Fatalf("decoded wrong chat message: %+v", ev) } } func TestNotificationPayloadRoundTrips(t *testing.T) { uid := uuid.New() in := notify.Notification(uid, notify.NotifyFriendRequest) if in.UserID != uid || in.Kind != notify.KindNotification || in.EventID == "" { t.Fatalf("intent metadata wrong: %+v", in) } ev := fb.GetRootAsNotificationEvent(in.Payload, 0) if got := string(ev.Kind()); got != notify.NotifyFriendRequest { t.Fatalf("notification sub-kind = %q, want %q", got, notify.NotifyFriendRequest) } }