package server import ( "context" "encoding/json" "net/http/httptest" "strings" "testing" "time" "github.com/coder/websocket" "github.com/greyson/super-auto-pets-board-game/internal/game" "github.com/greyson/super-auto-pets-board-game/internal/store" ) func readState(t *testing.T, ctx context.Context, ws *websocket.Conn) *game.View { t.Helper() for { _, data, err := ws.Read(ctx) if err != nil { t.Fatalf("ws read: %v", err) } var m serverMessage if err := json.Unmarshal(data, &m); err != nil { t.Fatalf("unmarshal: %v", err) } if m.Type == "error" { t.Logf("SERVER ERROR: %s", m.Error) continue } if m.Type == "state" && m.State != nil { return m.State } } } func send(t *testing.T, ctx context.Context, ws *websocket.Conn, v any) { t.Helper() data, _ := json.Marshal(v) if err := ws.Write(ctx, websocket.MessageText, data); err != nil { t.Fatalf("ws write: %v", err) } } func TestE2EBattleAckReturnsToShop(t *testing.T) { st, err := store.Open(t.TempDir()) if err != nil { t.Fatal(err) } defer st.Close() srv := New(st, "", false) ts := httptest.NewServer(srv.Handler()) defer ts.Close() base := ts.URL wsBase := "ws" + strings.TrimPrefix(base, "http") ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() // Directly build a game via the room to control decks, then drive acks // over the wire the way the two browsers do. g := game.New() p1, _ := g.AddPlayer("Alice") p2, _ := g.AddPlayer("Bob") r := &room{game: g, conns: map[*client]struct{}{}} srv.rooms[g.ID] = r if err := st.Save(g); err != nil { t.Fatal(err) } dialWS := func(pid, token string) *websocket.Conn { u := wsBase + "/api/ws?game=" + g.ID + "&player=" + pid + "&token=" + token c, _, err := websocket.Dial(ctx, u, nil) if err != nil { t.Fatalf("dial %s: %v", pid, err) } return c } ws1 := dialWS(p1.ID, p1.Token) defer ws1.Close(websocket.StatusNormalClosure, "") ws2 := dialWS(p2.ID, p2.Token) defer ws2.Close(websocket.StatusNormalClosure, "") // Force a battle under the lock. r.mu.Lock() g.Players[0].Deck = []game.Card{{ID: "c1", Kind: game.KindPet, Name: "A", Tier: 1, Power: 3, Suit: game.SuitRed}} g.Players[1].Deck = []game.Card{{ID: "c2", Kind: game.KindPet, Name: "B", Tier: 1, Power: 1, Suit: game.SuitRed}} g.Phase = game.PhaseArrange g.Players[0].Ready = false g.Players[1].Ready = false g.SubmitOrder(p1.ID, []string{g.Players[0].Deck[0].ID}) g.SubmitOrder(p2.ID, []string{g.Players[1].Deck[0].ID}) r.broadcastLocked() r.mu.Unlock() readUntilPhase(t, ctx, ws1, game.PhaseBattle) readUntilPhase(t, ctx, ws2, game.PhaseBattle) // Both acknowledge. send(t, ctx, ws1, map[string]string{"type": "ready"}) send(t, ctx, ws2, map[string]string{"type": "ready"}) // Each client should eventually observe the shop phase. got1 := readUntilPhase(t, ctx, ws1, game.PhaseShop) got2 := readUntilPhase(t, ctx, ws2, game.PhaseShop) t.Logf("ws1 final phase=%s, ws2 final phase=%s", got1, got2) } func readUntilPhase(t *testing.T, ctx context.Context, ws *websocket.Conn, want game.Phase) game.Phase { t.Helper() deadline := time.Now().Add(3 * time.Second) var last game.Phase for time.Now().Before(deadline) { rctx, cancel := context.WithTimeout(ctx, 2*time.Second) v := readState(t, rctx, ws) cancel() last = v.Phase if v.Phase == want { return v.Phase } } t.Fatalf("never reached %s; last=%s", want, last) return last }