From c5cfdb15e56bcae0947e6bfb4cbbb8d3f1fe1a0b Mon Sep 17 00:00:00 2001 From: LTbinglingfeng Date: Sat, 13 Jun 2026 20:52:07 +0800 Subject: [PATCH 1/4] Add plugin delete management endpoint --- internal/api/handlers/management/plugins.go | 96 +++++++++++++++++++ .../api/handlers/management/plugins_test.go | 75 +++++++++++++++ internal/api/server.go | 1 + internal/api/server_test.go | 11 +++ 4 files changed, 183 insertions(+) diff --git a/internal/api/handlers/management/plugins.go b/internal/api/handlers/management/plugins.go index 3c7ed100..078098a3 100644 --- a/internal/api/handlers/management/plugins.go +++ b/internal/api/handlers/management/plugins.go @@ -2,8 +2,10 @@ package management import ( "encoding/json" + "errors" "fmt" "net/http" + "os" "sort" "strconv" "strings" @@ -297,6 +299,87 @@ func (h *Handler) PatchPluginConfig(c *gin.Context) { h.persistLocked(c) } +// DeletePlugin removes the selected local plugin file and its saved config. +func (h *Handler) DeletePlugin(c *gin.Context) { + id, okID := pluginIDFromRequest(c) + if !okID { + return + } + if h == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "plugin_not_found", "message": "plugin not found"}) + return + } + + h.mu.Lock() + if h.cfg == nil { + h.mu.Unlock() + c.JSON(http.StatusNotFound, gin.H{"error": "plugin_not_found", "message": "plugin not found"}) + return + } + pluginsDir := normalizedPluginsDir(h.cfg.Plugins.Dir) + _, configured := h.cfg.Plugins.Configs[id] + host := h.pluginHost + h.mu.Unlock() + + path, errPath := pluginFilePath(pluginsDir, id) + if errPath != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "plugin_discovery_failed", "message": errPath.Error()}) + return + } + if path == "" && !configured { + c.JSON(http.StatusNotFound, gin.H{"error": "plugin_not_found", "message": "plugin not found"}) + return + } + + if pluginLoaded(host, id) && (host == nil || !host.UnloadPlugin(id)) && pluginLoaded(host, id) { + c.JSON(http.StatusConflict, gin.H{ + "error": "plugin_delete_requires_restart", + "message": "loaded plugin cannot be deleted while the server is running", + "restart_required": true, + }) + return + } + + fileDeleted := false + if path != "" { + if errRemove := os.Remove(path); errRemove != nil { + if !errors.Is(errRemove, os.ErrNotExist) { + c.JSON(http.StatusInternalServerError, gin.H{"error": "plugin_delete_failed", "message": errRemove.Error()}) + return + } + } else { + fileDeleted = true + } + } + + h.mu.Lock() + delete(h.cfg.Plugins.Configs, id) + if configured { + if errSave := config.SaveConfigPreserveComments(h.configFilePath, h.cfg); errSave != nil { + h.mu.Unlock() + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "config_save_failed", + "message": fmt.Sprintf("plugin deleted but saving config failed: %s", errSave.Error()), + "file_deleted": fileDeleted, + "path": path, + }) + return + } + } + reloadCfg := h.cfg + h.mu.Unlock() + + h.reloadConfigAfterManagementSave(c.Request.Context(), reloadCfg) + c.JSON(http.StatusOK, gin.H{ + "status": "deleted", + "id": htmlsanitize.String(id), + "path": htmlsanitize.String(path), + "file_deleted": fileDeleted, + "configured_removed": configured, + "restart_required": false, + }) +} + func normalizedPluginsDir(dir string) string { dir = strings.TrimSpace(dir) if dir == "" { @@ -337,6 +420,19 @@ func pluginDiscovered(pluginsDir string, id string) (bool, error) { return false, nil } +func pluginFilePath(pluginsDir string, id string) (string, error) { + files, errDiscover := pluginhost.DiscoverPluginFiles(pluginsDir) + if errDiscover != nil { + return "", errDiscover + } + for _, file := range files { + if file.ID == id { + return file.Path, nil + } + } + return "", nil +} + func pluginConfigFields(fields []pluginapi.ConfigField) []pluginConfigFieldInfo { out := make([]pluginConfigFieldInfo, 0, len(fields)) for _, field := range fields { diff --git a/internal/api/handlers/management/plugins_test.go b/internal/api/handlers/management/plugins_test.go index b88c2c56..feb65e2e 100644 --- a/internal/api/handlers/management/plugins_test.go +++ b/internal/api/handlers/management/plugins_test.go @@ -2,6 +2,7 @@ package management import ( "bytes" + "context" "encoding/json" "html" "net/http" @@ -325,6 +326,80 @@ func TestPatchPluginConfigMergesAndDeletesFields(t *testing.T) { } } +func TestDeletePluginRemovesDiscoveredFileAndConfig(t *testing.T) { + t.Parallel() + gin.SetMode(gin.TestMode) + + pluginsDir := writeManagementPluginFile(t, "sample") + h := &Handler{ + cfg: &config.Config{ + Plugins: config.PluginsConfig{ + Dir: pluginsDir, + Configs: map[string]config.PluginInstanceConfig{ + "sample": pluginConfigFromYAML(t, "enabled: true\nmode: safe\n"), + }, + }, + }, + configFilePath: writeTestConfigFile(t), + } + reloads := 0 + h.SetConfigReloadHook(func(_ context.Context, cfg *config.Config) { + reloads++ + if cfg != h.cfg { + t.Fatalf("reload config = %p, want handler config %p", cfg, h.cfg) + } + }) + + path, errPath := pluginFilePath(pluginsDir, "sample") + if errPath != nil { + t.Fatalf("pluginFilePath() error = %v", errPath) + } + if path == "" { + t.Fatal("plugin path is empty") + } + + rec := httptest.NewRecorder() + c, _ := gin.CreateTestContext(rec) + c.Params = gin.Params{{Key: "id", Value: "sample"}} + c.Request = httptest.NewRequest(http.MethodDelete, "/v0/management/plugins/sample", nil) + + h.DeletePlugin(c) + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String()) + } + if _, ok := h.cfg.Plugins.Configs["sample"]; ok { + t.Fatal("plugin config still exists after delete") + } + if _, errStat := os.Stat(path); !os.IsNotExist(errStat) { + t.Fatalf("plugin file stat error = %v, want not exist", errStat) + } + if reloads != 1 { + t.Fatalf("reloads = %d, want 1", reloads) + } +} + +func TestDeletePluginReturnsNotFoundForUnknownPlugin(t *testing.T) { + t.Parallel() + gin.SetMode(gin.TestMode) + + h := &Handler{ + cfg: &config.Config{}, + configFilePath: writeTestConfigFile(t), + } + + rec := httptest.NewRecorder() + c, _ := gin.CreateTestContext(rec) + c.Params = gin.Params{{Key: "id", Value: "missing"}} + c.Request = httptest.NewRequest(http.MethodDelete, "/v0/management/plugins/missing", nil) + + h.DeletePlugin(c) + + if rec.Code != http.StatusNotFound { + t.Fatalf("status = %d, want %d; body=%s", rec.Code, http.StatusNotFound, rec.Body.String()) + } +} + func TestPluginDisplayFieldsEscapeHTML(t *testing.T) { t.Parallel() diff --git a/internal/api/server.go b/internal/api/server.go index 9b414d7c..1d7bd28b 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -627,6 +627,7 @@ func (s *Server) registerManagementRoutes() { mgmt.GET("/plugins", s.mgmt.ListPlugins) mgmt.GET("/plugin-store", s.mgmt.ListPluginStore) mgmt.POST("/plugin-store/:id/install", s.mgmt.InstallPluginFromStore) + mgmt.DELETE("/plugins/:id", s.mgmt.DeletePlugin) mgmt.PATCH("/plugins/:id/enabled", s.mgmt.PatchPluginEnabled) mgmt.GET("/plugins/:id/config", s.mgmt.GetPluginConfig) mgmt.PUT("/plugins/:id/config", s.mgmt.PutPluginConfig) diff --git a/internal/api/server_test.go b/internal/api/server_test.go index b3b4eaa2..f71deacd 100644 --- a/internal/api/server_test.go +++ b/internal/api/server_test.go @@ -216,6 +216,9 @@ func TestManagementPluginsRouteRegistered(t *testing.T) { server.cfg.Plugins.Configs = map[string]proxyconfig.PluginInstanceConfig{ "sample": {Enabled: &enabled, Priority: 4}, } + if errWrite := os.WriteFile(server.configFilePath, []byte("{}\n"), 0o600); errWrite != nil { + t.Fatalf("failed to write config file: %v", errWrite) + } req := httptest.NewRequest(http.MethodGet, "/v0/management/plugins", nil) req.Header.Set("Authorization", "Bearer test-management-key") @@ -254,6 +257,14 @@ func TestManagementPluginsRouteRegistered(t *testing.T) { if !configPayload.Enabled || configPayload.Priority != 4 { t.Fatalf("plugin config = %#v, want enabled true priority 4", configPayload) } + + req = httptest.NewRequest(http.MethodDelete, "/v0/management/plugins/sample", nil) + req.Header.Set("Authorization", "Bearer test-management-key") + rr = httptest.NewRecorder() + server.engine.ServeHTTP(rr, req) + if rr.Code != http.StatusOK { + t.Fatalf("delete status = %d, want %d body=%s", rr.Code, http.StatusOK, rr.Body.String()) + } } func TestHomeEnabledHidesManagementEndpointsAndControlPanel(t *testing.T) { From b79647d8865f119fb59a45c421000584b8115e14 Mon Sep 17 00:00:00 2001 From: LTbinglingfeng Date: Sat, 13 Jun 2026 20:52:30 +0800 Subject: [PATCH 2/4] Document plugin delete endpoint --- examples/plugin/simple/README.md | 3 ++- examples/plugin/simple/README_CN.md | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/plugin/simple/README.md b/examples/plugin/simple/README.md index bf2f4966..8134353d 100644 --- a/examples/plugin/simple/README.md +++ b/examples/plugin/simple/README.md @@ -178,10 +178,11 @@ The host still performs the real HTTP request, so proxy handling, transport poli ## Management API -The native plugin management endpoints remain: +The native plugin management endpoints are: ```text GET /v0/management/plugins +DELETE /v0/management/plugins/{pluginID} PATCH /v0/management/plugins/{pluginID}/enabled GET /v0/management/plugins/{pluginID}/config PUT /v0/management/plugins/{pluginID}/config diff --git a/examples/plugin/simple/README_CN.md b/examples/plugin/simple/README_CN.md index c4c2cb48..3bee16dc 100644 --- a/examples/plugin/simple/README_CN.md +++ b/examples/plugin/simple/README_CN.md @@ -176,10 +176,11 @@ host.http.do ## Management API -原生插件管理接口保持不变: +原生插件管理接口包括: ```text GET /v0/management/plugins +DELETE /v0/management/plugins/{pluginID} PATCH /v0/management/plugins/{pluginID}/enabled GET /v0/management/plugins/{pluginID}/config PUT /v0/management/plugins/{pluginID}/config From b39ee66250a112b72991ba34200f25befa9fabad Mon Sep 17 00:00:00 2001 From: LTbinglingfeng Date: Sat, 13 Jun 2026 20:56:49 +0800 Subject: [PATCH 3/4] Add plugin store install timeout --- .../api/handlers/management/plugin_store.go | 33 ++++++++- .../handlers/management/plugin_store_test.go | 70 +++++++++++++++++++ 2 files changed, 101 insertions(+), 2 deletions(-) diff --git a/internal/api/handlers/management/plugin_store.go b/internal/api/handlers/management/plugin_store.go index 969e6ce6..a405de5a 100644 --- a/internal/api/handlers/management/plugin_store.go +++ b/internal/api/handlers/management/plugin_store.go @@ -21,6 +21,10 @@ import ( ) const ( + // defaultPluginStoreInstallTimeout bounds plugin store install downloads so + // a stalled registry, release, or asset request does not hold the management + // request forever. + defaultPluginStoreInstallTimeout = 5 * time.Minute // pluginReleaseCacheTTL bounds how long a resolved latest release version is // reused before the GitHub API is queried again. pluginReleaseCacheTTL = 10 * time.Minute @@ -139,14 +143,28 @@ func (h *Handler) InstallPluginFromStore(c *gin.Context) { } func (h *Handler) installPluginFromStore(c *gin.Context, goos, goarch string) { + h.installPluginFromStoreWithTimeout(c, goos, goarch, defaultPluginStoreInstallTimeout) +} + +func (h *Handler) installPluginFromStoreWithTimeout(c *gin.Context, goos, goarch string, timeout time.Duration) { id, okID := pluginIDFromRequest(c) if !okID { return } + installCtx := c.Request.Context() + if timeout > 0 { + var cancel context.CancelFunc + installCtx, cancel = context.WithTimeout(installCtx, timeout) + defer cancel() + } pluginsEnabled, pluginsDir, proxyURL, _, host := h.pluginStoreSnapshot() client := h.newPluginStoreClient(proxyURL) - registry, errRegistry := client.FetchRegistry(c.Request.Context()) + registry, errRegistry := client.FetchRegistry(installCtx) if errRegistry != nil { + if pluginStoreRequestTimedOut(installCtx, errRegistry) { + c.JSON(http.StatusGatewayTimeout, gin.H{"error": "plugin_store_timeout", "message": "plugin store request timed out"}) + return + } c.JSON(http.StatusBadGateway, gin.H{"error": "plugin_store_registry_failed", "message": errRegistry.Error()}) return } @@ -158,7 +176,7 @@ func (h *Handler) installPluginFromStore(c *gin.Context, goos, goarch string) { pluginIsLoaded := func() bool { return pluginLoaded(host, id) } unloadedBeforeWrite := false - result, errInstall := client.Install(c.Request.Context(), plugin, pluginstore.InstallOptions{ + result, errInstall := client.Install(installCtx, plugin, pluginstore.InstallOptions{ PluginsDir: pluginsDir, GOOS: goos, GOARCH: goarch, @@ -196,6 +214,10 @@ func (h *Handler) installPluginFromStore(c *gin.Context, goos, goarch string) { }) return } + if pluginStoreRequestTimedOut(installCtx, errInstall) { + c.JSON(http.StatusGatewayTimeout, gin.H{"error": "plugin_install_timeout", "message": "plugin install timed out"}) + return + } c.JSON(http.StatusBadGateway, gin.H{"error": "plugin_install_failed", "message": errInstall.Error()}) return } @@ -250,6 +272,13 @@ func (h *Handler) installPluginFromStore(c *gin.Context, goos, goarch string) { }) } +func pluginStoreRequestTimedOut(ctx context.Context, err error) bool { + if errors.Is(err, context.DeadlineExceeded) { + return true + } + return ctx != nil && errors.Is(ctx.Err(), context.DeadlineExceeded) +} + // enablePluginConfigLocked sets plugins.configs..enabled to true while preserving // the rest of the plugin's raw configuration. Callers must hold h.mu. func (h *Handler) enablePluginConfigLocked(id string) error { diff --git a/internal/api/handlers/management/plugin_store_test.go b/internal/api/handlers/management/plugin_store_test.go index 4cb59b4e..d3ae7358 100644 --- a/internal/api/handlers/management/plugin_store_test.go +++ b/internal/api/handlers/management/plugin_store_test.go @@ -17,6 +17,7 @@ import ( "strings" "sync" "testing" + "time" "github.com/gin-gonic/gin" "github.com/router-for-me/CLIProxyAPI/v7/internal/config" @@ -317,6 +318,62 @@ func TestInstallPluginFromStoreWritesFileAndEnablesConfig(t *testing.T) { } } +func TestInstallPluginFromStoreTimesOutDownloadingAsset(t *testing.T) { + t.Parallel() + gin.SetMode(gin.TestMode) + + pluginsDir := t.TempDir() + archiveName := "sample-provider_0.1.0_" + runtime.GOOS + "_" + runtime.GOARCH + ".zip" + archiveURL := "https://downloads.example/" + archiveName + h := &Handler{ + cfg: &config.Config{ + Plugins: config.PluginsConfig{ + Enabled: false, + Dir: pluginsDir, + }, + }, + configFilePath: writeTestConfigFile(t), + pluginStoreRegistryURL: "https://registry.example/registry.json", + pluginStoreHTTPClient: blockingPluginStoreHTTPClient{ + blockURL: archiveURL, + responses: fakePluginStoreHTTPClient{ + "https://registry.example/registry.json": registryJSON(t), + "https://api.github.com/repos/author-name/cliproxy-sample-provider-plugin/releases/latest": []byte(`{ + "tag_name": "v0.1.0", + "assets": [ + {"name": "` + archiveName + `", "browser_download_url": "` + archiveURL + `"}, + {"name": "checksums.txt", "browser_download_url": "https://downloads.example/checksums.txt"} + ] + }`), + }, + }, + } + + rec := httptest.NewRecorder() + c, _ := gin.CreateTestContext(rec) + c.Params = gin.Params{{Key: "id", Value: "sample-provider"}} + c.Request = httptest.NewRequest(http.MethodPost, "/v0/management/plugin-store/sample-provider/install", nil) + + h.installPluginFromStoreWithTimeout(c, runtime.GOOS, runtime.GOARCH, 20*time.Millisecond) + + if rec.Code != http.StatusGatewayTimeout { + t.Fatalf("status = %d, want %d; body=%s", rec.Code, http.StatusGatewayTimeout, rec.Body.String()) + } + var body struct { + Error string `json:"error"` + } + if errDecode := json.Unmarshal(rec.Body.Bytes(), &body); errDecode != nil { + t.Fatalf("Unmarshal() error = %v; body=%s", errDecode, rec.Body.String()) + } + if body.Error != "plugin_install_timeout" { + t.Fatalf("error = %q, want plugin_install_timeout", body.Error) + } + targetPath := filepath.Join(pluginsDir, runtime.GOOS, runtime.GOARCH, "sample-provider"+managementPluginExtension(runtime.GOOS)) + if _, errStat := os.Stat(targetPath); !os.IsNotExist(errStat) { + t.Fatalf("target stat error = %v, want not exist", errStat) + } +} + func TestInstallPluginFromStoreOverwritesFilePreservesConfigAndReloads(t *testing.T) { t.Parallel() gin.SetMode(gin.TestMode) @@ -461,6 +518,19 @@ func (c fakePluginStoreHTTPClient) Do(req *http.Request) (*http.Response, error) }, nil } +type blockingPluginStoreHTTPClient struct { + responses fakePluginStoreHTTPClient + blockURL string +} + +func (c blockingPluginStoreHTTPClient) Do(req *http.Request) (*http.Response, error) { + if req.URL.String() == c.blockURL { + <-req.Context().Done() + return nil, req.Context().Err() + } + return c.responses.Do(req) +} + type countingPluginStoreHTTPClient struct { responses fakePluginStoreHTTPClient mu sync.Mutex From 9cdb18e1d694663ff140a285f637f06d595228dd Mon Sep 17 00:00:00 2001 From: LTbinglingfeng Date: Sat, 13 Jun 2026 21:14:06 +0800 Subject: [PATCH 4/4] refactor(plugin): remove timeout handling from plugin installation logic --- .../api/handlers/management/plugin_store.go | 28 -------- .../handlers/management/plugin_store_test.go | 70 ------------------- 2 files changed, 98 deletions(-) diff --git a/internal/api/handlers/management/plugin_store.go b/internal/api/handlers/management/plugin_store.go index a405de5a..d1a18624 100644 --- a/internal/api/handlers/management/plugin_store.go +++ b/internal/api/handlers/management/plugin_store.go @@ -21,10 +21,6 @@ import ( ) const ( - // defaultPluginStoreInstallTimeout bounds plugin store install downloads so - // a stalled registry, release, or asset request does not hold the management - // request forever. - defaultPluginStoreInstallTimeout = 5 * time.Minute // pluginReleaseCacheTTL bounds how long a resolved latest release version is // reused before the GitHub API is queried again. pluginReleaseCacheTTL = 10 * time.Minute @@ -143,28 +139,15 @@ func (h *Handler) InstallPluginFromStore(c *gin.Context) { } func (h *Handler) installPluginFromStore(c *gin.Context, goos, goarch string) { - h.installPluginFromStoreWithTimeout(c, goos, goarch, defaultPluginStoreInstallTimeout) -} - -func (h *Handler) installPluginFromStoreWithTimeout(c *gin.Context, goos, goarch string, timeout time.Duration) { id, okID := pluginIDFromRequest(c) if !okID { return } installCtx := c.Request.Context() - if timeout > 0 { - var cancel context.CancelFunc - installCtx, cancel = context.WithTimeout(installCtx, timeout) - defer cancel() - } pluginsEnabled, pluginsDir, proxyURL, _, host := h.pluginStoreSnapshot() client := h.newPluginStoreClient(proxyURL) registry, errRegistry := client.FetchRegistry(installCtx) if errRegistry != nil { - if pluginStoreRequestTimedOut(installCtx, errRegistry) { - c.JSON(http.StatusGatewayTimeout, gin.H{"error": "plugin_store_timeout", "message": "plugin store request timed out"}) - return - } c.JSON(http.StatusBadGateway, gin.H{"error": "plugin_store_registry_failed", "message": errRegistry.Error()}) return } @@ -214,10 +197,6 @@ func (h *Handler) installPluginFromStoreWithTimeout(c *gin.Context, goos, goarch }) return } - if pluginStoreRequestTimedOut(installCtx, errInstall) { - c.JSON(http.StatusGatewayTimeout, gin.H{"error": "plugin_install_timeout", "message": "plugin install timed out"}) - return - } c.JSON(http.StatusBadGateway, gin.H{"error": "plugin_install_failed", "message": errInstall.Error()}) return } @@ -272,13 +251,6 @@ func (h *Handler) installPluginFromStoreWithTimeout(c *gin.Context, goos, goarch }) } -func pluginStoreRequestTimedOut(ctx context.Context, err error) bool { - if errors.Is(err, context.DeadlineExceeded) { - return true - } - return ctx != nil && errors.Is(ctx.Err(), context.DeadlineExceeded) -} - // enablePluginConfigLocked sets plugins.configs..enabled to true while preserving // the rest of the plugin's raw configuration. Callers must hold h.mu. func (h *Handler) enablePluginConfigLocked(id string) error { diff --git a/internal/api/handlers/management/plugin_store_test.go b/internal/api/handlers/management/plugin_store_test.go index d3ae7358..4cb59b4e 100644 --- a/internal/api/handlers/management/plugin_store_test.go +++ b/internal/api/handlers/management/plugin_store_test.go @@ -17,7 +17,6 @@ import ( "strings" "sync" "testing" - "time" "github.com/gin-gonic/gin" "github.com/router-for-me/CLIProxyAPI/v7/internal/config" @@ -318,62 +317,6 @@ func TestInstallPluginFromStoreWritesFileAndEnablesConfig(t *testing.T) { } } -func TestInstallPluginFromStoreTimesOutDownloadingAsset(t *testing.T) { - t.Parallel() - gin.SetMode(gin.TestMode) - - pluginsDir := t.TempDir() - archiveName := "sample-provider_0.1.0_" + runtime.GOOS + "_" + runtime.GOARCH + ".zip" - archiveURL := "https://downloads.example/" + archiveName - h := &Handler{ - cfg: &config.Config{ - Plugins: config.PluginsConfig{ - Enabled: false, - Dir: pluginsDir, - }, - }, - configFilePath: writeTestConfigFile(t), - pluginStoreRegistryURL: "https://registry.example/registry.json", - pluginStoreHTTPClient: blockingPluginStoreHTTPClient{ - blockURL: archiveURL, - responses: fakePluginStoreHTTPClient{ - "https://registry.example/registry.json": registryJSON(t), - "https://api.github.com/repos/author-name/cliproxy-sample-provider-plugin/releases/latest": []byte(`{ - "tag_name": "v0.1.0", - "assets": [ - {"name": "` + archiveName + `", "browser_download_url": "` + archiveURL + `"}, - {"name": "checksums.txt", "browser_download_url": "https://downloads.example/checksums.txt"} - ] - }`), - }, - }, - } - - rec := httptest.NewRecorder() - c, _ := gin.CreateTestContext(rec) - c.Params = gin.Params{{Key: "id", Value: "sample-provider"}} - c.Request = httptest.NewRequest(http.MethodPost, "/v0/management/plugin-store/sample-provider/install", nil) - - h.installPluginFromStoreWithTimeout(c, runtime.GOOS, runtime.GOARCH, 20*time.Millisecond) - - if rec.Code != http.StatusGatewayTimeout { - t.Fatalf("status = %d, want %d; body=%s", rec.Code, http.StatusGatewayTimeout, rec.Body.String()) - } - var body struct { - Error string `json:"error"` - } - if errDecode := json.Unmarshal(rec.Body.Bytes(), &body); errDecode != nil { - t.Fatalf("Unmarshal() error = %v; body=%s", errDecode, rec.Body.String()) - } - if body.Error != "plugin_install_timeout" { - t.Fatalf("error = %q, want plugin_install_timeout", body.Error) - } - targetPath := filepath.Join(pluginsDir, runtime.GOOS, runtime.GOARCH, "sample-provider"+managementPluginExtension(runtime.GOOS)) - if _, errStat := os.Stat(targetPath); !os.IsNotExist(errStat) { - t.Fatalf("target stat error = %v, want not exist", errStat) - } -} - func TestInstallPluginFromStoreOverwritesFilePreservesConfigAndReloads(t *testing.T) { t.Parallel() gin.SetMode(gin.TestMode) @@ -518,19 +461,6 @@ func (c fakePluginStoreHTTPClient) Do(req *http.Request) (*http.Response, error) }, nil } -type blockingPluginStoreHTTPClient struct { - responses fakePluginStoreHTTPClient - blockURL string -} - -func (c blockingPluginStoreHTTPClient) Do(req *http.Request) (*http.Response, error) { - if req.URL.String() == c.blockURL { - <-req.Context().Done() - return nil, req.Context().Err() - } - return c.responses.Do(req) -} - type countingPluginStoreHTTPClient struct { responses fakePluginStoreHTTPClient mu sync.Mutex