mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-15 23:22:59 +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>
58 lines
2.3 KiB
Go
58 lines
2.3 KiB
Go
package routes
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/handler"
|
|
adminhandler "github.com/Wei-Shaw/sub2api/internal/handler/admin"
|
|
servermiddleware "github.com/Wei-Shaw/sub2api/internal/server/middleware"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// The Claude redeem route consumes an irreversible credit just like the Codex
|
|
// reset-quota route, so it must sit behind exactly the same middleware chain.
|
|
func TestClaudeResetRedeemRouteMatchesCodexResetProtection(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
handlers := &handler.Handlers{Admin: &handler.AdminHandlers{Account: &adminhandler.AccountHandler{}, OpenAIOAuth: &adminhandler.OpenAIOAuthHandler{}}}
|
|
chains := map[string][]string{}
|
|
adminAuth := servermiddleware.AdminAuthMiddleware(func(c *gin.Context) {
|
|
names := c.HandlerNames()
|
|
chains[c.FullPath()] = names[:len(names)-1]
|
|
if c.GetHeader("Authorization") == "" {
|
|
servermiddleware.AbortWithError(c, http.StatusUnauthorized, "UNAUTHORIZED", "Authorization required")
|
|
return
|
|
}
|
|
servermiddleware.AbortWithError(c, http.StatusForbidden, "FORBIDDEN", "Admin access required")
|
|
})
|
|
auditLog := servermiddleware.AuditLogMiddleware(func(c *gin.Context) { c.Next() })
|
|
stepUp := servermiddleware.StepUpAuthMiddleware(func(c *gin.Context) { c.Next() })
|
|
RegisterAdminRoutes(router.Group("/api/v1"), handlers, adminAuth, auditLog, stepUp, nil, nil)
|
|
|
|
codex := "/api/v1/admin/openai/accounts/1/reset-quota"
|
|
claude := "/api/v1/admin/accounts/1/claude/reset-credits/redeem"
|
|
for _, path := range []string{codex, claude} {
|
|
for _, auth := range []string{"", "Bearer user-token"} {
|
|
recorder := httptest.NewRecorder()
|
|
request := httptest.NewRequest(http.MethodPost, path, nil)
|
|
if auth != "" {
|
|
request.Header.Set("Authorization", auth)
|
|
}
|
|
router.ServeHTTP(recorder, request)
|
|
if auth == "" {
|
|
require.Equal(t, http.StatusUnauthorized, recorder.Code, path)
|
|
} else {
|
|
require.Equal(t, http.StatusForbidden, recorder.Code, path)
|
|
}
|
|
}
|
|
}
|
|
codexChain := chains["/api/v1/admin/openai/accounts/:id/reset-quota"]
|
|
claudeChain := chains["/api/v1/admin/accounts/:id/claude/reset-credits/redeem"]
|
|
require.NotEmpty(t, codexChain)
|
|
require.Equal(t, strings.Join(codexChain, "\n"), strings.Join(claudeChain, "\n"))
|
|
}
|