mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
POST /admin/accounts/:id/claude/reset-credits/redeem (Idempotency-Key required, same middleware chain as the Codex reset-quota route). The server re-queries eligibility and claims only the upstream next_grant_id when it is redeemable; grant/org IDs never reach the client. Claims are serialized by account and organization Redis leases, and a durable per-organization fence in the idempotency table blocks further redemption after an unconfirmed outcome (settles after 24h). Ported from upstream PR #7591. Co-authored-by: korkin25 <eugeny.kuzakov@gmail.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
57 lines
1.8 KiB
Go
57 lines
1.8 KiB
Go
package admin
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type claudeResetHandlerStub struct {
|
|
key string
|
|
calls int
|
|
}
|
|
|
|
func (s *claudeResetHandlerStub) Query(context.Context, int64) (*service.ClaudeResetCredits, error) {
|
|
return &service.ClaudeResetCredits{Credits: []service.ClaudeResetCredit{}}, nil
|
|
}
|
|
|
|
func (s *claudeResetHandlerStub) Redeem(_ context.Context, _ int64, key string) (*service.ClaudeResetOutcome, error) {
|
|
s.calls++
|
|
s.key = key
|
|
if key == "" {
|
|
return nil, service.ErrIdempotencyKeyRequired
|
|
}
|
|
return &service.ClaudeResetOutcome{Outcome: "reset", Cleared: []string{"five_hour"}}, nil
|
|
}
|
|
|
|
func TestClaudeResetHandlerRedeemContract(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
stub := &claudeResetHandlerStub{}
|
|
h := &AccountHandler{claudeResetCredits: stub}
|
|
r := gin.New()
|
|
r.POST("/accounts/:id/claude/reset-credits/redeem", h.RedeemClaudeResetCredit)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/accounts/1/claude/reset-credits/redeem", nil)
|
|
req.Header.Set("Idempotency-Key", "same-operation")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
require.Equal(t, "same-operation", stub.key)
|
|
require.Contains(t, w.Body.String(), `"outcome":"reset"`)
|
|
|
|
w = httptest.NewRecorder()
|
|
r.ServeHTTP(w, httptest.NewRequest(http.MethodPost, "/accounts/1/claude/reset-credits/redeem", nil))
|
|
require.Equal(t, http.StatusBadRequest, w.Code)
|
|
require.Contains(t, w.Body.String(), "IDEMPOTENCY_KEY_REQUIRED")
|
|
|
|
w = httptest.NewRecorder()
|
|
r.ServeHTTP(w, httptest.NewRequest(http.MethodPost, "/accounts/abc/claude/reset-credits/redeem", nil))
|
|
require.Equal(t, http.StatusBadRequest, w.Code)
|
|
require.Equal(t, 2, stub.calls)
|
|
}
|