2026-09-20 19:23:00 +08:00
|
|
|
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")
|
|
|
|
|
}
|
2026-10-02 13:08:40 +08:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|