package homeplugins import ( "context" "errors" "io" "net/http" "net/http/httptest" "strings" "sync/atomic" "testing" "time" "github.com/router-for-me/CLIProxyAPI/v8/internal/config" internalpluginstore "github.com/router-for-me/CLIProxyAPI/v8/internal/pluginstore" sdkpluginstore "github.com/router-for-me/CLIProxyAPI/v8/sdk/pluginstore" ) type networkScopeDoer func(*http.Request) (*http.Response, error) func (f networkScopeDoer) Do(req *http.Request) (*http.Response, error) { return f(req) } func TestPluginStoreClientsShareProxyCooldown(t *testing.T) { var proxyCalls atomic.Int32 proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { proxyCalls.Add(1) w.WriteHeader(http.StatusBadGateway) })) defer proxy.Close() const tokenEnv = "HOME_PLUGIN_NETWORK_SCOPE_TEST_TOKEN" t.Setenv(tokenEnv, t.Name()) auth := []sdkpluginstore.ResolvedAuthConfig{{ Match: "https://api.github.com/", Type: sdkpluginstore.AuthTypeGitHubToken, Token: sdkpluginstore.Secret(t.Name()), }} expiresAt := time.Now().Add(time.Hour) plugin := sdkpluginstore.Plugin{Repository: "https://github.com/test/home-network-scope"} // Seed the same process-wide cooldown used by management and Home clients. seed := sdkpluginstore.NewClientWithResolvedAuthExpiry(networkScopeDoer(func(*http.Request) (*http.Response, error) { return &http.Response{StatusCode: http.StatusTooManyRequests, Header: http.Header{"Retry-After": {"3600"}}, Body: io.NopCloser(strings.NewReader("limited"))}, nil }), "", auth, expiresAt).WithNetworkScope(proxy.URL) _, errRelease := seed.FetchLatestRelease(context.Background(), plugin) var rateLimit *internalpluginstore.RateLimitError if !errors.As(errRelease, &rateLimit) { t.Fatalf("seed cooldown: %v", errRelease) } cfg := &config.Config{} cfg.ProxyURL = " " + proxy.URL + " " cfg.Plugins.StoreAuth = []sdkpluginstore.AuthConfig{{ Match: "https://api.github.com/", Type: sdkpluginstore.AuthTypeGitHubToken, TokenEnv: tokenEnv, }} for name, client := range map[string]sdkpluginstore.Client{ "configured auth": newPluginStoreClient(cfg), "resolved auth": newResolvedPluginStoreClient(cfg, auth, expiresAt), } { t.Run(name, func(t *testing.T) { _, errRelease := client.FetchLatestRelease(context.Background(), plugin) var rateLimit *internalpluginstore.RateLimitError if !errors.As(errRelease, &rateLimit) { t.Fatalf("Home client did not inherit the proxy cooldown: %v", errRelease) } }) } if proxyCalls.Load() != 0 { t.Fatalf("cooldown made %d proxy requests, want 0", proxyCalls.Load()) } }