package router import ( "bytes" "errors" "fmt" "io" "net/http" "net/http/httptest" "strings" "sync" "sync/atomic" "testing" "time" "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" "github.com/QuantumNous/new-api/controller" "github.com/QuantumNous/new-api/i18n" "github.com/QuantumNous/new-api/middleware" "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/pkg/jsplugin" "github.com/QuantumNous/new-api/plugins" "github.com/gin-contrib/gzip" "github.com/gin-gonic/gin" "github.com/glebarez/sqlite" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/gorm" ) func TestPluginDispatcherMissFallsThroughWithoutLeakingInnerResponse(t *testing.T) { outer, registry := newPluginRouterTest(t, nil, nil) dispatcher := (&pluginRouteDispatcher{registry: registry}).dispatch outer.NoRoute( dispatcher, func(c *gin.Context) { assert.Equal(t, "before", c.GetString(middleware.RouteTagKey)) c.Header("X-Fallback", "true") c.String(http.StatusOK, "spa") }, ) recorder := performPluginRequest(outer, http.MethodGet, "/not-owned") assert.Equal(t, http.StatusOK, recorder.Code) assert.Equal(t, "spa", recorder.Body.String()) assert.Equal(t, "true", recorder.Header().Get("X-Fallback")) assert.Empty(t, recorder.Header().Get("Location")) } func TestPluginDebugLogsOnlyOwnedRoutes(t *testing.T) { previousDebug := common.DebugEnabled common.DebugEnabled = true t.Cleanup(func() { common.DebugEnabled = previousDebug }) var output bytes.Buffer common.LogWriterMu.Lock() previousWriter := gin.DefaultErrorWriter gin.DefaultErrorWriter = &output common.LogWriterMu.Unlock() t.Cleanup(func() { common.LogWriterMu.Lock() gin.DefaultErrorWriter = previousWriter common.LogWriterMu.Unlock() }) plugin := compileRouterPlugin(t, "debug-owner", "1.0.0", `[ {method: "GET", path: "/vendor/debug", type: "dynamic"} ]`) handlers := func(generation *jsplugin.RoutingGeneration, group jsplugin.RouteGroup) []gin.HandlerFunc { production := productionPluginRouteHandlers(generation, group) return []gin.HandlerFunc{ production[0], func(c *gin.Context) { c.Status(http.StatusNoContent) }, } } outer, registry := newPluginRouterTest(t, []*jsplugin.LoadedPlugin{plugin}, handlers) outer.NoRoute( (&pluginRouteDispatcher{registry: registry}).dispatch, func(c *gin.Context) { c.String(http.StatusOK, "fallback") }, ) output.Reset() owned := performPluginRequest(outer, http.MethodGet, "/vendor/debug") require.Equal(t, http.StatusNoContent, owned.Code) logOutput := output.String() assert.Contains(t, logOutput, "request-phase-two") assert.Contains(t, logOutput, "task_plugin subsystem=router event=route_matched") assert.Contains(t, logOutput, `plugin="debug-owner"`) assert.NotContains(t, logOutput, "/vendor/debug") assert.Contains(t, logOutput, "event=route_complete") output.Reset() miss := performPluginRequest(outer, http.MethodGet, "/not-owned") require.Equal(t, http.StatusOK, miss.Code) assert.Equal(t, "fallback", miss.Body.String()) assert.NotContains(t, output.String(), "task_plugin") } func TestPluginAuthoredHeaderOnly404PassesThrough(t *testing.T) { plugin := compileRouterPlugin(t, "authored-404", "1.0.0", `[ {method: "GET", path: "/vendor/missing", type: "dynamic"} ]`) handlers := testPluginRouteHandlers(func(c *gin.Context, _ *jsplugin.RoutingGeneration, _ jsplugin.RouteBinding) { c.Header("X-Plugin", "authored") c.Status(http.StatusNotFound) }) outer, registry := newPluginRouterTest(t, []*jsplugin.LoadedPlugin{plugin}, handlers) outer.NoRoute( (&pluginRouteDispatcher{registry: registry}).dispatch, func(c *gin.Context) { c.String(http.StatusOK, "fallback") }, ) recorder := performPluginRequest(outer, http.MethodGet, "/vendor/missing") assert.Equal(t, http.StatusNotFound, recorder.Code) assert.Empty(t, recorder.Body.String()) assert.Equal(t, "authored", recorder.Header().Get("X-Plugin")) } func TestPluginOwnedPathMethodMismatchReturns405(t *testing.T) { plugin := compileRouterPlugin(t, "method-owner", "1.0.0", `[ {method: "GET", path: "/vendor/jobs/:task_id", type: "query", render: "native"} ]`) outer, registry := newPluginRouterTest(t, []*jsplugin.LoadedPlugin{plugin}, testPluginRouteHandlers( func(c *gin.Context, _ *jsplugin.RoutingGeneration, _ jsplugin.RouteBinding) { c.String(http.StatusOK, "plugin") }, )) outer.NoRoute( (&pluginRouteDispatcher{registry: registry}).dispatch, func(c *gin.Context) { c.String(http.StatusOK, "fallback") }, ) recorder := performPluginRequest(outer, http.MethodPost, "/vendor/jobs/task-1") assert.Equal(t, http.StatusMethodNotAllowed, recorder.Code) assert.Empty(t, recorder.Body.String()) } func TestPluginTrailingSlashMissDoesNotRedirect(t *testing.T) { for _, testCase := range []struct { name string routePath string requestPath string }{ {name: "declared without slash", routePath: "/vendor/job", requestPath: "/vendor/job/"}, {name: "declared with slash", routePath: "/vendor/job/", requestPath: "/vendor/job"}, } { t.Run(testCase.name, func(t *testing.T) { plugin := compileRouterPlugin(t, "slash-owner", "1.0.0", fmt.Sprintf(`[ {method: "GET", path: %q, type: "dynamic"} ]`, testCase.routePath)) outer, registry := newPluginRouterTest(t, []*jsplugin.LoadedPlugin{plugin}, testPluginRouteHandlers( func(c *gin.Context, _ *jsplugin.RoutingGeneration, _ jsplugin.RouteBinding) { c.String(http.StatusOK, "plugin") }, )) outer.NoRoute( (&pluginRouteDispatcher{registry: registry}).dispatch, func(c *gin.Context) { c.String(http.StatusOK, "fallback") }, ) recorder := performPluginRequest(outer, http.MethodGet, testCase.requestPath) assert.Equal(t, http.StatusOK, recorder.Code) assert.Equal(t, "fallback", recorder.Body.String()) assert.Empty(t, recorder.Header().Get("Location")) }) } } func TestPluginSSEFlushesWithoutFallbackBuffering(t *testing.T) { plugin := compileRouterPlugin(t, "stream-owner", "1.0.0", `[ {method: "GET", path: "/vendor/stream", type: "dynamic"} ]`) handlers := testPluginRouteHandlers(func(c *gin.Context, _ *jsplugin.RoutingGeneration, _ jsplugin.RouteBinding) { c.Header("Content-Type", "text/event-stream") _, _ = c.Writer.WriteString("data: ready\n\n") c.Writer.Flush() }) outer, registry := newPluginRouterTest(t, []*jsplugin.LoadedPlugin{plugin}, handlers) outer.NoRoute( (&pluginRouteDispatcher{registry: registry}).dispatch, middleware.RouteTag("web"), gzip.Gzip(gzip.DefaultCompression), middleware.Cache(), func(c *gin.Context) { c.String(http.StatusOK, "fallback") }, ) request := httptest.NewRequest(http.MethodGet, "/vendor/stream", nil) request.Header.Set("Accept-Encoding", "gzip") recorder := httptest.NewRecorder() outer.ServeHTTP(recorder, request) assert.True(t, recorder.Flushed) assert.Equal(t, "text/event-stream", recorder.Header().Get("Content-Type")) assert.Equal(t, "data: ready\n\n", recorder.Body.String()) assert.Empty(t, recorder.Header().Get("Content-Encoding")) assert.Empty(t, recorder.Header().Get("Cache-Control")) assert.Empty(t, recorder.Header().Get("Cache-Version")) } // A plugin route reaches the client connection through the gated writer: a // relayed stream that stopped early cuts it off, and the client sees the // stream end as the vendor's did. func TestPluginRouteCanCutOffItsClientStream(t *testing.T) { plugin := compileRouterPlugin(t, "cut-owner", "1.0.0", `[ {method: "GET", path: "/vendor/stream", type: "dynamic"} ]`) handlers := testPluginRouteHandlers(func(c *gin.Context, _ *jsplugin.RoutingGeneration, _ jsplugin.RouteBinding) { c.Header("Content-Type", "text/event-stream") _, _ = c.Writer.WriteString("data: ready\n\n") c.Writer.Flush() assert.NoError(t, http.NewResponseController(c.Writer).SetWriteDeadline(time.Now())) }) outer, registry := newPluginRouterTest(t, []*jsplugin.LoadedPlugin{plugin}, handlers) outer.NoRoute((&pluginRouteDispatcher{registry: registry}).dispatch) server := httptest.NewServer(outer) defer server.Close() response, err := http.Get(server.URL + "/vendor/stream") require.NoError(t, err) defer response.Body.Close() body, err := io.ReadAll(response.Body) assert.Equal(t, "data: ready\n\n", string(body)) assert.ErrorIs(t, err, io.ErrUnexpectedEOF) } func TestWebCacheHeadersDoNotLeakOntoPluginRoutes(t *testing.T) { plugin := compileRouterPlugin(t, "cache-owner", "1.0.0", `[ {method: "GET", path: "/vendor/status/:task_id", type: "query", render: "native"} ]`) outer, registry := newPluginRouterTest(t, []*jsplugin.LoadedPlugin{plugin}, testPluginRouteHandlers( func(c *gin.Context, _ *jsplugin.RoutingGeneration, _ jsplugin.RouteBinding) { c.JSON(http.StatusOK, gin.H{"status": "queued"}) }, )) outer.NoRoute( (&pluginRouteDispatcher{registry: registry}).dispatch, middleware.Cache(), func(c *gin.Context) { c.String(http.StatusOK, "fallback") }, ) pluginResponse := performPluginRequest(outer, http.MethodGet, "/vendor/status/task-1") assert.Empty(t, pluginResponse.Header().Get("Cache-Control")) assert.Empty(t, pluginResponse.Header().Get("Cache-Version")) fallbackResponse := performPluginRequest(outer, http.MethodGet, "/unknown") assert.Equal(t, "max-age=604800", fallbackResponse.Header().Get("Cache-Control")) assert.NotEmpty(t, fallbackResponse.Header().Get("Cache-Version")) } func TestPluginInnerContextImportsRequestMetadataAndTrustedProxyConfig(t *testing.T) { plugin := compileRouterPlugin(t, "context-owner", "1.0.0", `[ {method: "GET", path: "/vendor/context", type: "dynamic"} ]`) handlers := testPluginRouteHandlers(func(c *gin.Context, _ *jsplugin.RoutingGeneration, _ jsplugin.RouteBinding) { c.JSON(http.StatusOK, gin.H{ "request_id": c.GetString(common.RequestIdKey), "language": c.GetString(string(constant.ContextKeyLanguage)), "client_ip": c.ClientIP(), "route_tag": c.GetString(middleware.RouteTagKey), }) }) outer, registry := newPluginRouterTestWithProxies(t, []*jsplugin.LoadedPlugin{plugin}, handlers, []string{"127.0.0.0/8"}) outer.NoRoute((&pluginRouteDispatcher{registry: registry}).dispatch) request := httptest.NewRequest(http.MethodGet, "/vendor/context", nil) request.RemoteAddr = "127.0.0.1:1234" request.Header.Set("X-Forwarded-For", "203.0.113.20") recorder := httptest.NewRecorder() outer.ServeHTTP(recorder, request) assert.JSONEq(t, `{"request_id":"request-phase-two","language":"zh-CN","client_ip":"203.0.113.20","route_tag":"relay"}`, recorder.Body.String()) } func TestPluginRequestPinsGenerationAcrossHotSwap(t *testing.T) { v1 := compileRouterPlugin(t, "hot-swap", "1.0.0", `[ {method: "GET", path: "/vendor/version", type: "dynamic"} ]`) v2 := compileRouterPlugin(t, "hot-swap", "2.0.0", `[ {method: "GET", path: "/vendor/version", type: "dynamic"} ]`) started := make(chan struct{}) release := make(chan struct{}) var startOnce sync.Once t.Cleanup(func() { select { case <-release: default: close(release) } }) var registry *jsplugin.Registry handlers := testPluginRouteHandlers(func(c *gin.Context, generation *jsplugin.RoutingGeneration, binding jsplugin.RouteBinding) { if binding.Plugin.Meta.Version == "1.0.0" { startOnce.Do(func() { close(started) }) <-release } pinnedValue, exists := c.Get(jsplugin.ContextKeyPinnedRoute) pinned, ok := pinnedValue.(jsplugin.PinnedRoute) if !exists || !ok || pinned.Plugin == nil || pinned.Generation == nil { c.AbortWithStatus(http.StatusInternalServerError) return } c.JSON(http.StatusOK, gin.H{ "version": binding.Plugin.Meta.Version, "generation": generation.Number, "pinned_version": pinned.Plugin.Meta.Version, "pinned_generation": pinned.Generation.Number, "current_generation": registry.Generation().Number, }) }) outer, activeRegistry := newPluginRouterTest(t, []*jsplugin.LoadedPlugin{v1}, handlers) registry = activeRegistry outer.NoRoute((&pluginRouteDispatcher{registry: registry}).dispatch) firstGeneration := registry.Generation().Number firstDone := make(chan *httptest.ResponseRecorder, 1) go func() { firstDone <- performPluginRequest(outer, http.MethodGet, "/vendor/version") }() select { case <-started: case <-time.After(2 * time.Second): require.FailNow(t, "first generation request did not start") } require.NoError(t, registry.ReplaceOverrides([]*jsplugin.LoadedPlugin{v2})) secondGeneration := registry.Generation().Number close(release) var first *httptest.ResponseRecorder select { case first = <-firstDone: case <-time.After(2 * time.Second): require.FailNow(t, "pinned first generation request did not finish") } assert.JSONEq(t, fmt.Sprintf(`{ "version": "1.0.0", "generation": %d, "pinned_version": "1.0.0", "pinned_generation": %d, "current_generation": %d }`, firstGeneration, firstGeneration, secondGeneration), first.Body.String()) second := performPluginRequest(outer, http.MethodGet, "/vendor/version") assert.JSONEq(t, fmt.Sprintf(`{ "version": "2.0.0", "generation": %d, "pinned_version": "2.0.0", "pinned_generation": %d, "current_generation": %d }`, secondGeneration, secondGeneration, secondGeneration), second.Body.String()) } // A host route, or the trailing-slash redirect it causes, shadows only the // top-level route of a built-in plugin; the /{key} copy still serves it. func TestPluginStaticRouteConflictsAreExcluded(t *testing.T) { tests := []struct { name string staticPath string route string requestPath string served bool }{ {name: "parameter intersection", staticPath: "/core/:id", route: `{method: "GET", path: "/core/fixed", type: "dynamic"}`, requestPath: "/core/fixed"}, {name: "trailing slash redirect shadow", staticPath: "/fixed", route: `{method: "GET", path: "/fixed/", type: "dynamic"}`, requestPath: "/fixed/"}, {name: "catchall redirect shadow", staticPath: "/files/*filepath", route: `{method: "GET", path: "/files", type: "dynamic"}`, requestPath: "/files"}, {name: "literal against a host parameter", staticPath: "/jobs/:id/content", route: `{method: "GET", path: "/jobs/fixed/:name", type: "dynamic"}`, requestPath: "/jobs/fixed/content"}, {name: "query task id against a host parameter", staticPath: "/jobs/:id/content", route: `{method: "GET", path: "/jobs/:task_id/content", type: "query"}`, requestPath: "/jobs/task_1/content"}, {name: "other method", staticPath: "/core/:id", route: `{method: "POST", path: "/core/fixed", type: "submit"}`, requestPath: "/core/fixed", served: true}, {name: "query task id against a host literal", staticPath: "/jobs/:id/content", route: `{method: "GET", path: "/jobs/fixed/:task_id", type: "query"}`, requestPath: "/jobs/fixed/task_1", served: true}, } for _, testCase := range tests { t.Run(testCase.name, func(t *testing.T) { outer := newOuterPluginTestEngine() outer.GET(testCase.staticPath, func(c *gin.Context) { c.String(http.StatusOK, "static") }) registry := jsplugin.NewRegistry() plugin, err := registry.RegisterFactory(routerPluginSource("static-conflict", "1.0.0", "["+testCase.route+"]"), jsplugin.Options{}) require.NoError(t, err) builder := newPluginGenerationBuilder(outer.Routes(), nil, testPluginRouteHandlers( func(c *gin.Context, _ *jsplugin.RoutingGeneration, _ jsplugin.RouteBinding) { c.String(http.StatusOK, "plugin") }, )) require.NoError(t, registry.SetGenerationPreparer(builder.prepare)) outer.NoRoute( (&pluginRouteDispatcher{registry: registry}).dispatch, func(c *gin.Context) { c.String(http.StatusOK, "fallback") }, ) method := plugin.Meta.Routes[0].Method _, active := registry.Get("static-conflict") assert.True(t, active) assert.Empty(t, registry.RoutingErrors()) assert.Equal(t, "plugin", performPluginRequest(outer, method, "/static-conflict"+testCase.requestPath).Body.String()) recorder := performPluginRequest(outer, method, testCase.requestPath) if testCase.served { assert.Equal(t, "plugin", recorder.Body.String()) assert.Empty(t, registry.Generation().SkippedRoutes()) return } assert.NotEqual(t, "plugin", recorder.Body.String()) assert.Equal(t, []jsplugin.SkippedRoute{{PluginKey: "static-conflict", Route: plugin.Meta.Routes[0], Reason: "host_route"}}, registry.Generation().SkippedRoutes()) }) } } // Built-in and uploaded plugins alike serve their top-level paths, /v1 ones // included, unless a host route answers first or the first segment is another // plugin's key. Every plugin keeps its /{key} address, so a key may not name a // host segment or a built-in plugin's own prefix. func TestTopLevelRoutesSkipOnlyHostRoutesAndOtherKeys(t *testing.T) { outer := newOuterPluginTestEngine() outer.GET("/v1/models", func(c *gin.Context) { c.String(http.StatusOK, "host models") }) outer.GET("/v1beta/models", func(c *gin.Context) { c.String(http.StatusOK, "host gemini") }) outer.POST("/pg/chat/completions", func(c *gin.Context) { c.String(http.StatusOK, "host playground") }) registry := jsplugin.NewRegistry() builtIn, err := registry.RegisterFactory(routerPluginSource("builtin-probe", "1.0.0", `[ {method: "POST", path: "/v1/videos/foo", type: "submit"}, {method: "GET", path: "/v1/models", type: "query"} ]`), jsplugin.Options{}) require.NoError(t, err) for _, key := range []string{"alibaba", "sunoapi"} { prefix := map[string]string{"alibaba": "ali", "sunoapi": "suno"}[key] _, err = registry.RegisterFactory(routerPluginSource(key, "1.0.0", `[{method: "POST", path: "/`+prefix+`/jobs", type: "submit"}]`), jsplugin.Options{}) require.NoError(t, err) } third := compileRouterPlugin(t, "third-probe", "1.0.0", `[ {method: "POST", path: "/v1/systemone", type: "submit"}, {method: "POST", path: "/ent/v2/text2video", type: "submit"}, {method: "POST", path: "/beta/jobs", type: "submit"}, {method: "GET", path: "/v1/models", type: "query"} ]`) overrides := []*jsplugin.LoadedPlugin{third, compileRouterPlugin(t, "beta", "1.0.0", `[{method: "POST", path: "/own/jobs", type: "submit"}]`)} for _, key := range []string{"pg", "v1beta", "ali", "suno"} { overrides = append(overrides, compileRouterPlugin(t, key, "1.0.0", `[{method: "POST", path: "/own/jobs", type: "submit"}]`)) } require.NoError(t, registry.ReplaceOverrides(overrides)) builder := newPluginGenerationBuilder(outer.Routes(), nil, testPluginRouteHandlers(func(c *gin.Context, _ *jsplugin.RoutingGeneration, binding jsplugin.RouteBinding) { c.String(http.StatusOK, binding.Plugin.Meta.Key) })) require.NoError(t, registry.SetGenerationPreparer(builder.prepare)) outer.NoRoute( (&pluginRouteDispatcher{registry: registry}).dispatch, func(c *gin.Context) { c.String(http.StatusOK, "fallback") }, ) for _, request := range []struct{ method, path, answer string }{ {http.MethodPost, "/v1/videos/foo", "builtin-probe"}, {http.MethodPost, "/builtin-probe/v1/videos/foo", "builtin-probe"}, {http.MethodPost, "/v1/systemone", "third-probe"}, {http.MethodPost, "/third-probe/v1/systemone", "third-probe"}, {http.MethodGet, "/v1/models", "host models"}, {http.MethodGet, "/builtin-probe/v1/models", "builtin-probe"}, {http.MethodGet, "/third-probe/v1/models", "third-probe"}, {http.MethodPost, "/ent/v2/text2video", "third-probe"}, {http.MethodPost, "/beta/jobs", "fallback"}, {http.MethodPost, "/third-probe/beta/jobs", "third-probe"}, {http.MethodPost, "/beta/own/jobs", "beta"}, } { assert.Equal(t, request.answer, performPluginRequest(outer, request.method, request.path).Body.String(), request.method+" "+request.path) } assert.ElementsMatch(t, []jsplugin.SkippedRoute{ {PluginKey: "builtin-probe", Route: builtIn.Meta.Routes[1], Reason: "host_route"}, {PluginKey: "third-probe", Route: third.Meta.Routes[2], Reason: "key_namespace"}, {PluginKey: "third-probe", Route: third.Meta.Routes[3], Reason: "host_route"}, }, registry.Generation().SkippedRoutes()) routingErrors := registry.RoutingErrors() assert.Len(t, routingErrors, 4) assert.Contains(t, routingErrors["pg"], `plugin key "pg" is a host path segment`) assert.Contains(t, routingErrors["v1beta"], `plugin key "v1beta" is a host path segment`) assert.Contains(t, routingErrors["ali"], `plugin key "ali" is a prefix owned by built-in plugin alibaba`) assert.Contains(t, routingErrors["suno"], `plugin key "suno" is a prefix owned by built-in plugin sunoapi`) } // Plugins declaring the same path shape share one registration whose // candidates are all of them; each candidate reads the parameters under the // names it declares, and /{key} reaches one plugin only. func TestSharedPluginRoutesRegisterOnceWithEveryCandidate(t *testing.T) { alpha := compileRouterPlugin(t, "shared-alpha", "1.0.0", `[ {method: "POST", path: "/probe/v1/jobs", type: "submit"}, {method: "GET", path: "/probe/v1/jobs/:task_id", type: "query"} ]`) beta := compileRouterPlugin(t, "shared-beta", "1.0.0", `[ {method: "POST", path: "/probe/v1/jobs", type: "submit"}, {method: "GET", path: "/probe/v1/jobs/:job_id", type: "query", taskIdParam: "job_id"} ]`) handlers := testPluginRouteHandlers(func(c *gin.Context, _ *jsplugin.RoutingGeneration, _ jsplugin.RouteBinding) { pinned := c.MustGet(jsplugin.ContextKeyPinnedRoute).(jsplugin.PinnedRoute) answers := make([]string, 0, len(pinned.Candidates)) for _, candidate := range pinned.Candidates { answers = append(answers, fmt.Sprintf("%s %v", candidate.Plugin.Meta.Key, jsplugin.RouteParams(c.Params, candidate.Route))) } c.String(http.StatusOK, strings.Join(answers, " ")) }) outer, registry := newPluginRouterTest(t, []*jsplugin.LoadedPlugin{beta, alpha}, handlers) outer.NoRoute((&pluginRouteDispatcher{registry: registry}).dispatch) require.Empty(t, registry.RoutingErrors()) registrations := 0 for _, group := range registry.Generation().RouteGroups() { if group.Method == http.MethodPost && group.Shape == "/probe/v1/jobs" { registrations++ assert.Len(t, group.Bindings, 2) } } assert.Equal(t, 1, registrations) assert.Equal(t, "shared-alpha map[] shared-beta map[]", performPluginRequest(outer, http.MethodPost, "/probe/v1/jobs").Body.String()) assert.Equal(t, "shared-alpha map[task_id:task_1] shared-beta map[job_id:task_1]", performPluginRequest(outer, http.MethodGet, "/probe/v1/jobs/task_1").Body.String()) assert.Equal(t, "shared-beta map[]", performPluginRequest(outer, http.MethodPost, "/shared-beta/probe/v1/jobs").Body.String()) assert.Equal(t, "shared-beta map[job_id:task_2]", performPluginRequest(outer, http.MethodGet, "/shared-beta/probe/v1/jobs/task_2").Body.String()) } func TestBuiltInPluginRoutesAreServedBesideHostRoutes(t *testing.T) { require.Empty(t, jsplugin.DefaultRegistry.RoutingErrors()) builtIns := jsplugin.DefaultRegistry.Generation().Plugins() require.NotEmpty(t, builtIns) // The static routes SetRouter registers before the plugin router. outer := gin.New() SetApiRouter(outer) SetDashboardRouter(outer) SetRelayRouter(outer) SetTaskPluginProtocolRouter(outer) SetVideoRouter(outer) SetTaskRouter(outer) registry := jsplugin.NewRegistry() for _, plugin := range builtIns { source, err := plugins.Source(plugin.Meta.Key) require.NoError(t, err) _, err = registry.RegisterFactory(source, jsplugin.Options{Key: plugin.Meta.Key}) require.NoError(t, err) } builder := newPluginGenerationBuilder(outer.Routes(), nil, testPluginRouteHandlers( func(c *gin.Context, _ *jsplugin.RoutingGeneration, binding jsplugin.RouteBinding) { c.String(http.StatusOK, binding.Plugin.Meta.Key) }, )) require.NoError(t, registry.SetGenerationPreparer(builder.prepare)) outer.NoRoute((&pluginRouteDispatcher{registry: registry}).dispatch, controller.RelayNotFound) require.Empty(t, registry.RoutingErrors()) require.Empty(t, registry.Generation().SkippedRoutes()) for _, plugin := range builtIns { _, active := registry.Get(plugin.Meta.Key) assert.True(t, active, plugin.Meta.Key) } const vertexModel = "/projects/sample/locations/us-central1/publishers/google/models/veo-3.1-generate-preview" for _, request := range []struct{ method, path, plugin string }{ {http.MethodPost, "/api/v3/contents/generations/tasks", "doubao"}, {http.MethodGet, "/api/v3/contents/generations/tasks/task_sample", "doubao"}, {http.MethodPost, "/api/v3/images/generations", "doubao"}, {http.MethodPost, "/doubao/api/v3/contents/generations/tasks", "doubao"}, {http.MethodPost, "/api/v1/services/aigc/multimodal-generation/generation", "alibaba"}, {http.MethodPost, "/api/v1/services/aigc/video-generation/video-synthesis", "alibaba"}, {http.MethodGet, "/api/v1/tasks/task_sample", "alibaba"}, {http.MethodPost, "/alibaba/api/v1/services/aigc/video-generation/video-synthesis", "alibaba"}, {http.MethodPost, "/ali/api/v1/services/aigc/video-generation/video-synthesis", "alibaba"}, {http.MethodGet, "/ali/api/v1/tasks/task_sample", "alibaba"}, {http.MethodPost, "/v1/videos/text2video", "kling"}, {http.MethodGet, "/v1/videos/image2video/task_sample", "kling"}, {http.MethodPost, "/kling/v1/videos/text2video", "kling"}, {http.MethodPost, "/v1/videos/generations", "xai"}, {http.MethodPost, "/v1/videos/extensions", "xai"}, {http.MethodGet, "/xai/v1/videos/task_sample", "xai"}, {http.MethodGet, "/v1beta/models/veo-3.1-generate-preview/operations/task_sample", "google"}, {http.MethodPost, "/google/v1beta/models/veo-3.1-generate-preview:predictLongRunning", "google"}, {http.MethodPost, "/ent/v2/reference2video", "vidu"}, {http.MethodGet, "/ent/v2/tasks/task_sample/creations", "vidu"}, {http.MethodPost, "/vidu/ent/v2/text2video", "vidu"}, {http.MethodPost, "/v1" + vertexModel + ":predictLongRunning", "vertex-ai"}, {http.MethodPost, "/v1beta1" + vertexModel + ":predictLongRunning", "vertex-ai"}, {http.MethodPost, "/vertex-ai/v1beta1" + vertexModel + ":fetchPredictOperation", "vertex-ai"}, {http.MethodPost, "/v1/video_generation", "hailuo"}, {http.MethodGet, "/v1/query/video_generation?task_id=task_sample", "hailuo"}, {http.MethodGet, "/hailuo/v1/query/video_generation?task_id=task_sample", "hailuo"}, {http.MethodGet, "/hailuo/v1/query/video_generation/task_sample", "hailuo"}, {http.MethodGet, "/v1/files/retrieve?file_id=task_sample", "hailuo"}, {http.MethodPost, "/v2/video_generation", "hailuo"}, {http.MethodGet, "/v2/query/video_generation/task_sample", "hailuo"}, {http.MethodPost, "/api/common/v3/submit_task_v2", "jimeng"}, {http.MethodPost, "/api/common/v3/get_result_v2", "jimeng"}, {http.MethodPost, "/jimeng/api/common/v3/submit_task_v2", "jimeng"}, {http.MethodPost, "/suno/fetch", "sunoapi"}, {http.MethodGet, "/suno/fetch/task_sample", "sunoapi"}, } { recorder := performPluginRequest(outer, request.method, request.path) assert.Equal(t, request.plugin, recorder.Body.String(), request.method+" "+request.path) } // A prefix is never doubled, and GET /v1/files/:id is no longer a host route. for _, request := range []struct{ method, path string }{ {http.MethodPost, "/sunoapi/suno/fetch"}, {http.MethodPost, "/alibaba/ali/api/v1/services/aigc/video-generation/video-synthesis"}, {http.MethodGet, "/v1/files/file_sample"}, } { recorder := performPluginRequest(outer, request.method, request.path) assert.Equal(t, http.StatusNotFound, recorder.Code, request.method+" "+request.path) assert.Contains(t, recorder.Body.String(), "Invalid URL", request.method+" "+request.path) } } func TestPluginRouteOwnershipCanSwapWithinOneGeneration(t *testing.T) { alphaV1 := compileRouterPlugin(t, "route-swap-alpha", "1.0.0", `[ {method: "GET", path: "/route-swap/alpha", type: "dynamic"} ]`) betaV1 := compileRouterPlugin(t, "route-swap-beta", "1.0.0", `[ {method: "GET", path: "/route-swap/beta", type: "dynamic"} ]`) alphaV2 := compileRouterPlugin(t, "route-swap-alpha", "2.0.0", `[ {method: "GET", path: "/route-swap/beta", type: "dynamic"} ]`) betaV2 := compileRouterPlugin(t, "route-swap-beta", "2.0.0", `[ {method: "GET", path: "/route-swap/alpha", type: "dynamic"} ]`) handlers := testPluginRouteHandlers(func(c *gin.Context, _ *jsplugin.RoutingGeneration, binding jsplugin.RouteBinding) { c.String(http.StatusOK, binding.Plugin.Meta.Key+"@"+binding.Plugin.Meta.Version) }) outer, registry := newPluginRouterTest(t, []*jsplugin.LoadedPlugin{alphaV1, betaV1}, handlers) outer.NoRoute((&pluginRouteDispatcher{registry: registry}).dispatch) require.NoError(t, registry.ReplaceOverrides([]*jsplugin.LoadedPlugin{alphaV2, betaV2})) assert.Equal(t, "route-swap-beta@2.0.0", performPluginRequest(outer, http.MethodGet, "/route-swap/alpha").Body.String()) assert.Equal(t, "route-swap-alpha@2.0.0", performPluginRequest(outer, http.MethodGet, "/route-swap/beta").Body.String()) assert.Empty(t, registry.RoutingErrors()) } // Overlapping routes of different plugins are all admitted: per method, Gin // answers a literal segment before a parameter. func TestPluginPathOwnershipIsExclusivePerMethod(t *testing.T) { alpha := compileRouterPlugin(t, "alpha-owner", "1.0.0", `[ {method: "GET", path: "/shared/jobs/:task_id", type: "query", render: "native"} ]`) beta := compileRouterPlugin(t, "beta-owner", "1.0.0", `[ {method: "POST", path: "/shared/jobs/:task_id", type: "submit"} ]`) gamma := compileRouterPlugin(t, "gamma-owner", "1.0.0", `[ {method: "GET", path: "/shared/jobs/fixed", type: "dynamic"} ]`) // Display priority must not change route ownership. alpha.Meta.SortPriority = -100 gamma.Meta.SortPriority = 100 handlers := testPluginRouteHandlers(func(c *gin.Context, _ *jsplugin.RoutingGeneration, binding jsplugin.RouteBinding) { c.String(http.StatusOK, binding.Plugin.Meta.Key) }) outer, registry := newPluginRouterTest(t, []*jsplugin.LoadedPlugin{gamma, beta, alpha}, handlers) outer.NoRoute( (&pluginRouteDispatcher{registry: registry}).dispatch, func(c *gin.Context) { c.String(http.StatusOK, "fallback") }, ) assert.Equal(t, "alpha-owner", performPluginRequest(outer, http.MethodGet, "/shared/jobs/task-1").Body.String()) assert.Equal(t, "beta-owner", performPluginRequest(outer, http.MethodPost, "/shared/jobs/task-1").Body.String()) assert.Equal(t, "gamma-owner", performPluginRequest(outer, http.MethodGet, "/shared/jobs/fixed").Body.String()) assert.Equal(t, http.StatusMethodNotAllowed, performPluginRequest(outer, http.MethodPut, "/shared/jobs/task-1").Code) assert.Empty(t, registry.RoutingErrors()) } func TestOnePluginMayOwnMultipleMethodsForSamePath(t *testing.T) { plugin := compileRouterPlugin(t, "multi-method", "1.0.0", `[ {method: "GET", path: "/vendor/multi/:task_id", type: "query", render: "native"}, {method: "POST", path: "/vendor/multi/:task_id", type: "submit"} ]`) handlers := testPluginRouteHandlers(func(c *gin.Context, _ *jsplugin.RoutingGeneration, _ jsplugin.RouteBinding) { c.String(http.StatusOK, c.Request.Method) }) outer, registry := newPluginRouterTest(t, []*jsplugin.LoadedPlugin{plugin}, handlers) outer.NoRoute((&pluginRouteDispatcher{registry: registry}).dispatch) assert.Equal(t, http.MethodGet, performPluginRequest(outer, http.MethodGet, "/vendor/multi/task-1").Body.String()) assert.Equal(t, http.MethodPost, performPluginRequest(outer, http.MethodPost, "/vendor/multi/task-1").Body.String()) assert.NotContains(t, registry.RoutingErrors(), "multi-method") } // The inner router names wildcards by position, so routes may name the same // position differently; each reads its own parameter names. func TestPositionalWildcardsKeepDeclaredParameterNames(t *testing.T) { plugin := compileRouterPlugin(t, "wildcard-names", "1.0.0", `[ {method: "GET", path: "/vendor/:id/first", type: "query", render: "native", taskIdParam: "id"}, {method: "GET", path: "/vendor/:name/second", type: "query", render: "native", taskIdParam: "name"} ]`) outer, registry := newPluginRouterTest(t, []*jsplugin.LoadedPlugin{plugin}, func(generation *jsplugin.RoutingGeneration, group jsplugin.RouteGroup) []gin.HandlerFunc { return []gin.HandlerFunc{func(c *gin.Context) { c.String(http.StatusOK, fmt.Sprint(jsplugin.RouteParams(c.Params, group.Bindings[0].Route))) }} }) outer.NoRoute( (&pluginRouteDispatcher{registry: registry}).dispatch, func(c *gin.Context) { c.String(http.StatusOK, "fallback") }, ) assert.Equal(t, "map[id:1]", performPluginRequest(outer, http.MethodGet, "/vendor/1/first").Body.String()) assert.Equal(t, "map[name:2]", performPluginRequest(outer, http.MethodGet, "/vendor/2/second").Body.String()) assert.Equal(t, "map[name:3]", performPluginRequest(outer, http.MethodGet, "/wildcard-names/vendor/3/second").Body.String()) assert.Empty(t, registry.RoutingErrors()) } func TestGinRegistrationPanicRebuildsWithoutOffender(t *testing.T) { alpha := compileRouterPlugin(t, "panic-alpha", "1.0.0", `[ {method: "GET", path: "/panic/alpha", type: "dynamic"} ]`) beta := compileRouterPlugin(t, "panic-beta", "1.0.0", `[ {method: "GET", path: "/panic/beta", type: "dynamic"} ]`) registry := jsplugin.NewRegistry() require.NoError(t, registry.ReplaceOverrides([]*jsplugin.LoadedPlugin{alpha, beta})) outer := newOuterPluginTestEngine() builder := newPluginGenerationBuilder(outer.Routes(), nil, testPluginRouteHandlers( func(c *gin.Context, _ *jsplugin.RoutingGeneration, binding jsplugin.RouteBinding) { c.String(http.StatusOK, binding.Plugin.Meta.Key) }, )) normalRegister := builder.registerRoute builder.registerRoute = func(engine *gin.Engine, group jsplugin.RouteGroup, handlers []gin.HandlerFunc) { if group.Bindings[0].Plugin.Meta.Key == "panic-beta" { panic("registration failed") } normalRegister(engine, group, handlers) } require.NoError(t, registry.SetGenerationPreparer(builder.prepare)) outer.NoRoute( (&pluginRouteDispatcher{registry: registry}).dispatch, func(c *gin.Context) { c.String(http.StatusOK, "fallback") }, ) assert.Equal(t, "panic-alpha", performPluginRequest(outer, http.MethodGet, "/panic/alpha").Body.String()) assert.Equal(t, "fallback", performPluginRequest(outer, http.MethodGet, "/panic/beta").Body.String()) assert.Contains(t, registry.RoutingErrors()["panic-beta"], "registration panic") } func TestGinRegistrationPanicReadmitsPluginBlockedByOffender(t *testing.T) { alpha := compileRouterPlugin(t, "panic-owner-alpha", "1.0.0", `[ {method: "GET", path: "/panic/reconsider", type: "dynamic"} ]`) beta := compileRouterPlugin(t, "panic-owner-beta", "1.0.0", `[ {method: "GET", path: "/panic/:name", type: "dynamic"} ]`) registry := jsplugin.NewRegistry() require.NoError(t, registry.ReplaceOverrides([]*jsplugin.LoadedPlugin{alpha, beta})) outer := newOuterPluginTestEngine() builder := newPluginGenerationBuilder(outer.Routes(), nil, testPluginRouteHandlers( func(c *gin.Context, _ *jsplugin.RoutingGeneration, binding jsplugin.RouteBinding) { c.String(http.StatusOK, binding.Plugin.Meta.Key) }, )) normalRegister := builder.registerRoute builder.registerRoute = func(engine *gin.Engine, group jsplugin.RouteGroup, handlers []gin.HandlerFunc) { if group.Bindings[0].Plugin.Meta.Key == "panic-owner-alpha" { panic("registration failed") } normalRegister(engine, group, handlers) } require.NoError(t, registry.SetGenerationPreparer(builder.prepare)) outer.NoRoute((&pluginRouteDispatcher{registry: registry}).dispatch) _, alphaActive := registry.Get("panic-owner-alpha") assert.False(t, alphaActive) activeBeta, ok := registry.Get("panic-owner-beta") require.True(t, ok) assert.Same(t, beta, activeBeta) assert.Equal(t, "panic-owner-beta", performPluginRequest(outer, http.MethodGet, "/panic/reconsider").Body.String()) assert.Contains(t, registry.RoutingErrors()["panic-owner-alpha"], "registration panic") assert.NotContains(t, registry.RoutingErrors(), "panic-owner-beta") } func TestUpdatedRegistrationPanicRestoresIncumbent(t *testing.T) { v1 := compileRouterPlugin(t, "panic-update", "1.0.0", `[ {method: "GET", path: "/panic/stable", type: "dynamic"} ]`) v2 := compileRouterPlugin(t, "panic-update", "2.0.0", `[ {method: "GET", path: "/panic/stable", type: "dynamic"} ]`) registry := jsplugin.NewRegistry() require.NoError(t, registry.ReplaceOverrides([]*jsplugin.LoadedPlugin{v1})) outer := newOuterPluginTestEngine() handlers := testPluginRouteHandlers(func(c *gin.Context, _ *jsplugin.RoutingGeneration, binding jsplugin.RouteBinding) { c.String(http.StatusOK, binding.Plugin.Meta.Version) }) builder := newPluginGenerationBuilder(outer.Routes(), nil, handlers) normalRegister := builder.registerRoute builder.registerRoute = func(engine *gin.Engine, group jsplugin.RouteGroup, routeHandlers []gin.HandlerFunc) { if group.Bindings[0].Plugin.Meta.Version == "2.0.0" { panic("new version registration failed") } normalRegister(engine, group, routeHandlers) } require.NoError(t, registry.SetGenerationPreparer(builder.prepare)) outer.NoRoute((&pluginRouteDispatcher{registry: registry}).dispatch) require.NoError(t, registry.ReplaceOverrides([]*jsplugin.LoadedPlugin{v2})) active, ok := registry.Get("panic-update") require.True(t, ok) assert.Same(t, v1, active) assert.Equal(t, "1.0.0", performPluginRequest(outer, http.MethodGet, "/panic/stable").Body.String()) assert.Contains(t, registry.RoutingErrors()["panic-update"], "registration panic") } func TestUnattributableRebuildFailureRetainsOldGeneration(t *testing.T) { v1 := compileRouterPlugin(t, "rebuild-stable", "1.0.0", `[ {method: "GET", path: "/rebuild/version", type: "dynamic"} ]`) v2 := compileRouterPlugin(t, "rebuild-stable", "2.0.0", `[ {method: "GET", path: "/rebuild/version", type: "dynamic"} ]`) handlers := testPluginRouteHandlers(func(c *gin.Context, _ *jsplugin.RoutingGeneration, binding jsplugin.RouteBinding) { c.String(http.StatusOK, binding.Plugin.Meta.Version) }) outer := newOuterPluginTestEngine() registry := jsplugin.NewRegistry() require.NoError(t, registry.ReplaceOverrides([]*jsplugin.LoadedPlugin{v1})) builder := newPluginGenerationBuilder(outer.Routes(), nil, handlers) require.NoError(t, registry.SetGenerationPreparer(builder.prepare)) outer.NoRoute((&pluginRouteDispatcher{registry: registry}).dispatch) before := registry.Generation() normalConfigure := builder.configure builder.configure = func(*gin.Engine) error { return errors.New("engine configuration failed") } require.Error(t, registry.ReplaceOverrides([]*jsplugin.LoadedPlugin{v2})) assert.Same(t, before, registry.Generation()) assert.Equal(t, "1.0.0", performPluginRequest(outer, http.MethodGet, "/rebuild/version").Body.String()) assert.Contains(t, registry.LastRebuildError(), "engine configuration failed") builder.configure = normalConfigure require.NoError(t, registry.ReplaceOverrides([]*jsplugin.LoadedPlugin{v2})) assert.Equal(t, "2.0.0", performPluginRequest(outer, http.MethodGet, "/rebuild/version").Body.String()) } func TestPluginRouteRecoverySanitizesPanicResponse(t *testing.T) { plugin := compileRouterPlugin(t, "panic-route", "1.0.0", `[ {method: "GET", path: "/vendor/panic", type: "dynamic"} ]`) var calls atomic.Int32 outer, registry := newPluginRouterTest(t, []*jsplugin.LoadedPlugin{plugin}, testPluginRouteHandlers( func(c *gin.Context, _ *jsplugin.RoutingGeneration, _ jsplugin.RouteBinding) { if calls.Add(1) == 1 { panic("https://secret.example/internal?token=credential") } c.String(http.StatusOK, "recovered") }, )) outer.NoRoute((&pluginRouteDispatcher{registry: registry}).dispatch) recorder := performPluginRequest(outer, http.MethodGet, "/vendor/panic") assert.Equal(t, http.StatusInternalServerError, recorder.Code) assert.Contains(t, recorder.Body.String(), "internal plugin route error") assert.NotContains(t, recorder.Body.String(), "secret.example") assert.NotContains(t, recorder.Body.String(), "credential") second := performPluginRequest(outer, http.MethodGet, "/vendor/panic") assert.Equal(t, http.StatusOK, second.Code) assert.Equal(t, "recovered", second.Body.String()) } func TestProductionPluginRoutePipelineRequiresTokenAuth(t *testing.T) { plugin := compileRouterPlugin(t, "forced-auth", "1.0.0", `[ {method: "GET", path: "/vendor/protected/:task_id", type: "query", render: "native"} ]`) outer, registry := newPluginRouterTest(t, []*jsplugin.LoadedPlugin{plugin}, productionPluginRouteHandlers) outer.NoRoute((&pluginRouteDispatcher{registry: registry}).dispatch) recorder := performPluginRequest(outer, http.MethodGet, "/vendor/protected/task-1") assert.NotEqual(t, http.StatusNotImplemented, recorder.Code) assert.Contains(t, recorder.Body.String(), "error") } func TestProductionPluginNativeQueryTraversesInnerRouter(t *testing.T) { previousDB := model.DB database, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) require.NoError(t, database.AutoMigrate(&model.Task{})) model.DB = database t.Cleanup(func() { model.DB = previousDB }) require.NoError(t, database.Create(&model.Task{ TaskID: "task_native_router", Platform: constant.TaskPlatform("kling"), UserId: 91, Status: model.TaskStatusSuccess, Progress: "100%", ChannelId: 17, PrivateData: model.TaskPrivateData{ UpstreamTaskID: "private_upstream_id", ResultURL: "https://secret.example/video.mp4", }, }).Error) kling, found := jsplugin.DefaultRegistry.Get("kling") require.True(t, found) authenticatedProductionHandlers := func( generation *jsplugin.RoutingGeneration, group jsplugin.RouteGroup, ) []gin.HandlerFunc { production := productionPluginRouteHandlers(generation, group) return []gin.HandlerFunc{ production[0], func(c *gin.Context) { common.SetContextKey(c, constant.ContextKeyUserId, 91) common.SetContextKey(c, constant.ContextKeyUserGroup, "default") common.SetContextKey(c, constant.ContextKeyTokenGroup, "default") c.Next() }, production[3], production[4], production[5], production[6], production[7], } } outer, registry := newPluginRouterTest(t, []*jsplugin.LoadedPlugin{kling}, authenticatedProductionHandlers) outer.NoRoute((&pluginRouteDispatcher{registry: registry}).dispatch) request := httptest.NewRequest(http.MethodGet, "/kling/v1/videos/text2video/task_native_router", nil) recorder := httptest.NewRecorder() outer.ServeHTTP(recorder, request) require.Equal(t, http.StatusOK, recorder.Code, recorder.Body.String()) assert.Contains(t, recorder.Body.String(), `"task_id":"task_native_router"`) assert.Contains(t, recorder.Body.String(), `"task_status":"succeed"`) assert.NotContains(t, recorder.Body.String(), "private_upstream_id") assert.NotContains(t, recorder.Body.String(), "secret.example") } // On the built-in plugin whose vendor uses it, a native route takes the // gateway token in that vendor's API-key form; everywhere else only the Bearer // form works. TokenAuth validates the token whatever form carried it. func TestNativeRouteCredentialFormsAuthenticateThroughTokenAuth(t *testing.T) { setupRelayRouterTestDB(t) require.NoError(t, i18n.Init()) user := model.User{Username: "native-auth-user", Status: common.UserStatusEnabled, Group: "default", Quota: 100} require.NoError(t, model.DB.Create(&user).Error) allowedIPs := "10.9.8.0/24" require.NoError(t, model.DB.Create(&model.Token{UserId: user.Id, Key: "nativeauthkey", Status: common.TokenStatusEnabled, ExpiredTime: -1, UnlimitedQuota: true}).Error) require.NoError(t, model.DB.Create(&model.Token{UserId: user.Id, Key: "nativeiplimited", Status: common.TokenStatusEnabled, ExpiredTime: -1, UnlimitedQuota: true, AllowIps: &allowedIPs}).Error) outer := gin.New() SetTaskPluginProtocolRouter(outer) registry := jsplugin.NewRegistry() for _, key := range []string{"vidu", "google", "doubao"} { source, err := plugins.Source(key) require.NoError(t, err) _, err = registry.RegisterFactory(source, jsplugin.Options{Key: key}) require.NoError(t, err) } handlers := func(generation *jsplugin.RoutingGeneration, group jsplugin.RouteGroup) []gin.HandlerFunc { production := productionPluginRouteHandlers(generation, group) return []gin.HandlerFunc{production[0], production[1], production[2], func(c *gin.Context) { c.String(http.StatusOK, "user %d %s", c.GetInt("id"), c.Request.RequestURI) }} } require.NoError(t, registry.SetGenerationPreparer(newPluginGenerationBuilder(outer.Routes(), nil, handlers).prepare)) require.Empty(t, registry.RoutingErrors()) outer.NoRoute((&pluginRouteDispatcher{registry: registry}).dispatch) const operation = "/v1beta/models/veo-3.1-generate-preview/operations/task_1" for _, testCase := range []struct { name string method string path string header, value string status int requestURI string }{ {name: "Vidu Token form", method: http.MethodPost, path: "/ent/v2/text2video", header: "Authorization", value: "Token nativeauthkey", status: http.StatusOK}, {name: "Vidu Token form under /vidu", method: http.MethodPost, path: "/vidu/ent/v2/text2video", header: "Authorization", value: "token nativeauthkey", status: http.StatusOK}, {name: "Token form on another plugin", method: http.MethodPost, path: "/doubao/api/v3/contents/generations/tasks", header: "Authorization", value: "Token nativeauthkey", status: http.StatusUnauthorized}, {name: "Token form on a host protocol", method: http.MethodPost, path: "/v1/videos", header: "Authorization", value: "Token nativeauthkey", status: http.StatusUnauthorized}, {name: "Gemini header form", method: http.MethodPost, path: "/google/v1beta/models/veo-3.1-generate-preview:predictLongRunning", header: "X-Goog-Api-Key", value: "nativeauthkey", status: http.StatusOK}, {name: "Gemini header form on a query", method: http.MethodGet, path: "/google" + operation, header: "X-Goog-Api-Key", value: "nativeauthkey", status: http.StatusOK}, {name: "Gemini query form", method: http.MethodGet, path: "/google" + operation + "?key=nativeauthkey&alt=json", status: http.StatusOK, requestURI: "/google" + operation + "?alt=json"}, {name: "Gemini query form at the vendor path", method: http.MethodGet, path: operation + "?key=nativeauthkey", status: http.StatusOK, requestURI: operation}, {name: "Gemini header form on another plugin", method: http.MethodPost, path: "/vidu/ent/v2/text2video", header: "X-Goog-Api-Key", value: "nativeauthkey", status: http.StatusUnauthorized}, {name: "Gemini query form on another plugin", method: http.MethodPost, path: "/vidu/ent/v2/text2video?key=nativeauthkey", status: http.StatusUnauthorized}, {name: "invalid token in the Token form", method: http.MethodPost, path: "/ent/v2/text2video", header: "Authorization", value: "Token nativewrongkey", status: http.StatusUnauthorized}, {name: "empty token in the Token form", method: http.MethodPost, path: "/ent/v2/text2video", header: "Authorization", value: "Token ", status: http.StatusUnauthorized}, {name: "invalid token in the Gemini header form", method: http.MethodGet, path: "/google" + operation, header: "X-Goog-Api-Key", value: "nativewrongkey", status: http.StatusUnauthorized}, {name: "invalid token in the Gemini query form", method: http.MethodGet, path: "/google" + operation + "?key=nativewrongkey", status: http.StatusUnauthorized}, {name: "IP limits apply to the Token form", method: http.MethodPost, path: "/ent/v2/text2video", header: "Authorization", value: "Token nativeiplimited", status: http.StatusForbidden}, } { t.Run(testCase.name, func(t *testing.T) { request := httptest.NewRequest(testCase.method, testCase.path, strings.NewReader(`{}`)) request.RemoteAddr = "192.0.2.10:1234" if testCase.header != "" { request.Header.Set(testCase.header, testCase.value) } recorder := httptest.NewRecorder() outer.ServeHTTP(recorder, request) assert.Equal(t, testCase.status, recorder.Code, recorder.Body.String()) if testCase.status == http.StatusOK { requestURI := testCase.requestURI if requestURI == "" { requestURI = testCase.path } assert.Equal(t, fmt.Sprintf("user %d %s", user.Id, requestURI), recorder.Body.String()) return } for _, secret := range []string{"nativeauthkey", "nativewrongkey", "nativeiplimited"} { assert.NotContains(t, recorder.Body.String(), secret) } }) } } func newPluginRouterTest( t *testing.T, plugins []*jsplugin.LoadedPlugin, handlers pluginRouteHandlers, ) (*gin.Engine, *jsplugin.Registry) { t.Helper() return newPluginRouterTestWithProxies(t, plugins, handlers, nil) } func newPluginRouterTestWithProxies( t *testing.T, plugins []*jsplugin.LoadedPlugin, handlers pluginRouteHandlers, trustedProxies []string, ) (*gin.Engine, *jsplugin.Registry) { t.Helper() gin.SetMode(gin.TestMode) outer := newOuterPluginTestEngine() registry := jsplugin.NewRegistry() if plugins != nil { require.NoError(t, registry.ReplaceOverrides(plugins)) } if handlers == nil { handlers = testPluginRouteHandlers(func(c *gin.Context, _ *jsplugin.RoutingGeneration, binding jsplugin.RouteBinding) { c.String(http.StatusOK, binding.Plugin.Meta.Key) }) } builder := newPluginGenerationBuilder(outer.Routes(), trustedProxies, handlers) require.NoError(t, registry.SetGenerationPreparer(builder.prepare)) return outer, registry } func newOuterPluginTestEngine() *gin.Engine { outer := gin.New() outer.Use(func(c *gin.Context) { c.Set(common.RequestIdKey, "request-phase-two") c.Set(string(constant.ContextKeyLanguage), "zh-CN") c.Set(middleware.RouteTagKey, "before") c.Next() }) return outer } func testPluginRouteHandlers( handler func(*gin.Context, *jsplugin.RoutingGeneration, jsplugin.RouteBinding), ) pluginRouteHandlers { return func(generation *jsplugin.RoutingGeneration, group jsplugin.RouteGroup) []gin.HandlerFunc { binding := group.Bindings[0] return []gin.HandlerFunc{func(c *gin.Context) { pinnedGeneration := generation if state, _ := c.Request.Context().Value(pluginDispatchStateKey{}).(*pluginDispatchState); state != nil && state.generation != nil { pinnedGeneration = state.generation } c.Set(jsplugin.ContextKeyPinnedRoute, jsplugin.PinnedRoute{ Generation: pinnedGeneration, Plugin: binding.Plugin, Route: binding.Route, Candidates: group.Bindings, }) handler(c, pinnedGeneration, binding) }} } } func compileRouterPlugin(t *testing.T, key, version, routes string) *jsplugin.LoadedPlugin { t.Helper() plugin, err := jsplugin.CompilePlugin(routerPluginSource(key, version, routes), jsplugin.Options{Key: key, Version: version}) require.NoError(t, err) return plugin } func routerPluginSource(key, version, routes string) string { return fmt.Sprintf(` export const meta = { apiVersion: 1, key: %q, name: %q, version: %q, author: {name: "Test"}, models: ["model"], fetchMode: "per_task", routes: (%s).map(function(route) { const migrated = Object.assign({}, route); delete migrated.renderer; migrated.render = route.render || route.renderer || "render"; if (route.type !== "query") migrated.decode = route.decode || "decode"; return migrated; }), }; export const native = { decode: function(ctx) { return {kind: "submit", model: "model", requestBody: ctx.body.value}; }, render: function(ctx, task) { return task; }, native: function(ctx, task) { return task; }, }; export function buildSubmitRequest() { return {}; } export function parseSubmitResponse() { return {}; } export function buildQueryRequest() { return {}; } export function parseTaskResult() { return {}; } `, key, key, version, routes) } func performPluginRequest(handler http.Handler, method, path string) *httptest.ResponseRecorder { recorder := httptest.NewRecorder() request := httptest.NewRequest(method, path, strings.NewReader("")) handler.ServeHTTP(recorder, request) return recorder } func TestWebFallbackDoesNotCacheMissingAPIOrAssets(t *testing.T) { outer := gin.New() SetWebRouter(outer, WebAssets{IndexPage: []byte("dashboard")}, func(c *gin.Context) { c.Next() }) for _, path := range []string{"/api/user/token/status", "/api/audit/self?p=1", "/v1/missing", "/assets/missing.js"} { t.Run(path, func(t *testing.T) { response := performPluginRequest(outer, http.MethodGet, path) assert.Equal(t, http.StatusNotFound, response.Code) assert.Contains(t, response.Body.String(), "Invalid URL") assert.Contains(t, response.Header().Get("Cache-Control"), "no-store") assert.NotContains(t, response.Header().Get("Cache-Control"), "604800") }) } page := performPluginRequest(outer, http.MethodGet, "/security") assert.Equal(t, http.StatusOK, page.Code) assert.Equal(t, "dashboard", page.Body.String()) assert.Equal(t, "no-cache", page.Header().Get("Cache-Control")) } func TestSecurityRoutesDisableCachingBeforeAuthentication(t *testing.T) { outer := gin.New() SetApiRouter(outer) for _, path := range []string{"/api/user/access_tokens", "/api/user/access_tokens/catalog", "/api/user/access_tokens/scopes", "/api/audit/self", "/api/audit"} { t.Run(path, func(t *testing.T) { response := performPluginRequest(outer, http.MethodGet, path) assert.Equal(t, http.StatusUnauthorized, response.Code) assert.Contains(t, response.Header().Get("Cache-Control"), "no-store") }) } }