feat: backend service
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user