package http import ( "bytes" "context" "encoding/json" "errors" "io" "net" "net/http" "net/http/httptest" "testing" "time" "github.com/redis/go-redis/v9" roomv1 "hyapp.local/api/proto/room/v1" "hyapp/services/gateway-service/internal/auth" "hyapp/services/gateway-service/internal/transport/http/giftlimit" ) type fakeGiftCapacityLimiter struct { decision giftlimit.Decision err error inputs []giftlimit.Input releases int } func (f *fakeGiftCapacityLimiter) Acquire(_ context.Context, input giftlimit.Input) (giftlimit.Decision, error) { f.inputs = append(f.inputs, input) return f.decision, f.err } func (f *fakeGiftCapacityLimiter) Release(context.Context, giftlimit.Lease) error { f.releases++ return nil } func TestGiftCapacityLimitWeightsUniqueTargetsAndReleasesInFlightSlot(t *testing.T) { roomClient := &fakeRoomClient{} limiter := &fakeGiftCapacityLimiter{decision: giftlimit.Decision{Allowed: true}} handler := NewHandlerWithClients(roomClient, nil, nil, &fakeUserProfileClient{regionID: 1001}) handler.SetGiftCapacityProtection(true, 5*time.Second, 30, limiter) router := handler.Routes(auth.NewVerifier("secret")) request := httptest.NewRequest(http.MethodPost, "/api/v1/rooms/gift/batch-send", bytes.NewReader([]byte(`{"room_id":"room-1","command_id":"cmd-weighted","target_user_ids":[43,44,43,0],"gift_id":"rose","gift_count":1}`))) request.Header.Set("Authorization", "Bearer "+signGatewayTokenWithAppCode(t, "secret", 42, "huwaa")) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, request) if recorder.Code != http.StatusOK { t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String()) } if len(limiter.inputs) != 1 { t.Fatalf("limiter acquire count mismatch: %d", len(limiter.inputs)) } input := limiter.inputs[0] if input.AppCode != "huwaa" || input.UserID != 42 || input.RoomID != "room-1" || input.Weight != 2 || input.RequestID == "" { t.Fatalf("weighted limiter input mismatch: %+v", input) } if limiter.releases != 1 { t.Fatalf("successful acquire must release in-flight slot exactly once: %d", limiter.releases) } if roomClient.lastGift == nil || len(roomClient.lastGift.GetTargetUserIds()) != 2 { t.Fatalf("normal batch-send behavior changed: %+v", roomClient.lastGift) } } func TestGiftCapacityLimitPreservesSingleTargetWeight(t *testing.T) { limiter := &fakeGiftCapacityLimiter{decision: giftlimit.Decision{Allowed: true}} handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{regionID: 1001}) handler.SetGiftCapacityProtection(true, 5*time.Second, 30, limiter) router := handler.Routes(auth.NewVerifier("secret")) request := httptest.NewRequest(http.MethodPost, "/api/v1/rooms/gift/send", bytes.NewReader([]byte(`{"room_id":"room-1","command_id":"cmd-single","target_user_id":43,"gift_id":"rose","gift_count":1}`))) request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42)) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, request) if recorder.Code != http.StatusOK { t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String()) } if len(limiter.inputs) != 1 || limiter.inputs[0].Weight != 1 { t.Fatalf("legacy single-target request must consume one token: %+v", limiter.inputs) } } func TestGiftCapacityLimitWeightsGiftCountTimesUniqueTargets(t *testing.T) { limiter := &fakeGiftCapacityLimiter{decision: giftlimit.Decision{Allowed: true}} handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{regionID: 1001}) handler.SetGiftCapacityProtection(true, 5*time.Second, 30, limiter) router := handler.Routes(auth.NewVerifier("secret")) request := httptest.NewRequest(http.MethodPost, "/api/v1/rooms/gift/batch-send", bytes.NewReader([]byte(`{"room_id":"room-1","command_id":"cmd-units","target_user_ids":[43,44,43],"gift_id":"rose","gift_count":99}`))) request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42)) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, request) if recorder.Code != http.StatusOK || len(limiter.inputs) != 1 || limiter.inputs[0].Weight != 198 { t.Fatalf("gift work units must be count*deduplicated targets: status=%d inputs=%+v body=%s", recorder.Code, limiter.inputs, recorder.Body.String()) } } func TestGiftCapacityLimitRejectsExplosiveGiftUnitsBeforeRedis(t *testing.T) { targets := make([]int64, 0, 6) for userID := int64(43); userID < 49; userID++ { targets = append(targets, userID) } body, err := json.Marshal(map[string]any{ "room_id": "room-1", "command_id": "cmd-too-many-units", "target_user_ids": targets, "gift_id": "rose", "gift_count": 999, }) if err != nil { t.Fatalf("marshal body: %v", err) } roomClient := &fakeRoomClient{} limiter := &fakeGiftCapacityLimiter{decision: giftlimit.Decision{Allowed: true}} handler := NewHandlerWithClients(roomClient, nil, nil, &fakeUserProfileClient{regionID: 1001}) handler.SetGiftCapacityProtection(true, 5*time.Second, 30, limiter) router := handler.Routes(auth.NewVerifier("secret")) request := httptest.NewRequest(http.MethodPost, "/api/v1/rooms/gift/batch-send", bytes.NewReader(body)) request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42)) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, request) if recorder.Code != http.StatusBadRequest || len(limiter.inputs) != 0 || roomClient.lastGift != nil { t.Fatalf("gift_count*targets above hard cap reached Redis/downstream: status=%d inputs=%+v gift=%+v", recorder.Code, limiter.inputs, roomClient.lastGift) } } func TestGiftCapacityLimitRejectsGiftCountAboveV2Contract(t *testing.T) { roomClient := &fakeRoomClient{} limiter := &fakeGiftCapacityLimiter{decision: giftlimit.Decision{Allowed: true}} handler := NewHandlerWithClients(roomClient, nil, nil, &fakeUserProfileClient{regionID: 1001}) handler.SetGiftCapacityProtection(true, 5*time.Second, 30, limiter) router := handler.Routes(auth.NewVerifier("secret")) request := httptest.NewRequest(http.MethodPost, "/api/v1/rooms/gift/send", bytes.NewReader([]byte(`{"room_id":"room-1","command_id":"cmd-count-overflow","target_user_id":43,"gift_id":"rose","gift_count":1000}`))) request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42)) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, request) if recorder.Code != http.StatusBadRequest || len(limiter.inputs) != 0 || roomClient.lastGift != nil { t.Fatalf("gift_count above 999 reached Redis/downstream: status=%d inputs=%+v gift=%+v", recorder.Code, limiter.inputs, roomClient.lastGift) } } func TestSendGiftV2PreservesV1FieldsAndForwardsStableBatchIdentity(t *testing.T) { roomClient := &fakeRoomClient{sendGiftResp: &roomv1.SendGiftResponse{ Result: &roomv1.CommandResult{Applied: true, RoomVersion: 14}, BillingReceiptId: "receipt-v2", CoinBalanceAfter: 900, V2: &roomv1.SendGiftResultV2{ ApiVersion: 2, CommandId: "combo-1_b1", ComboSessionId: "combo-1", BatchSeq: 1, CommittedRoomVersion: 14, BillingReceiptId: "receipt-v2", CoinBalanceAfter: 900, SettledGiftCount: 9, SettledTargetUserIds: []int64{43}, OperationStatus: "committed", }, }} handler := NewHandlerWithClients(roomClient, nil, nil, &fakeUserProfileClient{regionID: 1001}) router := handler.Routes(auth.NewVerifier("secret")) request := httptest.NewRequest(http.MethodPost, "/api/v2/rooms/gift/send", bytes.NewReader([]byte(`{"room_id":"room-1","command_id":"combo-1_b1","combo_session_id":"combo-1","batch_seq":1,"target_user_id":43,"gift_id":"rose","gift_count":9}`))) request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42)) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, request) if recorder.Code != http.StatusOK { t.Fatalf("V2 status mismatch: got=%d body=%s", recorder.Code, recorder.Body.String()) } if roomClient.lastGift == nil || roomClient.lastGift.GetComboSessionId() != "combo-1" || roomClient.lastGift.GetBatchSeq() != 1 || roomClient.lastGift.GetMeta().GetCommandId() != "combo-1_b1" || roomClient.lastGift.GetDisplayMode() != "" { t.Fatalf("V2 stable batch identity was not forwarded: %+v", roomClient.lastGift) } var envelope struct { Data struct { BillingReceiptID string `json:"billing_receipt_id"` CoinBalanceAfter int64 `json:"coin_balance_after"` V2 struct { CommandID string `json:"command_id"` CommittedRoomVersion int64 `json:"committed_room_version"` } `json:"v2"` } `json:"data"` } if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil { t.Fatalf("decode V2 response: %v", err) } if envelope.Data.BillingReceiptID != "receipt-v2" || envelope.Data.CoinBalanceAfter != 900 || envelope.Data.V2.CommandID != "combo-1_b1" || envelope.Data.V2.CommittedRoomVersion != 14 { t.Fatalf("V1 compatibility or nested V2 result missing: %+v body=%s", envelope, recorder.Body.String()) } } func TestSendGiftV1RoutesPreserveTopLevelFieldsAndStripNestedV2(t *testing.T) { tests := []struct { name string path string body string displayMode string }{ { name: "single", path: "/api/v1/rooms/gift/send", body: `{"room_id":"room-1","command_id":"legacy-single","target_user_id":43,"gift_id":"rose","gift_count":1}`, }, { name: "batch", path: "/api/v1/rooms/gift/batch-send", body: `{"room_id":"room-1","command_id":"legacy-batch","target_user_ids":[43,44],"gift_id":"rose","gift_count":2}`, displayMode: "batch", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { roomClient := &fakeRoomClient{sendGiftResp: &roomv1.SendGiftResponse{ Result: &roomv1.CommandResult{Applied: true, RoomVersion: 14}, BillingReceiptId: "legacy-receipt", RoomHeat: 81, CoinBalanceAfter: 900, GiftIncomeBalanceAfter: 700, V2: &roomv1.SendGiftResultV2{ ApiVersion: 2, CommandId: "internal-result", OperationStatus: "committed", }, }} router := NewHandlerWithClients(roomClient, nil, nil, &fakeUserProfileClient{regionID: 1001}).Routes(auth.NewVerifier("secret")) request := httptest.NewRequest(http.MethodPost, test.path, bytes.NewReader([]byte(test.body))) request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42)) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, request) if recorder.Code != http.StatusOK { t.Fatalf("V1 status mismatch: got=%d body=%s", recorder.Code, recorder.Body.String()) } if roomClient.lastGift == nil || roomClient.lastGift.GetDisplayMode() != test.displayMode { t.Fatalf("V1 request behavior changed: %+v", roomClient.lastGift) } var envelope struct { Data map[string]json.RawMessage `json:"data"` } if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil { t.Fatalf("decode V1 response: %v", err) } if _, exists := envelope.Data["v2"]; exists { t.Fatalf("legacy V1 response exposed nested v2: %s", recorder.Body.String()) } var receipt string var balance int64 var roomHeat int64 if err := json.Unmarshal(envelope.Data["billing_receipt_id"], &receipt); err != nil { t.Fatalf("decode billing_receipt_id: %v", err) } if err := json.Unmarshal(envelope.Data["coin_balance_after"], &balance); err != nil { t.Fatalf("decode coin_balance_after: %v", err) } if err := json.Unmarshal(envelope.Data["room_heat"], &roomHeat); err != nil { t.Fatalf("decode room_heat: %v", err) } if receipt != "legacy-receipt" || balance != 900 || roomHeat != 81 { t.Fatalf("V1 top-level fields changed: receipt=%q balance=%d heat=%d body=%s", receipt, balance, roomHeat, recorder.Body.String()) } }) } } func TestSendGiftV2RequiresComboIdentityBeforeDownstream(t *testing.T) { roomClient := &fakeRoomClient{} handler := NewHandlerWithClients(roomClient, nil, nil, &fakeUserProfileClient{regionID: 1001}) router := handler.Routes(auth.NewVerifier("secret")) request := httptest.NewRequest(http.MethodPost, "/api/v2/rooms/gift/send", bytes.NewReader([]byte(`{"room_id":"room-1","command_id":"cmd-no-combo","target_user_id":43,"gift_id":"rose","gift_count":1}`))) request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42)) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, request) if recorder.Code != http.StatusBadRequest || roomClient.lastGift != nil { t.Fatalf("V2 without combo identity reached downstream: status=%d gift=%+v body=%s", recorder.Code, roomClient.lastGift, recorder.Body.String()) } } func TestGiftCapacityLimitReturns429EnvelopeBeforeDownstreamCalls(t *testing.T) { for _, endpoint := range []struct { name string path string body string }{ {name: "single", path: "/api/v1/rooms/gift/send", body: `{"room_id":"room-1","command_id":"cmd-denied-single","target_user_id":43,"gift_id":"rose","gift_count":1}`}, {name: "batch", path: "/api/v1/rooms/gift/batch-send", body: `{"room_id":"room-1","command_id":"cmd-denied-batch","target_user_ids":[43,44],"gift_id":"rose","gift_count":1}`}, } { t.Run(endpoint.name, func(t *testing.T) { roomClient := &fakeRoomClient{} profileClient := &fakeUserProfileClient{regionID: 1001} limiter := &fakeGiftCapacityLimiter{decision: giftlimit.Decision{RetryAfter: 275 * time.Millisecond}} handler := NewHandlerWithClients(roomClient, nil, nil, profileClient) handler.SetGiftCapacityProtection(true, 5*time.Second, 30, limiter) router := handler.Routes(auth.NewVerifier("secret")) request := httptest.NewRequest(http.MethodPost, endpoint.path, bytes.NewReader([]byte(endpoint.body))) request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42)) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, request) if recorder.Code != http.StatusTooManyRequests { t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String()) } if recorder.Header().Get("Retry-After") != "1" { t.Fatalf("Retry-After mismatch: %q", recorder.Header().Get("Retry-After")) } var envelope struct { Code string `json:"code"` Message string `json:"message"` RequestID string `json:"request_id"` Data struct { RetryAfterMS int64 `json:"retry_after_ms"` } `json:"data"` } if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil { t.Fatalf("decode response failed: %v body=%s", err, recorder.Body.String()) } if envelope.Code != "RATE_LIMITED" || envelope.Message != "rate limited" || envelope.RequestID == "" || envelope.Data.RetryAfterMS != 275 { t.Fatalf("rate-limit envelope mismatch: %+v", envelope) } if roomClient.lastGift != nil || profileClient.lastGet != nil || len(profileClient.batchRequests) != 0 { t.Fatalf("denied request reached downstream: gift=%+v profile_get=%+v profile_batches=%d", roomClient.lastGift, profileClient.lastGet, len(profileClient.batchRequests)) } if limiter.releases != 0 { t.Fatalf("denied request must not release an unowned slot: %d", limiter.releases) } }) } } func TestGiftCapacityLimitFailsClosedWhenRedisUnavailable(t *testing.T) { roomClient := &fakeRoomClient{} limiter := &fakeGiftCapacityLimiter{err: errors.New("redis unavailable")} handler := NewHandlerWithClients(roomClient, nil, nil, &fakeUserProfileClient{regionID: 1001}) handler.SetGiftCapacityProtection(true, 5*time.Second, 30, limiter) router := handler.Routes(auth.NewVerifier("secret")) request := httptest.NewRequest(http.MethodPost, "/api/v1/rooms/gift/send", bytes.NewReader([]byte(`{"room_id":"room-1","command_id":"cmd-error","target_user_id":43,"gift_id":"rose","gift_count":1}`))) request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42)) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, request) if recorder.Code != http.StatusBadGateway || roomClient.lastGift != nil { t.Fatalf("limiter backend failure must fail closed: status=%d body=%s gift=%+v", recorder.Code, recorder.Body.String(), roomClient.lastGift) } } func TestGiftCapacityLimitBlockedRedisFailsClosedQuickly(t *testing.T) { // net.Pipe 对 Redis 写入只读不回,模拟连接存在但服务端永久不返回 Lua 结果;真正的 // RedisLimiter 必须靠自己的 operation deadline 结束,而不是依赖测试 fake 立即报错。 redisClient := redis.NewClient(&redis.Options{ Addr: "blocked-redis", MaxRetries: -1, ReadTimeout: time.Second, WriteTimeout: time.Second, ContextTimeoutEnabled: true, Dialer: func(context.Context, string, string) (net.Conn, error) { clientConn, serverConn := net.Pipe() go func() { defer serverConn.Close() _, _ = io.Copy(io.Discard, serverConn) }() return clientConn, nil }, }) defer redisClient.Close() limiter := giftlimit.NewRedisLimiter(redisClient, giftlimit.Config{RedisOperationTimeout: 50 * time.Millisecond}) roomClient := &fakeRoomClient{} handler := NewHandlerWithClients(roomClient, nil, nil, &fakeUserProfileClient{regionID: 1001}) handler.SetGiftCapacityProtection(true, 5*time.Second, 30, limiter) router := handler.Routes(auth.NewVerifier("secret")) request := httptest.NewRequest(http.MethodPost, "/api/v1/rooms/gift/send", bytes.NewReader([]byte(`{"room_id":"room-1","command_id":"cmd-blocked-redis","target_user_id":43,"gift_id":"rose","gift_count":1}`))) request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42)) recorder := httptest.NewRecorder() startedAt := time.Now() router.ServeHTTP(recorder, request) elapsed := time.Since(startedAt) if recorder.Code != http.StatusBadGateway || roomClient.lastGift != nil || elapsed < 35*time.Millisecond || elapsed > 500*time.Millisecond { t.Fatalf("blocked Redis must fail closed inside short deadline: status=%d elapsed=%s body=%s gift=%+v", recorder.Code, elapsed, recorder.Body.String(), roomClient.lastGift) } } func TestGiftCapacityLimitRejectsTooManyUniqueTargetsBeforeRedis(t *testing.T) { uniqueTargets := make([]int64, 0, 31) for userID := int64(1); userID <= 31; userID++ { uniqueTargets = append(uniqueTargets, userID) } body, err := json.Marshal(map[string]any{ "room_id": "room-1", "command_id": "cmd-too-many", "target_user_ids": uniqueTargets, "gift_id": "rose", "gift_count": 1, }) if err != nil { t.Fatalf("marshal body: %v", err) } roomClient := &fakeRoomClient{} limiter := &fakeGiftCapacityLimiter{decision: giftlimit.Decision{Allowed: true}} handler := NewHandlerWithClients(roomClient, nil, nil, &fakeUserProfileClient{regionID: 1001}) handler.SetGiftCapacityProtection(true, 5*time.Second, 30, limiter) router := handler.Routes(auth.NewVerifier("secret")) request := httptest.NewRequest(http.MethodPost, "/api/v1/rooms/gift/batch-send", bytes.NewReader(body)) request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42)) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, request) if recorder.Code != http.StatusBadRequest || len(limiter.inputs) != 0 || roomClient.lastGift != nil { t.Fatalf("31 unique targets must fail before Redis/downstream: status=%d body=%s inputs=%+v gift=%+v", recorder.Code, recorder.Body.String(), limiter.inputs, roomClient.lastGift) } } func TestGiftCapacityLimitRejectsUnsupportedTargetTypeBeforeRedis(t *testing.T) { roomClient := &fakeRoomClient{} limiter := &fakeGiftCapacityLimiter{decision: giftlimit.Decision{Allowed: true}} handler := NewHandlerWithClients(roomClient, nil, nil, &fakeUserProfileClient{regionID: 1001}) handler.SetGiftCapacityProtection(true, 5*time.Second, 30, limiter) router := handler.Routes(auth.NewVerifier("secret")) request := httptest.NewRequest(http.MethodPost, "/api/v1/rooms/gift/send", bytes.NewReader([]byte(`{"room_id":"room-1","command_id":"cmd-unsupported","target_type":"all_mic","gift_id":"rose","gift_count":1}`))) request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42)) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, request) if recorder.Code != http.StatusBadRequest || len(limiter.inputs) != 0 || roomClient.lastGift != nil { t.Fatalf("unsupported target_type must fail before Redis/downstream: status=%d body=%s inputs=%+v gift=%+v", recorder.Code, recorder.Body.String(), limiter.inputs, roomClient.lastGift) } } func TestGiftCapacityLimitAppliesMaximumAfterTargetDeduplication(t *testing.T) { targets := make([]int64, 0, 31) for userID := int64(1); userID <= 30; userID++ { targets = append(targets, userID) } targets = append(targets, 30) body, err := json.Marshal(map[string]any{ "room_id": "room-1", "command_id": "cmd-dedup-maximum", "target_user_ids": targets, "gift_id": "rose", "gift_count": 1, }) if err != nil { t.Fatalf("marshal body: %v", err) } limiter := &fakeGiftCapacityLimiter{decision: giftlimit.Decision{RetryAfter: 10 * time.Millisecond}} handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{regionID: 1001}) handler.SetGiftCapacityProtection(true, 5*time.Second, 30, limiter) router := handler.Routes(auth.NewVerifier("secret")) request := httptest.NewRequest(http.MethodPost, "/api/v1/rooms/gift/batch-send", bytes.NewReader(body)) request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42)) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, request) if recorder.Code != http.StatusTooManyRequests || len(limiter.inputs) != 1 || limiter.inputs[0].Weight != 30 { t.Fatalf("duplicate target must not trip max_targets_per_request: status=%d body=%s inputs=%+v", recorder.Code, recorder.Body.String(), limiter.inputs) } } func TestGiftRequestTotalDeadlineReleasesInFlightLease(t *testing.T) { roomClient := &fakeRoomClient{sendGiftWaitForContext: true} limiter := &fakeGiftCapacityLimiter{decision: giftlimit.Decision{Allowed: true}} handler := NewHandlerWithClients(roomClient, nil, nil, &fakeUserProfileClient{regionID: 1001}) handler.SetGiftCapacityProtection(true, 40*time.Millisecond, 30, limiter) router := handler.Routes(auth.NewVerifier("secret")) request := httptest.NewRequest(http.MethodPost, "/api/v1/rooms/gift/send", bytes.NewReader([]byte(`{"room_id":"room-1","command_id":"cmd-timeout","target_user_id":43,"gift_id":"rose","gift_count":1}`))) request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42)) recorder := httptest.NewRecorder() startedAt := time.Now() router.ServeHTTP(recorder, request) elapsed := time.Since(startedAt) if recorder.Code == http.StatusOK || elapsed < 30*time.Millisecond || elapsed > 300*time.Millisecond { t.Fatalf("gift orchestration must honor one total deadline: status=%d elapsed=%s body=%s", recorder.Code, elapsed, recorder.Body.String()) } if roomClient.lastGift == nil || limiter.releases != 1 { t.Fatalf("timed-out request must reach room once and release lease: gift=%+v releases=%d", roomClient.lastGift, limiter.releases) } }