fix: bind queue idempotency to compatibility

This commit is contained in:
Josh Creek
2026-08-31 22:51:00 +01:00
parent 585056ccc3
commit 99680dbf6f
4 changed files with 62 additions and 2 deletions
+45
View File
@@ -221,6 +221,51 @@ func TestQueueCreateRejectsCandidateMetadataMismatch(t *testing.T) {
}
}
func TestQueueCreateAPIRetriesIdenticallyAndRejectsKeyReuseWithChangedPayload(t *testing.T) {
now := time.Unix(1000, 0).UTC()
sessions := domain.NewSessionStore()
session, token, err := sessions.Issue("player-1", time.Hour, now)
if err != nil {
t.Fatal(err)
}
service := &Service{Sessions: sessions, Queue: domain.NewQueue(), Now: func() time.Time { return now }, Candidate: func(playerID, ticketID string) (domain.Candidate, error) {
return domain.Candidate{PlayerID: playerID, TicketID: ticketID, EnqueuedAt: now}, nil
}}
server := httptest.NewServer(service.Handler())
defer server.Close()
request := func(body, key string) (int, queueResponse) {
req, _ := http.NewRequest(http.MethodPost, server.URL+"/v1/queue", strings.NewReader(body))
req.Header.Set("Authorization", "Bearer "+session.SessionID+":"+token)
req.Header.Set("Idempotency-Key", key)
response, requestErr := http.DefaultClient.Do(req)
if requestErr != nil {
t.Fatal(requestErr)
}
defer response.Body.Close()
var decoded queueResponse
if response.StatusCode == http.StatusCreated {
if err := json.NewDecoder(response.Body).Decode(&decoded); err != nil {
t.Fatal(err)
}
}
return response.StatusCode, decoded
}
body := `{"ticket_id":"ticket-idempotent","playlist":"casual","client_build":"build-1","protocol_version":1}`
status, first := request(body, "idempotency-key-123456")
if status != http.StatusCreated {
t.Fatalf("first create status=%d", status)
}
status, replay := request(body, "idempotency-key-123456")
if status != http.StatusCreated || replay != first {
t.Fatalf("identical replay status=%d first=%+v replay=%+v", status, first, replay)
}
changed := `{"ticket_id":"ticket-idempotent","playlist":"casual","client_build":"build-2","protocol_version":1}`
status, _ = request(changed, "idempotency-key-123456")
if status != http.StatusConflict {
t.Fatalf("changed-payload replay status=%d, want conflict", status)
}
}
func TestQueueAPIUsesInjectedPersistentBackendWithoutCandidateProvider(t *testing.T) {
now := time.Unix(1000, 0).UTC()
sessions := domain.NewSessionStore()
+1 -1
View File
@@ -218,5 +218,5 @@ func createPayload(playerID, ticketID string, candidate Candidate) string {
for _, region := range regions {
rtts = append(rtts, fmt.Sprintf("%s=%.9f", region, candidate.PredictedRTT[region]))
}
return strings.Join([]string{playerID, ticketID, candidate.PlayerID, candidate.TicketID, fmt.Sprintf("%.9f", candidate.Rating), candidate.EnqueuedAt.UTC().Format(time.RFC3339Nano), strings.Join(rtts, ",")}, "\x00")
return strings.Join([]string{playerID, ticketID, candidate.PlayerID, candidate.TicketID, string(candidate.Playlist), candidate.ClientBuild, fmt.Sprintf("%d", candidate.ProtocolVersion), fmt.Sprintf("%.9f", candidate.Rating), candidate.EnqueuedAt.UTC().Format(time.RFC3339Nano), strings.Join(rtts, ",")}, "\x00")
}
+15
View File
@@ -77,6 +77,21 @@ func TestQueueCreateIdempotencyIncludesCandidatePayload(t *testing.T) {
if _, err := q.Create("player-a", "ticket-a", "create-key-123456", changed, now); !errors.Is(err, ErrConflict) {
t.Fatalf("changed create payload error = %v", err)
}
changed = base
changed.Playlist = Ranked
if _, err := q.Create("player-a", "ticket-a", "create-key-123456", changed, now); !errors.Is(err, ErrConflict) {
t.Fatalf("changed playlist payload error = %v", err)
}
changed = base
changed.ClientBuild = "build-2"
if _, err := q.Create("player-a", "ticket-a", "create-key-123456", changed, now); !errors.Is(err, ErrConflict) {
t.Fatalf("changed build payload error = %v", err)
}
changed = base
changed.ProtocolVersion = 2
if _, err := q.Create("player-a", "ticket-a", "create-key-123456", changed, now); !errors.Is(err, ErrConflict) {
t.Fatalf("changed protocol payload error = %v", err)
}
}
func TestQueueCreateRejectsCandidateOwnedByAnotherPlayer(t *testing.T) {