c127bc9f0e
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 54s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m25s
Functional: the in-game add-friend handshake aimed at an auto-match opponent who is secretly a pooled robot now records the request per (game, seat) in a new robot_friend_requests table -- never against the shared robot account -- mirroring the robot_blocks pattern. The shared account stays out of friendships, the "requested" state is pinned to the seat (not leaked across the player's other games), the robot ignores it, and a background reaper drops the row 7 days after its game finishes. The outgoing-requests list carries these per-game rows so the seat control stays disabled across reloads. No withdraw UI, per owner decision. Cosmetics: - Lobby: an in-progress game tints the viewer's own score number green when leading or tied, red when trailing (reusing --ok/--danger); scores now render in seat-number order, matching the over-the-board scoreboard. - Stats: the per-variant best move is laid out on two lines -- the variant label, then the score (right-aligned in its column) and the word tiles (left-aligned) below it. - Dark theme: the played-tile background is a touch darker so a player-placed tile reads with more contrast (light theme unchanged). Docs (ARCHITECTURE, FUNCTIONAL + _ru mirror, backend README, UI_DESIGN) updated in the same change.
83 lines
3.1 KiB
Go
83 lines
3.1 KiB
Go
//go:build integration
|
|
|
|
package inttest
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"scrabble/backend/internal/account"
|
|
"scrabble/backend/internal/engine"
|
|
"scrabble/backend/internal/game"
|
|
)
|
|
|
|
// TestRobotFriendRequestIsPerGameAndReaped checks that sending a friend request to a
|
|
// disguised-robot opponent is recorded per-game in robot_friend_requests — never in the
|
|
// friendships table or against the shared robot account — that a re-send is idempotent, and
|
|
// that the reaper deletes the row once its game has been finished past the retention window
|
|
// while keeping the row for an active game.
|
|
func TestRobotFriendRequestIsPerGameAndReaped(t *testing.T) {
|
|
ctx := context.Background()
|
|
svc := newSocialService()
|
|
accs := account.NewStore(testDB)
|
|
human := provisionAccount(t)
|
|
robot, err := accs.ProvisionRobot(ctx, "robot-friend-"+uuid.NewString(), "Robbie")
|
|
if err != nil {
|
|
t.Fatalf("provision robot: %v", err)
|
|
}
|
|
g, err := newGameService().Create(ctx, game.CreateParams{
|
|
Variant: engine.VariantEnglish, Seats: []uuid.UUID{human, robot.ID},
|
|
TurnTimeout: 24 * time.Hour, Seed: openingSeed(t),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create game: %v", err)
|
|
}
|
|
|
|
if err := svc.RequestInGame(ctx, human, robot.ID, g.ID); err != nil {
|
|
t.Fatalf("request robot: %v", err)
|
|
}
|
|
rfr, err := svc.ListRobotFriendRequests(ctx, human)
|
|
if err != nil {
|
|
t.Fatalf("list robot friend requests: %v", err)
|
|
}
|
|
if len(rfr) != 1 || rfr[0].GameID != g.ID || rfr[0].Seat != 1 {
|
|
t.Fatalf("robot friend requests = %v, want one for game %s seat 1", rfr, g.ID)
|
|
}
|
|
// The friendships table must stay empty: no outgoing request against the shared robot account.
|
|
if out, _ := svc.ListOutgoingRequests(ctx, human); len(out) != 0 {
|
|
t.Errorf("friendships outgoing must be empty for a robot request, got %v", out)
|
|
}
|
|
|
|
// A re-send to the same game+seat is idempotent (the UNIQUE constraint collapses it).
|
|
if err := svc.RequestInGame(ctx, human, robot.ID, g.ID); err != nil {
|
|
t.Fatalf("re-request robot: %v", err)
|
|
}
|
|
if rfr, _ := svc.ListRobotFriendRequests(ctx, human); len(rfr) != 1 {
|
|
t.Fatalf("re-request must not duplicate, got %v", rfr)
|
|
}
|
|
|
|
// While the game is active the reaper keeps the row.
|
|
if n, err := svc.ReapExpiredRobotFriendRequests(ctx); err != nil || n != 0 {
|
|
t.Fatalf("reap on active game = (%d, %v), want (0, nil)", n, err)
|
|
}
|
|
if rfr, _ := svc.ListRobotFriendRequests(ctx, human); len(rfr) != 1 {
|
|
t.Fatalf("active-game request must survive the reaper, got %v", rfr)
|
|
}
|
|
|
|
// Finish the game well past the retention window: the reaper now deletes the row.
|
|
finished := time.Now().UTC().Add(-8 * 24 * time.Hour)
|
|
if _, err := testDB.ExecContext(ctx,
|
|
`UPDATE backend.games SET status='finished', finished_at=$1 WHERE game_id=$2`, finished, g.ID); err != nil {
|
|
t.Fatalf("finish game: %v", err)
|
|
}
|
|
if n, err := svc.ReapExpiredRobotFriendRequests(ctx); err != nil || n != 1 {
|
|
t.Fatalf("reap on long-finished game = (%d, %v), want (1, nil)", n, err)
|
|
}
|
|
if rfr, _ := svc.ListRobotFriendRequests(ctx, human); len(rfr) != 0 {
|
|
t.Errorf("long-finished request not reaped: %v", rfr)
|
|
}
|
|
}
|