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 TestPluginStoreDirectManifestPinsRequestedVersionArtifacts(t *testing.T) {
plugin := pluginstore.Plugin{
ID: "sample", Name: "Sample", Description: "Sample plugin", Author: "tester", Version: "1.0.0",
Install: pluginstore.InstallPlan{Type: pluginstore.InstallTypeDirect, Artifacts: []pluginstore.Artifact{{
GOOS: "linux", GOARCH: "amd64", URL: "https://downloads.example/sample-1.0.0.zip",
SHA256: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", Size: 100,
}}},
Versions: []pluginstore.Version{{
Version: "0.9.0",
Install: pluginstore.InstallPlan{Type: pluginstore.InstallTypeDirect, Artifacts: []pluginstore.Artifact{{
GOOS: "linux", GOARCH: "amd64", URL: "https://downloads.example/sample-0.9.0.zip",
SHA256: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", Size: 90,
}}},
}},
}
manifest, errManifest := pluginStoreDirectManifest(pluginstore.DefaultSource(), plugin, "0.9.0")
if errManifest != nil {
t.Fatalf("pluginStoreDirectManifest() error = %v", errManifest)
}
if manifest.Version != "0.9.0" || len(manifest.Install.Artifacts) != 1 {
t.Fatalf("manifest = %#v, want pinned historical version artifact", manifest)
}
artifact := manifest.Install.Artifacts[0]
if artifact.URL != "https://downloads.example/sample-0.9.0.zip" || artifact.Size != 90 {
t.Fatalf("artifact = %#v, want historical 0.9.0 artifact", artifact)
}
}
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": "",
"description": "
",
"author": "\"attacker\"",
"version": "0.1.0",
"repository": "https://github.com/author-name/cliproxy-sample-provider-plugin",
"logo": "