139 lines
4.4 KiB
Go
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)
|
|
}
|