Files
galaxy-game/integration/notification_flow_test.go
2026-05-06 10:14:55 +03:00

139 lines
4.4 KiB
Go

package integration_test
import (
"context"
"net/http"
"strings"
"testing"
"time"
"galaxy/integration/testenv"
)
// TestNotificationFlow_LobbyInvite asserts that a `lobby.invite.received`
// intent triggers a push frame on the gateway SubscribeEvents stream
// for the invitee AND a captured email at mailpit.
func TestNotificationFlow_LobbyInvite(t *testing.T) {
plat := testenv.Bootstrap(t, testenv.BootstrapOptions{})
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
// Register an engine version so private-game creation can pass
// validation.
admin := testenv.NewBackendAdminClient(plat.Backend.HTTPURL, plat.Backend.AdminUser, plat.Backend.AdminPassword)
if _, resp, err := admin.Do(ctx, http.MethodPost, "/api/v1/admin/engine-versions", map[string]any{
"version": "v1.0.0", "image_ref": "galaxy/game:integration", "enabled": true,
}); err != nil || resp.StatusCode/100 != 2 {
t.Fatalf("seed engine_version: err=%v resp=%v", err, resp)
}
inviter := testenv.RegisterSession(t, plat, "inviter@example.com")
invitee := testenv.RegisterSession(t, plat, "invitee@example.com")
inviterUser, err := inviter.LookupUserID(ctx, plat)
if err != nil {
t.Fatalf("resolve inviter user_id: %v", err)
}
inviteeUser, err := invitee.LookupUserID(ctx, plat)
if err != nil {
t.Fatalf("resolve invitee user_id: %v", err)
}
// Inviter creates a private game.
inviterClient := testenv.NewBackendUserClient(plat.Backend.HTTPURL, inviterUser)
gameBody := map[string]any{
"game_name": "Private Sortie",
"visibility": "private",
"min_players": 2,
"max_players": 4,
"start_gap_hours": 1,
"start_gap_players": 2,
"enrollment_ends_at": time.Now().Add(24 * time.Hour).UTC().Format(time.RFC3339),
"turn_schedule": "0 * * * *",
"target_engine_version": "v1.0.0",
}
raw, resp, err := inviterClient.Do(ctx, http.MethodPost, "/api/v1/user/lobby/games", gameBody)
if err != nil || resp.StatusCode != http.StatusCreated {
t.Fatalf("create private game: err=%v status=%d body=%s", err, resp.StatusCode, string(raw))
}
var game struct {
GameID string `json:"game_id"`
}
if err := decodeJSON(raw, &game); err != nil {
t.Fatalf("decode game: %v", err)
}
// Invitee opens SubscribeEvents stream BEFORE the invite is
// issued so we cannot miss the push frame.
gw, err := invitee.DialAuthenticated(ctx, plat)
if err != nil {
t.Fatalf("invitee dial: %v", err)
}
defer gw.Close()
streamCtx, streamCancel := context.WithCancel(ctx)
defer streamCancel()
events, errCh, err := gw.SubscribeEvents(streamCtx, "gateway.subscribe")
if err != nil {
t.Fatalf("subscribe events: %v", err)
}
// Drain the bootstrap server-time event before the test gets
// going so the invite event is the next thing observed.
select {
case <-events:
case err := <-errCh:
t.Fatalf("subscribe stream error before invite: %v", err)
case <-time.After(5 * time.Second):
t.Fatalf("bootstrap event not received within 5s")
}
// Now clear mailpit so we can detect the new invite email.
if err := plat.Mailpit.DeleteAll(ctx); err != nil {
t.Fatalf("clear mailpit: %v", err)
}
// Inviter issues an invite for invitee.
inviteBody := map[string]any{
"invited_user_id": inviteeUser,
"race_name": "Invitee-Crew",
}
raw, resp, err = inviterClient.Do(ctx, http.MethodPost, "/api/v1/user/lobby/games/"+game.GameID+"/invites", inviteBody)
if err != nil || resp.StatusCode != http.StatusCreated {
t.Fatalf("issue invite: err=%v status=%d body=%s", err, resp.StatusCode, string(raw))
}
// Push: expect a non-bootstrap event.
pushDeadline := time.After(20 * time.Second)
gotPush := false
PUSH:
for {
select {
case ev, ok := <-events:
if !ok {
break PUSH
}
if ev == nil || ev.GetEventType() == "gateway.server_time" {
continue
}
gotPush = true
break PUSH
case err := <-errCh:
t.Fatalf("subscribe stream error during invite: %v", err)
case <-pushDeadline:
break PUSH
}
}
if !gotPush {
t.Fatalf("no push event received for lobby invite within 20s")
}
// Email: expect mailpit to receive a message addressed to invitee.
if _, err := plat.Mailpit.WaitForMessage(ctx, "to:"+invitee.Email, 30*time.Second); err != nil {
t.Fatalf("invite email not captured: %v", err)
}
_ = strings.TrimSpace
}
func decodeJSON(raw []byte, v any) error {
return jsonUnmarshal(raw, v)
}