Files
2026-07-13 12:35:43 +08:00

1359 lines
48 KiB
Go

package management
import (
"archive/zip"
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"html"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"testing"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/pluginstore"
)
func TestListPluginStoreMergesInstalledStatus(t *testing.T) {
t.Parallel()
pluginsDir := writeManagementPluginFile(t, "sample-provider")
h := &Handler{
cfg: &config.Config{
Plugins: config.PluginsConfig{
Enabled: true,
Dir: pluginsDir,
Configs: map[string]config.PluginInstanceConfig{
"sample-provider": pluginConfigFromYAML(t, "enabled: true\nmode: fast\n"),
},
},
},
configFilePath: writeTestConfigFile(t),
pluginStoreRegistryURL: "https://registry.example/registry.json",
pluginStoreHTTPClient: fakePluginStoreHTTPClient{
"https://registry.example/registry.json": registryJSON(t),
},
}
rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest(http.MethodGet, "/v0/management/plugin-store", nil)
h.ListPluginStore(c)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
}
var body pluginStoreListResponse
if errDecode := json.Unmarshal(rec.Body.Bytes(), &body); errDecode != nil {
t.Fatalf("Unmarshal() error = %v; body=%s", errDecode, rec.Body.String())
}
if !body.PluginsEnabled {
t.Fatal("plugins_enabled = false, want true")
}
if len(body.Plugins) != 1 {
t.Fatalf("plugins len = %d, want 1", len(body.Plugins))
}
entry := body.Plugins[0]
if !entry.Installed || !entry.Configured || !entry.Enabled {
t.Fatalf("store entry status = %#v, want installed configured enabled", entry)
}
if entry.Registered || entry.EffectiveEnabled {
t.Fatalf("runtime status = registered %v effective %v, want false false", entry.Registered, entry.EffectiveEnabled)
}
if entry.InstalledVersion != "" {
t.Fatalf("installed_version = %q, want empty for unregistered plugin", entry.InstalledVersion)
}
if entry.UpdateAvailable {
t.Fatal("update_available = true, want false when installed version is unknown")
}
if entry.Path == "" {
t.Fatal("path is empty")
}
}
func TestListPluginStoreUsesVersionFromInstalledFilename(t *testing.T) {
t.Parallel()
pluginsDir := t.TempDir()
archDir := filepath.Join(pluginsDir, runtime.GOOS, runtime.GOARCH)
if errMkdirAll := os.MkdirAll(archDir, 0o755); errMkdirAll != nil {
t.Fatalf("MkdirAll(%s) error = %v", archDir, errMkdirAll)
}
pluginPath := filepath.Join(archDir, "sample-provider-v0.0.1"+managementPluginExtension(runtime.GOOS))
if errWriteFile := os.WriteFile(pluginPath, []byte("x"), 0o644); errWriteFile != nil {
t.Fatalf("WriteFile(%s) error = %v", pluginPath, errWriteFile)
}
h := &Handler{
cfg: &config.Config{
Plugins: config.PluginsConfig{
Enabled: true,
Dir: pluginsDir,
},
},
configFilePath: writeTestConfigFile(t),
pluginStoreRegistryURL: "https://registry.example/registry.json",
pluginStoreHTTPClient: fakePluginStoreHTTPClient{
"https://registry.example/registry.json": registryJSON(t),
},
}
rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest(http.MethodGet, "/v0/management/plugin-store", nil)
h.ListPluginStore(c)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
}
var body pluginStoreListResponse
if errDecode := json.Unmarshal(rec.Body.Bytes(), &body); errDecode != nil {
t.Fatalf("Unmarshal() error = %v; body=%s", errDecode, rec.Body.String())
}
if len(body.Plugins) != 1 {
t.Fatalf("plugins len = %d, want 1", len(body.Plugins))
}
entry := body.Plugins[0]
if !entry.Installed || entry.InstalledVersion != "0.0.1" {
t.Fatalf("store entry status = %#v, want installed version 0.0.1", entry)
}
if !entry.UpdateAvailable {
t.Fatalf("update_available = false, want true for installed 0.0.1 and registry 0.1.0")
}
}
func TestListPluginStoreUsesConfiguredStoreVersionWhenFilesCoexist(t *testing.T) {
t.Parallel()
pluginsDir := t.TempDir()
archDir := filepath.Join(pluginsDir, runtime.GOOS, runtime.GOARCH)
if errMkdirAll := os.MkdirAll(archDir, 0o755); errMkdirAll != nil {
t.Fatalf("MkdirAll(%s) error = %v", archDir, errMkdirAll)
}
extension := managementPluginExtension(runtime.GOOS)
pinnedPath := filepath.Join(archDir, "sample-provider-v0.1.0"+extension)
newerPath := filepath.Join(archDir, "sample-provider-v0.2.0"+extension)
for _, path := range []string{pinnedPath, newerPath} {
if errWriteFile := os.WriteFile(path, []byte("x"), 0o644); errWriteFile != nil {
t.Fatalf("WriteFile(%s) error = %v", path, errWriteFile)
}
}
h := &Handler{
cfg: &config.Config{
Plugins: config.PluginsConfig{
Enabled: true,
Dir: pluginsDir,
Configs: map[string]config.PluginInstanceConfig{
"sample-provider": pluginConfigFromYAML(t, "enabled: true\nstore:\n version: 0.1.0\n release-tag: v0.1.0\n"),
},
},
},
configFilePath: writeTestConfigFile(t),
pluginStoreRegistryURL: "https://registry.example/registry.json",
pluginStoreHTTPClient: fakePluginStoreHTTPClient{
"https://registry.example/registry.json": registryJSON(t),
},
}
rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest(http.MethodGet, "/v0/management/plugin-store", nil)
h.ListPluginStore(c)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
}
var body pluginStoreListResponse
if errDecode := json.Unmarshal(rec.Body.Bytes(), &body); errDecode != nil {
t.Fatalf("Unmarshal() error = %v; body=%s", errDecode, rec.Body.String())
}
if len(body.Plugins) != 1 {
t.Fatalf("plugins len = %d, want 1", len(body.Plugins))
}
entry := body.Plugins[0]
if !entry.Installed || entry.InstalledVersion != "0.1.0" || entry.Path != pinnedPath {
t.Fatalf("store entry status = %#v, want pinned version/path %s", entry, pinnedPath)
}
}
func TestListPluginStoreEscapesRegistryStrings(t *testing.T) {
t.Parallel()
h := &Handler{
cfg: &config.Config{
Plugins: config.PluginsConfig{
Enabled: true,
Dir: t.TempDir(),
},
},
configFilePath: writeTestConfigFile(t),
pluginStoreRegistryURL: "https://registry.example/registry.json",
pluginStoreHTTPClient: fakePluginStoreHTTPClient{
"https://registry.example/registry.json": []byte(`{
"schema_version": 1,
"plugins": [{
"id": "sample-provider",
"name": "<script>alert(1)</script>",
"description": "<img src=x onerror=alert(1)>",
"author": "\"attacker\"",
"version": "0.1.0",
"repository": "https://github.com/author-name/cliproxy-sample-provider-plugin",
"logo": "<svg onload=alert(1)>",
"homepage": "https://example.com/?q=<x>",
"license": "<b>MIT</b>",
"tags": ["<provider>", "safe & sound"]
}]
}`),
},
}
rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest(http.MethodGet, "/v0/management/plugin-store", nil)
h.ListPluginStore(c)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
}
var body pluginStoreListResponse
if errDecode := json.Unmarshal(rec.Body.Bytes(), &body); errDecode != nil {
t.Fatalf("Unmarshal() error = %v; body=%s", errDecode, rec.Body.String())
}
if len(body.Plugins) != 1 {
t.Fatalf("plugins len = %d, want 1", len(body.Plugins))
}
entry := body.Plugins[0]
if entry.Name != html.EscapeString("<script>alert(1)</script>") ||
entry.Description != html.EscapeString("<img src=x onerror=alert(1)>") ||
entry.Author != html.EscapeString(`"attacker"`) ||
entry.Version != "0.1.0" ||
entry.Repository != "https://github.com/author-name/cliproxy-sample-provider-plugin" ||
entry.Logo != html.EscapeString("<svg onload=alert(1)>") ||
entry.Homepage != html.EscapeString("https://example.com/?q=<x>") ||
entry.License != html.EscapeString("<b>MIT</b>") {
t.Fatalf("store entry = %#v, want escaped strings", entry)
}
if len(entry.Tags) != 2 ||
entry.Tags[0] != html.EscapeString("<provider>") ||
entry.Tags[1] != html.EscapeString("safe & sound") {
t.Fatalf("tags = %#v, want escaped strings", entry.Tags)
}
}
func TestListPluginStoreShowsLatestReleaseVersionAndCaches(t *testing.T) {
t.Parallel()
httpClient := &countingPluginStoreHTTPClient{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.2.0",
"assets": []
}`),
}}
h := &Handler{
cfg: &config.Config{
Plugins: config.PluginsConfig{
Enabled: true,
Dir: t.TempDir(),
},
},
configFilePath: writeTestConfigFile(t),
pluginStoreRegistryURL: "https://registry.example/registry.json",
pluginStoreHTTPClient: httpClient,
}
listOnce := func() pluginStoreListResponse {
rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest(http.MethodGet, "/v0/management/plugin-store", nil)
h.ListPluginStore(c)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
}
var body pluginStoreListResponse
if errDecode := json.Unmarshal(rec.Body.Bytes(), &body); errDecode != nil {
t.Fatalf("Unmarshal() error = %v; body=%s", errDecode, rec.Body.String())
}
return body
}
for call := 0; call < 2; call++ {
body := listOnce()
if len(body.Plugins) != 1 {
t.Fatalf("plugins len = %d, want 1", len(body.Plugins))
}
if body.Plugins[0].Version != "0.2.0" {
t.Fatalf("version = %q, want 0.2.0 from latest release tag", body.Plugins[0].Version)
}
}
releaseCalls := httpClient.count("https://api.github.com/repos/author-name/cliproxy-sample-provider-plugin/releases/latest")
if releaseCalls != 1 {
t.Fatalf("latest release fetched %d times, want 1 (cached)", releaseCalls)
}
}
func TestListPluginStoreFallsBackToRegistryVersion(t *testing.T) {
t.Parallel()
h := &Handler{
cfg: &config.Config{
Plugins: config.PluginsConfig{
Enabled: true,
Dir: t.TempDir(),
},
},
configFilePath: writeTestConfigFile(t),
pluginStoreRegistryURL: "https://registry.example/registry.json",
pluginStoreHTTPClient: fakePluginStoreHTTPClient{
"https://registry.example/registry.json": registryJSON(t),
},
}
rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest(http.MethodGet, "/v0/management/plugin-store", nil)
h.ListPluginStore(c)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
}
var body pluginStoreListResponse
if errDecode := json.Unmarshal(rec.Body.Bytes(), &body); errDecode != nil {
t.Fatalf("Unmarshal() error = %v; body=%s", errDecode, rec.Body.String())
}
if len(body.Plugins) != 1 {
t.Fatalf("plugins len = %d, want 1", len(body.Plugins))
}
if body.Plugins[0].Version != "0.1.0" {
t.Fatalf("version = %q, want registry fallback 0.1.0", body.Plugins[0].Version)
}
}
func TestListPluginStoreIncludesThirdPartySources(t *testing.T) {
t.Parallel()
h := &Handler{
cfg: &config.Config{
Plugins: config.PluginsConfig{
Enabled: true,
Dir: t.TempDir(),
StoreSources: []string{"https://community.example/registry.json"},
},
},
configFilePath: writeTestConfigFile(t),
pluginStoreHTTPClient: fakePluginStoreHTTPClient{
pluginstore.DefaultRegistryURL: registryJSON(t),
"https://community.example/registry.json": []byte(`{
"schema_version": 1,
"plugins": [{
"id": "third-provider",
"name": "Third Provider",
"description": "Adds third-party provider support.",
"author": "community",
"version": "0.3.0",
"repository": "https://github.com/community/cliproxy-third-provider-plugin"
}]
}`),
},
}
rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest(http.MethodGet, "/v0/management/plugin-store", nil)
h.ListPluginStore(c)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
}
var body pluginStoreListResponse
if errDecode := json.Unmarshal(rec.Body.Bytes(), &body); errDecode != nil {
t.Fatalf("Unmarshal() error = %v; body=%s", errDecode, rec.Body.String())
}
if len(body.Sources) != 2 {
t.Fatalf("sources len = %d, want 2: %#v", len(body.Sources), body.Sources)
}
if len(body.Plugins) != 2 {
t.Fatalf("plugins len = %d, want 2: %#v", len(body.Plugins), body.Plugins)
}
byID := map[string]pluginStoreListEntry{}
for _, entry := range body.Plugins {
byID[entry.ID] = entry
}
if byID["sample-provider"].SourceID != pluginstore.DefaultSourceID {
t.Fatalf("official source id = %q, want %q", byID["sample-provider"].SourceID, pluginstore.DefaultSourceID)
}
third := byID["third-provider"]
communitySourceID := pluginstore.SourceID("https://community.example/registry.json")
if third.StoreID != communitySourceID+"/third-provider" || third.SourceID != communitySourceID || third.SourceName != "community.example" || third.SourceURL != "https://community.example/registry.json" {
t.Fatalf("third-party source fields = %#v", third)
}
}
func TestListPluginStoreMatchesInstalledStatusToManifestSource(t *testing.T) {
t.Parallel()
pluginsDir := t.TempDir()
archDir := filepath.Join(pluginsDir, runtime.GOOS, runtime.GOARCH)
if errMkdirAll := os.MkdirAll(archDir, 0o755); errMkdirAll != nil {
t.Fatalf("MkdirAll(%s) error = %v", archDir, errMkdirAll)
}
pluginPath := filepath.Join(archDir, "sample-provider-v0.0.1"+managementPluginExtension(runtime.GOOS))
if errWriteFile := os.WriteFile(pluginPath, []byte("x"), 0o644); errWriteFile != nil {
t.Fatalf("WriteFile(%s) error = %v", pluginPath, errWriteFile)
}
communityURL := "https://community.example/registry.json"
h := &Handler{
cfg: &config.Config{
Plugins: config.PluginsConfig{
Enabled: true,
Dir: pluginsDir,
StoreSources: []string{communityURL},
Configs: map[string]config.PluginInstanceConfig{
"sample-provider": pluginConfigWithStoreSource(t, pluginstore.DefaultSourceID, pluginstore.DefaultRegistryURL),
},
},
},
configFilePath: writeTestConfigFile(t),
pluginStoreHTTPClient: fakePluginStoreHTTPClient{
pluginstore.DefaultRegistryURL: registryJSON(t),
communityURL: thirdPartySampleRegistryJSON(t),
},
}
rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest(http.MethodGet, "/v0/management/plugin-store", nil)
h.ListPluginStore(c)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
}
var body struct {
Plugins []struct {
SourceID string `json:"source_id"`
InstalledSourceID string `json:"installed_source_id"`
InstallSourceStatus string `json:"install_source_status"`
UpdateAvailable bool `json:"update_available"`
} `json:"plugins"`
}
if errDecode := json.Unmarshal(rec.Body.Bytes(), &body); errDecode != nil {
t.Fatalf("Unmarshal() error = %v; body=%s", errDecode, rec.Body.String())
}
if len(body.Plugins) != 2 {
t.Fatalf("plugins len = %d, want 2", len(body.Plugins))
}
entries := make(map[string]struct {
InstalledSourceID string
InstallSourceStatus string
UpdateAvailable bool
}, len(body.Plugins))
for _, entry := range body.Plugins {
entries[entry.SourceID] = struct {
InstalledSourceID string
InstallSourceStatus string
UpdateAvailable bool
}{entry.InstalledSourceID, entry.InstallSourceStatus, entry.UpdateAvailable}
}
official := entries[pluginstore.DefaultSourceID]
if official.InstalledSourceID != pluginstore.DefaultSourceID || official.InstallSourceStatus != "matched" || !official.UpdateAvailable {
t.Fatalf("official entry = %#v, want matched update", official)
}
communitySourceID := pluginstore.SourceID(communityURL)
community := entries[communitySourceID]
if community.InstalledSourceID != pluginstore.DefaultSourceID || community.InstallSourceStatus != "different" || community.UpdateAvailable {
t.Fatalf("community entry = %#v, want different source without update", community)
}
}
func TestInstallPluginFromStoreRejectsImplicitSourceSwitch(t *testing.T) {
t.Parallel()
communityURL := "https://community.example/registry.json"
h := &Handler{
cfg: &config.Config{
Plugins: config.PluginsConfig{
Enabled: true,
Dir: writeManagementPluginFile(t, "sample-provider"),
StoreSources: []string{communityURL},
Configs: map[string]config.PluginInstanceConfig{
"sample-provider": pluginConfigWithStoreSource(t, pluginstore.DefaultSourceID, pluginstore.DefaultRegistryURL),
},
},
},
configFilePath: writeTestConfigFile(t),
pluginStoreHTTPClient: fakePluginStoreHTTPClient{
pluginstore.DefaultRegistryURL: registryJSON(t),
communityURL: thirdPartySampleRegistryJSON(t),
},
}
rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
c.Params = gin.Params{{Key: "id", Value: "sample-provider"}}
communitySourceID := pluginstore.SourceID(communityURL)
c.Request = httptest.NewRequest(http.MethodPost, "/v0/management/plugin-store/sample-provider/install?source="+communitySourceID, nil)
h.InstallPluginFromStore(c)
if rec.Code != http.StatusConflict {
t.Fatalf("status = %d, want %d; body=%s", rec.Code, http.StatusConflict, rec.Body.String())
}
if !strings.Contains(rec.Body.String(), "plugin_store_source_conflict") || !strings.Contains(rec.Body.String(), pluginstore.DefaultSourceID) {
t.Fatalf("body = %s, want source conflict with installed source", rec.Body.String())
}
}
func TestInstallPluginFromStoreRejectsUnknownManagedSource(t *testing.T) {
t.Parallel()
h := &Handler{
cfg: &config.Config{
Plugins: config.PluginsConfig{
Enabled: true,
Dir: writeManagementPluginFile(t, "sample-provider"),
Configs: map[string]config.PluginInstanceConfig{
"sample-provider": pluginConfigWithStoreSource(t, "", ""),
},
},
},
configFilePath: writeTestConfigFile(t),
pluginStoreRegistryURL: "https://registry.example/registry.json",
pluginStoreHTTPClient: fakePluginStoreHTTPClient{
"https://registry.example/registry.json": registryJSON(t),
},
}
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.InstallPluginFromStore(c)
if rec.Code != http.StatusConflict {
t.Fatalf("status = %d, want %d; body=%s", rec.Code, http.StatusConflict, rec.Body.String())
}
if !strings.Contains(rec.Body.String(), "plugin_store_installed_source_unknown") {
t.Fatalf("body = %s, want unknown installed source error", rec.Body.String())
}
}
func TestListPluginStoreIncludesDirectMetadataAndAuth(t *testing.T) {
t.Setenv("PLUGIN_STORE_TOKEN", "secret-token")
h := &Handler{
cfg: &config.Config{
Plugins: config.PluginsConfig{
Enabled: true,
Dir: t.TempDir(),
StoreAuth: []pluginstore.AuthConfig{{
Match: "https://registry.example/",
ApplyTo: []string{pluginstore.RequestKindRegistry},
Type: pluginstore.AuthTypeBearer,
TokenEnv: "PLUGIN_STORE_TOKEN",
}},
},
},
configFilePath: writeTestConfigFile(t),
pluginStoreRegistryURL: "https://registry.example/registry.json",
pluginStoreHTTPClient: fakePluginStoreHTTPClient{
"https://registry.example/registry.json": directRegistryJSON("https://downloads.example/sample-provider.zip", "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"),
},
}
rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest(http.MethodGet, "/v0/management/plugin-store", nil)
h.ListPluginStore(c)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
}
var body pluginStoreListResponse
if errDecode := json.Unmarshal(rec.Body.Bytes(), &body); errDecode != nil {
t.Fatalf("Unmarshal() error = %v; body=%s", errDecode, rec.Body.String())
}
if len(body.Plugins) != 1 {
t.Fatalf("plugins len = %d, want 1", len(body.Plugins))
}
entry := body.Plugins[0]
if entry.InstallType != pluginstore.InstallTypeDirect || !entry.AuthRequired || !entry.AuthConfigured {
t.Fatalf("direct metadata = %#v, want direct auth metadata", entry)
}
if !pluginStorePlatformsContain(entry.Platforms, "linux", "amd64") {
t.Fatalf("platforms = %#v, want linux/amd64", entry.Platforms)
}
}
func TestListPluginStoreReportsVersionArtifactAuth(t *testing.T) {
t.Setenv("PLUGIN_STORE_TOKEN", "secret-token")
h := &Handler{
cfg: &config.Config{
Plugins: config.PluginsConfig{
Enabled: true,
Dir: t.TempDir(),
StoreAuth: []pluginstore.AuthConfig{{
Match: "https://versioned.example/",
ApplyTo: []string{pluginstore.RequestKindArtifact},
Type: pluginstore.AuthTypeBearer,
TokenEnv: "PLUGIN_STORE_TOKEN",
}},
},
},
configFilePath: writeTestConfigFile(t),
pluginStoreRegistryURL: "https://registry.example/registry.json",
pluginStoreHTTPClient: fakePluginStoreHTTPClient{
"https://registry.example/registry.json": directRegistryJSONWithVersionArtifact(
"https://downloads.example/sample-provider.zip",
"https://versioned.example/sample-provider-0.3.0.zip",
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
),
},
}
rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest(http.MethodGet, "/v0/management/plugin-store", nil)
h.ListPluginStore(c)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
}
var body pluginStoreListResponse
if errDecode := json.Unmarshal(rec.Body.Bytes(), &body); errDecode != nil {
t.Fatalf("Unmarshal() error = %v; body=%s", errDecode, rec.Body.String())
}
if len(body.Plugins) != 1 {
t.Fatalf("plugins len = %d, want 1", len(body.Plugins))
}
if !body.Plugins[0].AuthConfigured {
t.Fatalf("auth_configured = false, want true for version artifact auth")
}
}
func TestListPluginStoreReportsGitHubMetadataAuth(t *testing.T) {
t.Setenv("PLUGIN_STORE_TOKEN", "secret-token")
h := &Handler{
cfg: &config.Config{
Plugins: config.PluginsConfig{
Enabled: true,
Dir: t.TempDir(),
StoreAuth: []pluginstore.AuthConfig{{
Match: "https://api.github.com/repos/author-name/cliproxy-sample-provider-plugin/releases/",
ApplyTo: []string{pluginstore.RequestKindMetadata},
Type: pluginstore.AuthTypeBearer,
TokenEnv: "PLUGIN_STORE_TOKEN",
}},
},
},
configFilePath: writeTestConfigFile(t),
pluginStoreRegistryURL: "https://registry.example/registry.json",
pluginStoreHTTPClient: fakePluginStoreHTTPClient{
"https://registry.example/registry.json": registryJSON(t),
},
}
rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest(http.MethodGet, "/v0/management/plugin-store", nil)
h.ListPluginStore(c)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
}
var body pluginStoreListResponse
if errDecode := json.Unmarshal(rec.Body.Bytes(), &body); errDecode != nil {
t.Fatalf("Unmarshal() error = %v; body=%s", errDecode, rec.Body.String())
}
if len(body.Plugins) != 1 {
t.Fatalf("plugins len = %d, want 1", len(body.Plugins))
}
if !body.Plugins[0].AuthConfigured {
t.Fatalf("auth_configured = false, want true for GitHub metadata auth")
}
}
func TestInstallPluginFromStoreWritesFileAndEnablesConfig(t *testing.T) {
t.Parallel()
pluginsDir := t.TempDir()
archiveData := makeManagementPluginStoreZip(t, "sample-provider"+managementPluginExtension(runtime.GOOS), "library-data")
archiveName := "sample-provider_0.1.0_" + runtime.GOOS + "_" + runtime.GOARCH + ".zip"
checksum := sha256.Sum256(archiveData)
h := &Handler{
cfg: &config.Config{
Plugins: config.PluginsConfig{
Enabled: false,
Dir: pluginsDir,
Configs: map[string]config.PluginInstanceConfig{
"sample-provider": pluginConfigFromYAML(t, "enabled: false\nmode: fast\n"),
},
},
},
configFilePath: writeTestConfigFile(t),
pluginStoreRegistryURL: "https://registry.example/registry.json",
pluginStoreHTTPClient: 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": "https://downloads.example/` + archiveName + `"},
{"name": "checksums.txt", "browser_download_url": "https://downloads.example/checksums.txt"}
]
}`),
"https://downloads.example/" + archiveName: archiveData,
"https://downloads.example/checksums.txt": []byte(hex.EncodeToString(checksum[:]) + " " + archiveName + "\n"),
},
}
reloads, reloadDone := captureConfigReload(h)
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.InstallPluginFromStore(c)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
}
cfgSnapshot := waitForAsyncReload(t, reloads)
waitForReloadDone(t, reloadDone)
if cfgSnapshot == h.cfg {
t.Fatalf("reload config = handler config %p, want independent snapshot", h.cfg)
}
var body pluginInstallResponse
if errDecode := json.Unmarshal(rec.Body.Bytes(), &body); errDecode != nil {
t.Fatalf("Unmarshal() error = %v; body=%s", errDecode, rec.Body.String())
}
if body.Status != "installed" || body.ID != "sample-provider" || body.Version != "0.1.0" {
t.Fatalf("install response = %#v", body)
}
if body.PluginsEnabled {
t.Fatal("plugins_enabled = true, want false")
}
if body.RestartRequired {
t.Fatal("restart_required = true, want false")
}
targetPath := filepath.Join(pluginsDir, runtime.GOOS, runtime.GOARCH, "sample-provider-v0.1.0"+managementPluginExtension(runtime.GOOS))
data, errRead := os.ReadFile(targetPath)
if errRead != nil {
t.Fatalf("ReadFile(%s) error = %v", targetPath, errRead)
}
if string(data) != "library-data" {
t.Fatalf("installed file = %q, want library-data", data)
}
item := h.cfg.Plugins.Configs["sample-provider"]
if item.Enabled == nil || !*item.Enabled {
t.Fatalf("plugin enabled = %#v, want true", item.Enabled)
}
snapshotItem := cfgSnapshot.Plugins.Configs["sample-provider"]
if snapshotItem.Enabled == nil || !*snapshotItem.Enabled {
t.Fatalf("snapshot plugin enabled = %#v, want true", snapshotItem.Enabled)
}
if h.cfg.Plugins.Enabled {
t.Fatal("global plugins.enabled changed to true")
}
if cfgSnapshot.Plugins.Enabled {
t.Fatal("snapshot global plugins.enabled changed to true")
}
raw := marshalPluginRaw(t, item)
if !strings.Contains(raw, "mode: fast") {
t.Fatalf("plugin raw config lost custom field:\n%s", raw)
}
manifest := pluginStoreManifestFromConfig(t, item)
if manifest.InstallType() != pluginstore.InstallTypeGitHubRelease || manifest.ReleaseTag != "v0.1.0" || manifest.Version != "0.1.0" {
t.Fatalf("store manifest = %#v, want github-release v0.1.0", manifest)
}
if raw := marshalPluginRaw(t, snapshotItem); !strings.Contains(raw, "mode: fast") {
t.Fatalf("snapshot plugin raw config lost custom field:\n%s", raw)
}
}
func TestInstallPluginFromStoreInstallsDirectArtifact(t *testing.T) {
t.Parallel()
pluginsDir := t.TempDir()
archiveData := makeManagementPluginStoreZip(t, "sample-provider"+managementPluginExtension(runtime.GOOS), "direct-library-data")
checksum := sha256.Sum256(archiveData)
artifactURL := "https://downloads.example/sample-provider.zip"
h := &Handler{
cfg: &config.Config{
Plugins: config.PluginsConfig{
Enabled: false,
Dir: pluginsDir,
},
},
configFilePath: writeTestConfigFile(t),
pluginStoreRegistryURL: "https://registry.example/registry.json",
pluginStoreHTTPClient: fakePluginStoreHTTPClient{
"https://registry.example/registry.json": directRegistryJSON(artifactURL, hex.EncodeToString(checksum[:])),
artifactURL: archiveData,
},
}
reloads, reloadDone := captureConfigReload(h)
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.InstallPluginFromStore(c)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
}
waitForAsyncReload(t, reloads)
waitForReloadDone(t, reloadDone)
var body pluginInstallResponse
if errDecode := json.Unmarshal(rec.Body.Bytes(), &body); errDecode != nil {
t.Fatalf("Unmarshal() error = %v; body=%s", errDecode, rec.Body.String())
}
if body.InstallType != pluginstore.InstallTypeDirect || body.Version != "0.4.0" {
t.Fatalf("install response = %#v, want direct 0.4.0", body)
}
targetPath := filepath.Join(pluginsDir, runtime.GOOS, runtime.GOARCH, "sample-provider-v0.4.0"+managementPluginExtension(runtime.GOOS))
data, errRead := os.ReadFile(targetPath)
if errRead != nil {
t.Fatalf("ReadFile(%s) error = %v", targetPath, errRead)
}
if string(data) != "direct-library-data" {
t.Fatalf("installed file = %q, want direct-library-data", data)
}
manifest := pluginStoreManifestFromConfig(t, h.cfg.Plugins.Configs["sample-provider"])
if manifest.SchemaVersion != pluginstore.SchemaVersionV2 || manifest.InstallType() != pluginstore.InstallTypeDirect || manifest.Version != "0.4.0" {
t.Fatalf("store manifest = %#v, want direct schema v2 0.4.0", manifest)
}
if manifest.SourceURL != "https://registry.example/registry.json" || len(manifest.Install.Artifacts) != 0 {
t.Fatalf("store manifest source/artifacts = %q/%d, want source URL without artifacts", manifest.SourceURL, len(manifest.Install.Artifacts))
}
if raw := marshalPluginRaw(t, h.cfg.Plugins.Configs["sample-provider"]); strings.Contains(raw, "artifacts:") {
t.Fatalf("direct store manifest should not persist artifacts:\n%s", raw)
}
}
func TestInstallPluginFromStoreHonorsDirectQueryVersion(t *testing.T) {
t.Parallel()
pluginsDir := t.TempDir()
archiveData := makeManagementPluginStoreZip(t, "sample-provider"+managementPluginExtension(runtime.GOOS), "direct-history-data")
checksum := sha256.Sum256(archiveData)
topArtifactURL := "https://downloads.example/sample-provider-0.4.0.zip"
versionArtifactURL := "https://downloads.example/sample-provider-0.3.0.zip"
h := &Handler{
cfg: &config.Config{
Plugins: config.PluginsConfig{
Enabled: false,
Dir: pluginsDir,
},
},
configFilePath: writeTestConfigFile(t),
pluginStoreRegistryURL: "https://registry.example/registry.json",
pluginStoreHTTPClient: fakePluginStoreHTTPClient{
"https://registry.example/registry.json": directRegistryJSONWithVersionArtifact(topArtifactURL, versionArtifactURL, hex.EncodeToString(checksum[:])),
versionArtifactURL: archiveData,
},
}
reloads, reloadDone := captureConfigReload(h)
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?version=0.3.0", nil)
h.InstallPluginFromStore(c)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
}
waitForAsyncReload(t, reloads)
waitForReloadDone(t, reloadDone)
var body pluginInstallResponse
if errDecode := json.Unmarshal(rec.Body.Bytes(), &body); errDecode != nil {
t.Fatalf("Unmarshal() error = %v; body=%s", errDecode, rec.Body.String())
}
if body.InstallType != pluginstore.InstallTypeDirect || body.Version != "0.3.0" {
t.Fatalf("install response = %#v, want direct 0.3.0", body)
}
targetPath := filepath.Join(pluginsDir, runtime.GOOS, runtime.GOARCH, "sample-provider-v0.3.0"+managementPluginExtension(runtime.GOOS))
data, errRead := os.ReadFile(targetPath)
if errRead != nil {
t.Fatalf("ReadFile(%s) error = %v", targetPath, errRead)
}
if string(data) != "direct-history-data" {
t.Fatalf("installed file = %q, want direct-history-data", data)
}
manifest := pluginStoreManifestFromConfig(t, h.cfg.Plugins.Configs["sample-provider"])
if manifest.Version != "0.3.0" || manifest.InstallType() != pluginstore.InstallTypeDirect || len(manifest.Install.Artifacts) != 0 {
t.Fatalf("store manifest = %#v, want source-backed direct 0.3.0", manifest)
}
}
func TestInstallPluginFromStoreUsesRequestedThirdPartySource(t *testing.T) {
t.Parallel()
pluginsDir := t.TempDir()
archiveData := makeManagementPluginStoreZip(t, "sample-provider"+managementPluginExtension(runtime.GOOS), "third-party-library-data")
archiveName := "sample-provider_0.3.0_" + runtime.GOOS + "_" + runtime.GOARCH + ".zip"
checksum := sha256.Sum256(archiveData)
h := &Handler{
cfg: &config.Config{
Plugins: config.PluginsConfig{
Enabled: false,
Dir: pluginsDir,
StoreSources: []string{"https://community.example/registry.json"},
},
},
configFilePath: writeTestConfigFile(t),
pluginStoreHTTPClient: fakePluginStoreHTTPClient{
pluginstore.DefaultRegistryURL: registryJSON(t),
"https://community.example/registry.json": thirdPartySampleRegistryJSON(t),
"https://api.github.com/repos/community/cliproxy-sample-provider-plugin/releases/latest": []byte(`{
"tag_name": "v0.3.0",
"assets": [
{"name": "` + archiveName + `", "browser_download_url": "https://downloads.example/` + archiveName + `"},
{"name": "checksums.txt", "browser_download_url": "https://downloads.example/checksums.txt"}
]
}`),
"https://downloads.example/" + archiveName: archiveData,
"https://downloads.example/checksums.txt": []byte(hex.EncodeToString(checksum[:]) + " " + archiveName + "\n"),
},
}
reloads, reloadDone := captureConfigReload(h)
rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
c.Params = gin.Params{{Key: "id", Value: "sample-provider"}}
communitySourceID := pluginstore.SourceID("https://community.example/registry.json")
c.Request = httptest.NewRequest(http.MethodPost, "/v0/management/plugin-store/sample-provider/install?source="+communitySourceID, nil)
h.InstallPluginFromStore(c)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
}
cfgSnapshot := waitForAsyncReload(t, reloads)
waitForReloadDone(t, reloadDone)
if cfgSnapshot == h.cfg {
t.Fatalf("reload config = handler config %p, want independent snapshot", h.cfg)
}
var body pluginInstallResponse
if errDecode := json.Unmarshal(rec.Body.Bytes(), &body); errDecode != nil {
t.Fatalf("Unmarshal() error = %v; body=%s", errDecode, rec.Body.String())
}
if body.SourceID != communitySourceID || body.Version != "0.3.0" {
t.Fatalf("install response = %#v, want community source version 0.3.0", body)
}
targetPath := filepath.Join(pluginsDir, runtime.GOOS, runtime.GOARCH, "sample-provider-v0.3.0"+managementPluginExtension(runtime.GOOS))
data, errRead := os.ReadFile(targetPath)
if errRead != nil {
t.Fatalf("ReadFile(%s) error = %v", targetPath, errRead)
}
if string(data) != "third-party-library-data" {
t.Fatalf("installed file = %q, want third-party-library-data", data)
}
snapshotItem := cfgSnapshot.Plugins.Configs["sample-provider"]
if snapshotItem.Enabled == nil || !*snapshotItem.Enabled {
t.Fatalf("snapshot plugin enabled = %#v, want true", snapshotItem.Enabled)
}
}
func TestInstallPluginFromStoreRequiresSourceForDuplicateIDs(t *testing.T) {
t.Parallel()
h := &Handler{
cfg: &config.Config{
Plugins: config.PluginsConfig{
Enabled: false,
Dir: t.TempDir(),
StoreSources: []string{"https://community.example/registry.json"},
},
},
configFilePath: writeTestConfigFile(t),
pluginStoreHTTPClient: fakePluginStoreHTTPClient{
pluginstore.DefaultRegistryURL: registryJSON(t),
"https://community.example/registry.json": thirdPartySampleRegistryJSON(t),
},
}
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.InstallPluginFromStore(c)
if rec.Code != http.StatusConflict {
t.Fatalf("status = %d, want %d; body=%s", rec.Code, http.StatusConflict, rec.Body.String())
}
if !strings.Contains(rec.Body.String(), "plugin_store_source_required") {
t.Fatalf("body = %s, want source required error", rec.Body.String())
}
}
func TestInstallPluginFromStoreOverwritesFilePreservesConfigAndReloads(t *testing.T) {
t.Parallel()
pluginsDir := t.TempDir()
existingPath := filepath.Join(pluginsDir, runtime.GOOS, runtime.GOARCH, "sample-provider-v0.1.0"+managementPluginExtension(runtime.GOOS))
if errMkdir := os.MkdirAll(filepath.Dir(existingPath), 0o755); errMkdir != nil {
t.Fatalf("MkdirAll(%s) error = %v", filepath.Dir(existingPath), errMkdir)
}
if errWrite := os.WriteFile(existingPath, []byte("old-library-data"), 0o644); errWrite != nil {
t.Fatalf("WriteFile(%s) error = %v", existingPath, errWrite)
}
archiveData := makeManagementPluginStoreZip(t, "sample-provider"+managementPluginExtension(runtime.GOOS), "new-library-data")
archiveName := "sample-provider_0.1.0_" + runtime.GOOS + "_" + runtime.GOARCH + ".zip"
checksum := sha256.Sum256(archiveData)
h := &Handler{
cfg: &config.Config{
Plugins: config.PluginsConfig{
Enabled: true,
Dir: pluginsDir,
Configs: map[string]config.PluginInstanceConfig{
"sample-provider": pluginConfigFromYAML(t, "enabled: false\npriority: 5\nmode: fast\nextra: keep\n"),
},
},
},
configFilePath: writeTestConfigFile(t),
pluginStoreRegistryURL: "https://registry.example/registry.json",
pluginStoreHTTPClient: 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": "https://downloads.example/` + archiveName + `"},
{"name": "checksums.txt", "browser_download_url": "https://downloads.example/checksums.txt"}
]
}`),
"https://downloads.example/" + archiveName: archiveData,
"https://downloads.example/checksums.txt": []byte(hex.EncodeToString(checksum[:]) + " " + archiveName + "\n"),
},
}
reloads, reloadDone := captureConfigReload(h)
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.InstallPluginFromStore(c)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
}
cfgSnapshot := waitForAsyncReload(t, reloads)
waitForReloadDone(t, reloadDone)
if cfgSnapshot == h.cfg {
t.Fatalf("reload config = handler config %p, want independent snapshot", h.cfg)
}
data, errRead := os.ReadFile(existingPath)
if errRead != nil {
t.Fatalf("ReadFile(%s) error = %v", existingPath, errRead)
}
if string(data) != "new-library-data" {
t.Fatalf("installed file = %q, want new-library-data", data)
}
item := h.cfg.Plugins.Configs["sample-provider"]
if item.Enabled == nil || !*item.Enabled {
t.Fatalf("plugin enabled = %#v, want true", item.Enabled)
}
snapshotItem := cfgSnapshot.Plugins.Configs["sample-provider"]
if snapshotItem.Enabled == nil || !*snapshotItem.Enabled {
t.Fatalf("snapshot plugin enabled = %#v, want true", snapshotItem.Enabled)
}
if item.Priority != 5 {
t.Fatalf("plugin priority = %d, want 5", item.Priority)
}
if snapshotItem.Priority != 5 {
t.Fatalf("snapshot plugin priority = %d, want 5", snapshotItem.Priority)
}
raw := marshalPluginRaw(t, item)
if !strings.Contains(raw, "mode: fast") || !strings.Contains(raw, "extra: keep") {
t.Fatalf("plugin raw config lost custom fields:\n%s", raw)
}
if raw := marshalPluginRaw(t, snapshotItem); !strings.Contains(raw, "mode: fast") || !strings.Contains(raw, "extra: keep") {
t.Fatalf("snapshot plugin raw config lost custom fields:\n%s", raw)
}
}
func TestEnablePluginConfigLockedPreservesExistingFields(t *testing.T) {
t.Parallel()
h := &Handler{
cfg: &config.Config{
Plugins: config.PluginsConfig{
Enabled: false,
Configs: map[string]config.PluginInstanceConfig{
"sample-provider": pluginConfigFromYAML(t, "enabled: false\npriority: 5\nmode: fast\n"),
},
},
},
}
if errEnable := h.enablePluginConfigLocked("sample-provider", testStoreManifest()); errEnable != nil {
t.Fatalf("enablePluginConfigLocked() error = %v", errEnable)
}
if h.cfg.Plugins.Enabled {
t.Fatal("global Plugins.Enabled changed to true")
}
item := h.cfg.Plugins.Configs["sample-provider"]
if item.Enabled == nil || !*item.Enabled {
t.Fatalf("plugin enabled = %#v, want true", item.Enabled)
}
if item.Priority != 5 {
t.Fatalf("plugin priority = %d, want 5", item.Priority)
}
raw := marshalPluginRaw(t, item)
if !strings.Contains(raw, "mode: fast") || !strings.Contains(raw, "store:") {
t.Fatalf("plugin raw config lost custom field:\n%s", raw)
}
}
func TestEnablePluginConfigLockedCreatesMissingConfig(t *testing.T) {
t.Parallel()
h := &Handler{cfg: &config.Config{}}
if errEnable := h.enablePluginConfigLocked("sample-provider", testStoreManifest()); errEnable != nil {
t.Fatalf("enablePluginConfigLocked() error = %v", errEnable)
}
item := h.cfg.Plugins.Configs["sample-provider"]
if item.Enabled == nil || !*item.Enabled {
t.Fatalf("plugin enabled = %#v, want true", item.Enabled)
}
manifest := pluginStoreManifestFromConfig(t, item)
if manifest.ID != "sample-provider" || manifest.ReleaseTag != "v0.1.0" {
t.Fatalf("store manifest = %#v, want sample-provider v0.1.0", manifest)
}
}
type fakePluginStoreHTTPClient map[string][]byte
func (c fakePluginStoreHTTPClient) Do(req *http.Request) (*http.Response, error) {
body, ok := c[req.URL.String()]
if !ok {
return &http.Response{
StatusCode: http.StatusNotFound,
Body: io.NopCloser(strings.NewReader("not found")),
Header: make(http.Header),
Request: req,
}, nil
}
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewReader(body)),
Header: make(http.Header),
Request: req,
}, nil
}
type countingPluginStoreHTTPClient struct {
responses fakePluginStoreHTTPClient
mu sync.Mutex
counts map[string]int
}
func (c *countingPluginStoreHTTPClient) Do(req *http.Request) (*http.Response, error) {
c.mu.Lock()
if c.counts == nil {
c.counts = make(map[string]int)
}
c.counts[req.URL.String()]++
c.mu.Unlock()
return c.responses.Do(req)
}
func (c *countingPluginStoreHTTPClient) count(url string) int {
c.mu.Lock()
defer c.mu.Unlock()
return c.counts[url]
}
func registryJSON(t *testing.T) []byte {
t.Helper()
return []byte(`{
"schema_version": 1,
"plugins": [{
"id": "sample-provider",
"name": "Sample Provider",
"description": "Adds sample provider support.",
"author": "author-name",
"version": "0.1.0",
"repository": "https://github.com/author-name/cliproxy-sample-provider-plugin",
"tags": ["provider"]
}]
}`)
}
func thirdPartySampleRegistryJSON(t *testing.T) []byte {
t.Helper()
return []byte(`{
"schema_version": 1,
"plugins": [{
"id": "sample-provider",
"name": "Sample Provider Community Build",
"description": "Adds sample provider support from a third-party source.",
"author": "community",
"version": "0.3.0",
"repository": "https://github.com/community/cliproxy-sample-provider-plugin"
}]
}`)
}
func directRegistryJSON(artifactURL string, checksum string) []byte {
return []byte(`{
"schema_version": 2,
"plugins": [{
"id": "sample-provider",
"name": "Sample Provider",
"description": "Adds sample provider support.",
"author": "author-name",
"version": "0.4.0",
"auth_required": true,
"install": {
"type": "direct",
"artifacts": [{
"goos": "` + runtime.GOOS + `",
"goarch": "` + runtime.GOARCH + `",
"url": "` + artifactURL + `",
"sha256": "` + checksum + `"
}, {
"goos": "linux",
"goarch": "amd64",
"url": "` + artifactURL + `",
"sha256": "` + checksum + `"
}]
}
}]
}`)
}
func directRegistryJSONWithVersionArtifact(artifactURL string, versionArtifactURL string, checksum string) []byte {
return []byte(`{
"schema_version": 2,
"plugins": [{
"id": "sample-provider",
"name": "Sample Provider",
"description": "Adds sample provider support.",
"author": "author-name",
"version": "0.4.0",
"auth_required": true,
"install": {
"type": "direct",
"artifacts": [{
"goos": "` + runtime.GOOS + `",
"goarch": "` + runtime.GOARCH + `",
"url": "` + artifactURL + `",
"sha256": "` + checksum + `"
}]
},
"versions": [{
"version": "0.3.0",
"install": {
"type": "direct",
"artifacts": [{
"goos": "` + runtime.GOOS + `",
"goarch": "` + runtime.GOARCH + `",
"url": "` + versionArtifactURL + `",
"sha256": "` + checksum + `"
}]
}
}]
}]
}`)
}
func testStoreManifest() pluginstore.Manifest {
return pluginstore.Manifest{
ID: "sample-provider",
Name: "Sample Provider",
Description: "Adds sample provider support.",
Author: "author-name",
Version: "0.1.0",
ReleaseTag: "v0.1.0",
Repository: "https://github.com/author-name/cliproxy-sample-provider-plugin",
Install: pluginstore.InstallPlan{Type: pluginstore.InstallTypeGitHubRelease},
}
}
func pluginConfigWithStoreSource(t *testing.T, sourceID string, sourceURL string) config.PluginInstanceConfig {
t.Helper()
sourceFields := ""
if sourceID != "" {
sourceFields += " source-id: " + sourceID + "\n"
}
if sourceURL != "" {
sourceFields += " source-url: " + sourceURL + "\n"
}
return pluginConfigFromYAML(t, "enabled: true\nstore:\n schema-version: 1\n id: sample-provider\n version: 0.0.1\n release-tag: v0.0.1\n repository: https://github.com/author-name/cliproxy-sample-provider-plugin\n"+sourceFields+" install:\n type: github-release\n")
}
func pluginStoreManifestFromConfig(t *testing.T, item config.PluginInstanceConfig) pluginstore.Manifest {
t.Helper()
node := pluginConfigNode(item)
for index := 0; index+1 < len(node.Content); index += 2 {
key := node.Content[index]
value := node.Content[index+1]
if key == nil || key.Value != "store" {
continue
}
var manifest pluginstore.Manifest
if errDecode := value.Decode(&manifest); errDecode != nil {
t.Fatalf("decode store manifest: %v", errDecode)
}
if errValidate := manifest.Validate(); errValidate != nil {
t.Fatalf("store manifest Validate() error = %v; manifest=%#v", errValidate, manifest)
}
return manifest
}
t.Fatalf("plugin config missing store manifest:\n%s", marshalPluginRaw(t, item))
return pluginstore.Manifest{}
}
func pluginStorePlatformsContain(platforms []pluginStorePlatform, goos string, goarch string) bool {
for _, platform := range platforms {
if platform.GOOS == goos && platform.GOARCH == goarch {
return true
}
}
return false
}
func makeManagementPluginStoreZip(t *testing.T, name string, content string) []byte {
t.Helper()
var buffer bytes.Buffer
writer := zip.NewWriter(&buffer)
file, errCreate := writer.Create(name)
if errCreate != nil {
t.Fatalf("Create(%s) error = %v", name, errCreate)
}
if _, errWrite := file.Write([]byte(content)); errWrite != nil {
t.Fatalf("Write(%s) error = %v", name, errWrite)
}
if errClose := writer.Close(); errClose != nil {
t.Fatalf("Close() error = %v", errClose)
}
return buffer.Bytes()
}