new-api/middleware/concurrency_test.go
zizi 3a25007df2 feat: enforce relay concurrency limits
Add user RPM and concurrency middleware, enforce channel concurrency for relay selection and retry paths, and wire checks into relay routes.
2026-05-20 14:40:36 +08:00

138 lines
3.7 KiB
Go

package middleware
import (
"errors"
"net/http"
"net/http/httptest"
"testing"
"github.com/QuantumNous/new-api/model"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)
func restoreConcurrencyStubs(t *testing.T) {
t.Helper()
previousGetUser := getUserByIdForConcurrency
previousCheckRPM := checkUserRPMForConcurrency
previousAcquire := acquireUserConcurrencyForCheck
t.Cleanup(func() {
getUserByIdForConcurrency = previousGetUser
checkUserRPMForConcurrency = previousCheckRPM
acquireUserConcurrencyForCheck = previousAcquire
})
}
func performConcurrencyRequest(handler gin.HandlerFunc, userId int) *httptest.ResponseRecorder {
gin.SetMode(gin.TestMode)
router := gin.New()
router.GET("/test", func(c *gin.Context) {
if userId > 0 {
c.Set("id", userId)
}
}, handler, func(c *gin.Context) {
c.Status(http.StatusNoContent)
})
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/test", nil)
router.ServeHTTP(recorder, request)
return recorder
}
func TestConcurrencyCheckAllowsWhenUserMissingFromContext(t *testing.T) {
restoreConcurrencyStubs(t)
getUserByIdForConcurrency = func(id int, selectAll bool) (*model.User, error) {
t.Fatalf("expected user lookup to be skipped, got id %d", id)
return nil, nil
}
recorder := performConcurrencyRequest(ConcurrencyCheck(), 0)
require.Equal(t, http.StatusNoContent, recorder.Code)
}
func TestConcurrencyCheckFailOpenWhenUserLookupFails(t *testing.T) {
restoreConcurrencyStubs(t)
getUserByIdForConcurrency = func(id int, selectAll bool) (*model.User, error) {
return nil, errors.New("database unavailable")
}
recorder := performConcurrencyRequest(ConcurrencyCheck(), 1)
require.Equal(t, http.StatusNoContent, recorder.Code)
}
func TestConcurrencyCheckAllowsWhenLimitsDisabled(t *testing.T) {
restoreConcurrencyStubs(t)
getUserByIdForConcurrency = func(id int, selectAll bool) (*model.User, error) {
return &model.User{Id: id}, nil
}
checkUserRPMForConcurrency = func(userId, limit int) bool {
t.Fatalf("expected RPM check to be skipped")
return false
}
acquireUserConcurrencyForCheck = func(userId, limit int) (bool, func()) {
t.Fatalf("expected concurrency acquisition to be skipped")
return false, func() {}
}
recorder := performConcurrencyRequest(ConcurrencyCheck(), 1)
require.Equal(t, http.StatusNoContent, recorder.Code)
}
func TestConcurrencyCheckRejectsWhenRPMExceeded(t *testing.T) {
restoreConcurrencyStubs(t)
getUserByIdForConcurrency = func(id int, selectAll bool) (*model.User, error) {
return &model.User{Id: id, RpmLimit: 1}, nil
}
checkUserRPMForConcurrency = func(userId, limit int) bool {
return false
}
recorder := performConcurrencyRequest(ConcurrencyCheck(), 1)
require.Equal(t, http.StatusTooManyRequests, recorder.Code)
}
func TestConcurrencyCheckRejectsWhenUserConcurrencyExceeded(t *testing.T) {
restoreConcurrencyStubs(t)
getUserByIdForConcurrency = func(id int, selectAll bool) (*model.User, error) {
return &model.User{Id: id, ConcurrencyLimit: 1}, nil
}
acquireUserConcurrencyForCheck = func(userId, limit int) (bool, func()) {
return false, func() {}
}
recorder := performConcurrencyRequest(ConcurrencyCheck(), 1)
require.Equal(t, http.StatusTooManyRequests, recorder.Code)
}
func TestConcurrencyCheckReleasesAfterRequest(t *testing.T) {
restoreConcurrencyStubs(t)
released := false
getUserByIdForConcurrency = func(id int, selectAll bool) (*model.User, error) {
return &model.User{Id: id, ConcurrencyLimit: 1}, nil
}
acquireUserConcurrencyForCheck = func(userId, limit int) (bool, func()) {
return true, func() {
released = true
}
}
recorder := performConcurrencyRequest(ConcurrencyCheck(), 1)
require.Equal(t, http.StatusNoContent, recorder.Code)
require.True(t, released)
}