Files
sub2api/backend/internal/handler/gateway_systemone_test.go
T
shaw 2d8a386811 fix(typesafe): 补齐平台迁移、审计覆盖、端点隔离与错误策略
- 新增 241 迁移:user_platform_quotas / composite_model_routes 的 CHECK 约束
  加入 typesafe。此前设置 typesafe 默认配额会让注册时的多行配额快照整体
  违约(fail-open 后新用户所有平台配额丢失),单用户配额与 Composite 路由保存 500。
- Prompt 审计新增 typesafe_systemone 协议提取(state + 各问题 instructions/
  criteria/选项标签,键排序保证哈希稳定);此前提取为空,阻断与异步审计均被放行。
  旧内容审核同样覆盖问题文本,且不再丢弃含 <system-reminder> 的 System One 文本。
- TypeSafe 分组及路由到 TypeSafe 的 Composite 请求访问 Messages / count_tokens /
  Chat Completions / Responses 时返回 404,避免以 x-api-key 打到错误上游路径并
  污染账号状态;TypeSafe 账号 base_url 为空时不再回退 api.anthropic.com。
- SystemOne 补在途余额预留、利润控制准入终检、wrapReleaseOnDone,失败切换改用
  共享 FailoverState(同账号重试 / 池模式 / 临时封禁)。
- 上游错误沿用共享账号错误策略:402/403、自定义错误码、临时不可调度规则生效,
  记录 ops 上游错误;400/422 仍不切换且永不改变账号状态。
- 账号测试改走原生 System One 探测,可用模型只返回 jev-latest;
  响应超限显式报错,响应 Content-Type 仅透传 JSON 类型。
2026-10-02 13:08:40 +08:00

110 lines
4.9 KiB
Go

package handler
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/Wei-Shaw/sub2api/internal/config"
middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)
const validSystemOneHandlerBody = `{"model":"jev-latest","state":"sample","questions":{"q":{"type":"noul","instructions":"Evaluate"}}}`
func newSystemOneHandlerContext(body string) (*gin.Context, *httptest.ResponseRecorder) {
gin.SetMode(gin.TestMode)
recorder := httptest.NewRecorder()
c, _ := gin.CreateTestContext(recorder)
c.Request = httptest.NewRequest(http.MethodPost, "/v1/systemone", strings.NewReader(body))
c.Request.Header.Set("Content-Type", "application/json")
return c, recorder
}
func TestSystemOneRequiresAuthentication(t *testing.T) {
c, recorder := newSystemOneHandlerContext(validSystemOneHandlerBody)
(&GatewayHandler{}).SystemOne(c)
require.Equal(t, http.StatusUnauthorized, recorder.Code)
require.Contains(t, recorder.Body.String(), "authentication_error")
}
func TestSystemOneRejectsNonTypeSafeGroupBeforeScheduling(t *testing.T) {
c, recorder := newSystemOneHandlerContext(validSystemOneHandlerBody)
groupID := int64(3)
c.Set(string(middleware2.ContextKeyAPIKey), &service.APIKey{ID: 4, UserID: 5, GroupID: &groupID, Group: &service.Group{ID: groupID, Platform: service.PlatformOpenAI}})
c.Set(string(middleware2.ContextKeyUser), middleware2.AuthSubject{UserID: 5, Concurrency: 1})
(&GatewayHandler{cfg: &config.Config{Gateway: config.GatewayConfig{MaxBodySize: 1 << 20}}}).SystemOne(c)
require.Equal(t, http.StatusNotFound, recorder.Code)
require.Contains(t, recorder.Body.String(), "only available for TypeSafe")
}
func newTypeSafeGroupContext(t *testing.T, path, body, groupPlatform string) (*gin.Context, *httptest.ResponseRecorder) {
t.Helper()
c, recorder := newSystemOneHandlerContext(body)
c.Request = httptest.NewRequest(http.MethodPost, path, strings.NewReader(body))
c.Request.Header.Set("Content-Type", "application/json")
groupID := int64(9)
c.Set(string(middleware2.ContextKeyAPIKey), &service.APIKey{ID: 4, UserID: 5, GroupID: &groupID, Group: &service.Group{ID: groupID, Platform: groupPlatform}})
c.Set(string(middleware2.ContextKeyUser), middleware2.AuthSubject{UserID: 5, Concurrency: 1})
return c, recorder
}
func TestRejectSystemOneOnlyPlatform(t *testing.T) {
write := func(c *gin.Context, status int, errType, message string) {
c.JSON(status, gin.H{"type": errType, "message": message})
}
c, recorder := newTypeSafeGroupContext(t, "/v1/messages", `{}`, service.PlatformTypeSafe)
require.True(t, rejectSystemOneOnlyPlatform(c, mustAPIKey(t, c), write))
require.Equal(t, http.StatusNotFound, recorder.Code)
require.Contains(t, recorder.Body.String(), systemOneOnlyPlatformMessage)
c, _ = newTypeSafeGroupContext(t, "/v1/messages", `{}`, service.PlatformComposite)
require.False(t, rejectSystemOneOnlyPlatform(c, mustAPIKey(t, c), write))
c.Request = c.Request.WithContext(service.WithResolvedTargetPlatform(c.Request.Context(), service.PlatformTypeSafe))
require.True(t, rejectSystemOneOnlyPlatform(c, mustAPIKey(t, c), write))
c, _ = newTypeSafeGroupContext(t, "/v1/messages", `{}`, service.PlatformAnthropic)
require.False(t, rejectSystemOneOnlyPlatform(c, mustAPIKey(t, c), write))
c, _ = newTypeSafeGroupContext(t, "/antigravity/v1/messages", `{}`, service.PlatformTypeSafe)
c.Set(string(middleware2.ContextKeyForcePlatform), service.PlatformAntigravity)
require.False(t, rejectSystemOneOnlyPlatform(c, mustAPIKey(t, c), write))
}
func mustAPIKey(t *testing.T, c *gin.Context) *service.APIKey {
t.Helper()
apiKey, ok := middleware2.GetAPIKeyFromContext(c)
require.True(t, ok)
return apiKey
}
func TestTypeSafeGroupsRejectNonSystemOneProtocolsBeforeScheduling(t *testing.T) {
h := &GatewayHandler{
cfg: &config.Config{Gateway: config.GatewayConfig{MaxBodySize: 1 << 20}},
gatewayService: &service.GatewayService{},
}
for _, tc := range []struct {
name string
path string
body string
handler func(*gin.Context)
}{
{"messages", "/v1/messages", `{"model":"jev-latest","max_tokens":1,"messages":[{"role":"user","content":"hi"}]}`, h.Messages},
{"count_tokens", "/v1/messages/count_tokens", `{"model":"jev-latest","messages":[{"role":"user","content":"hi"}]}`, h.CountTokens},
{"chat_completions", "/v1/chat/completions", `{"model":"jev-latest","messages":[{"role":"user","content":"hi"}]}`, h.ChatCompletions},
{"responses", "/v1/responses", `{"model":"jev-latest","input":"hi"}`, h.Responses},
} {
t.Run(tc.name, func(t *testing.T) {
c, recorder := newTypeSafeGroupContext(t, tc.path, tc.body, service.PlatformTypeSafe)
tc.handler(c)
require.Equal(t, http.StatusNotFound, recorder.Code, recorder.Body.String())
require.Contains(t, recorder.Body.String(), systemOneOnlyPlatformMessage)
})
}
}