Files
sub2api/backend/internal/handler/admin/claude_reset_handler_test.go
T
196b47757a feat(claude): redeem native limit resets with server-selected grant
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>
2026-09-29 11:41:33 -04:00

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)
}