package pluginstore import ( "strings" "testing") func TestParseRegistryValidatesRegistry(t *testing.T) { t.Parallel() registry, errParse := ParseRegistry([]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", "logo": "https://example.com/logo.png", "homepage": "https://github.com/author-name/cliproxy-sample-provider-plugin", "license": "MIT", "tags": ["provider"] }] }`)) if errParse != nil { t.Fatalf("ParseRegistry() error = %v", errParse) } plugin, ok := registry.PluginByID("sample-provider") if !ok { t.Fatal("PluginByID(sample-provider) missing") } if plugin.Version != "0.1.0" { t.Fatalf("plugin version = %q, want 0.1.0", plugin.Version) }} func TestParseRegistryNormalizesPluginFields(t *testing.T) { t.Parallel() registry, errParse := ParseRegistry([]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 ", "logo": " https://example.com/logo.png ", "homepage": " https://github.com/author-name/cliproxy-sample-provider-plugin ", "license": " MIT ", "tags": [" provider "] }] }`)) if errParse != nil { t.Fatalf("ParseRegistry() error = %v", errParse) } plugin, ok := registry.PluginByID("sample-provider") if !ok { t.Fatal("PluginByID(sample-provider) missing") } if plugin.ID != "sample-provider" || plugin.Version != "0.1.0" || plugin.Repository != "https://github.com/author-name/cliproxy-sample-provider-plugin" { t.Fatalf("plugin not normalized: %#v", plugin) } if plugin.Name != "Sample Provider" || plugin.Tags[0] != "provider" { t.Fatalf("plugin display fields not normalized: %#v", plugin) }} func TestValidateRegistryAllowsMissingVersion(t *testing.T) { t.Parallel() registry := Registry{SchemaVersion: 1, Plugins: []Plugin{{ ID: "sample-provider", Name: "Sample Provider", Description: "Adds sample provider support.", Author: "author-name", Repository: "https://github.com/author-name/cliproxy-sample-provider-plugin", }}} if errValidate := ValidateRegistry(registry); errValidate != nil { t.Fatalf("ValidateRegistry() error = %v, want nil for missing version", errValidate) }} func TestParseRegistrySupportsDirectInstall(t *testing.T) { t.Parallel() registry, errParse := ParseRegistry([]byte(`{ "schema_version": 2, "plugins": [{ "id": "sample-provider", "name": "Sample Provider", "description": "Adds sample provider support.", "author": "author-name", "version": "0.2.0", "auth_required": true, "install": { "type": "direct", "artifacts": [{ "goos": "windows", "goarch": "x64", "url": "https://downloads.example/sample-provider.zip", "sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" }] }, "versions": [{ "version": "0.1.0", "install": { "type": "direct", "artifacts": [{ "goos": "linux", "goarch": "aarch64", "url": "https://downloads.example/sample-provider-0.1.0.zip", "sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" }] } }] }] }`)) if errParse != nil { t.Fatalf("ParseRegistry() error = %v", errParse) } plugin, ok := registry.PluginByID("sample-provider") if !ok { t.Fatal("PluginByID(sample-provider) missing") } if PluginInstallType(plugin) != InstallTypeDirect { t.Fatalf("install type = %q, want direct", PluginInstallType(plugin)) } if !plugin.AuthRequired { t.Fatal("AuthRequired = false, want true") } if len(plugin.Versions) != 1 || plugin.Versions[0].Version != "0.1.0" { t.Fatalf("versions = %#v, want normalized 0.1.0 entry", plugin.Versions) } platforms := PluginPlatforms(plugin) if len(platforms) != 2 || platforms[0].GOOS != "windows" || platforms[0].GOARCH != "amd64" || platforms[1].GOOS != "linux" || platforms[1].GOARCH != "arm64" { t.Fatalf("platforms = %#v, want normalized windows/amd64 and linux/arm64", platforms) } artifacts := PluginArtifacts(plugin) if len(artifacts) != 2 || artifacts[0].GOOS != "windows" || artifacts[0].GOARCH != "amd64" || artifacts[1].GOOS != "linux" || artifacts[1].GOARCH != "arm64" { t.Fatalf("artifacts = %#v, want normalized top-level and version artifacts", artifacts) }} func TestValidateRegistryRejectsInvalidDirectInstall(t *testing.T) { t.Parallel() registry := Registry{SchemaVersion: SchemaVersionV2, Plugins: []Plugin{{ ID: "sample-provider", Name: "Sample Provider", Description: "Adds sample provider support.", Author: "author-name", Version: "0.2.0", Install: InstallPlan{ Type: InstallTypeDirect, Artifacts: []Artifact{{ GOOS: "linux", GOARCH: "amd64", URL: "https://downloads.example/sample.zip?token=secret", SHA256: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", }}, }, }}} errValidate := ValidateRegistry(registry) if errValidate == nil { t.Fatal("ValidateRegistry() error = nil") } if !strings.Contains(errValidate.Error(), "sensitive query") { t.Fatalf("ValidateRegistry() error = %v, want sensitive query", errValidate) }} func TestValidateRegistryRejectsDirectInstallInSchemaV1(t *testing.T) { t.Parallel() registry := Registry{SchemaVersion: SchemaVersion, Plugins: []Plugin{{ ID: "sample-provider", Name: "Sample Provider", Description: "Adds sample provider support.", Author: "author-name", Version: "0.2.0", Install: InstallPlan{ Type: InstallTypeDirect, Artifacts: []Artifact{{ GOOS: "linux", GOARCH: "amd64", URL: "https://downloads.example/sample.zip", SHA256: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", }}, }, }}} errValidate := ValidateRegistry(registry) if errValidate == nil { t.Fatal("ValidateRegistry() error = nil") } if !strings.Contains(errValidate.Error(), "schema_version 2") { t.Fatalf("ValidateRegistry() error = %v, want schema_version 2", errValidate) }} func TestValidateRegistryRejectsInvalidEntries(t *testing.T) { t.Parallel() valid := Plugin{ 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", } tests := []struct { name string mutate func(*Registry) wantErr string }{ { name: "schema version", mutate: func(registry *Registry) { registry.SchemaVersion = 3 }, wantErr: "unsupported schema_version", }, { name: "missing required field", mutate: func(registry *Registry) { registry.Plugins[0].Name = "" }, wantErr: "missing required field name", }, { name: "duplicate id", mutate: func(registry *Registry) { registry.Plugins = append(registry.Plugins, valid) }, wantErr: "duplicate plugin id", }, { name: "invalid id", mutate: func(registry *Registry) { registry.Plugins[0].ID = "../sample-provider" }, wantErr: "invalid plugin id", }, { name: "v-prefixed version", mutate: func(registry *Registry) { registry.Plugins[0].Version = "v0.1.0" }, wantErr: "invalid plugin version", }, { name: "invalid repository", mutate: func(registry *Registry) { registry.Plugins[0].Repository = "https://example.com/author/repo" }, wantErr: "repository must be", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() registry := Registry{SchemaVersion: 1, Plugins: []Plugin{valid}} tt.mutate(®istry) errValidate := ValidateRegistry(registry) if errValidate == nil { t.Fatal("ValidateRegistry() error = nil") } if !strings.Contains(errValidate.Error(), tt.wantErr) { t.Fatalf("ValidateRegistry() error = %v, want substring %q", errValidate, tt.wantErr) } }) }} func TestNormalizeSourcesAppendsURLsToDefaultSource(t *testing.T) { t.Parallel() sources, errNormalize := NormalizeSources([]string{" https://community.example/registry.json "}) if errNormalize != nil { t.Fatalf("NormalizeSources() error = %v", errNormalize) } if len(sources) != 2 { t.Fatalf("sources len = %d, want 2", len(sources)) } if sources[0].ID != DefaultSourceID || sources[0].URL != DefaultRegistryURL { t.Fatalf("default source = %#v", sources[0]) } if sources[1].ID != SourceID("https://community.example/registry.json") || sources[1].Name != "community.example" || sources[1].URL != "https://community.example/registry.json" { t.Fatalf("third-party source = %#v", sources[1]) }} func TestNormalizeSourcesSkipsDuplicates(t *testing.T) { t.Parallel() sources, errNormalize := NormalizeSources([]string{ DefaultRegistryURL, "https://community.example/registry.json", "https://community.example/registry.json", }) if errNormalize != nil { t.Fatalf("NormalizeSources() error = %v", errNormalize) } if len(sources) != 2 { t.Fatalf("sources len = %d, want 2: %#v", len(sources), sources) }} func TestGitHubRepositoryPartsRejectsNonRepositoryURLs(t *testing.T) { t.Parallel() tests := []string{ "http://github.com/owner/repo", "https://github.com/owner", "https://github.com/owner/repo/issues", "https://github.com/owner/repo.git", "https://github.com/owner/repo?tab=readme", } for _, repository := range tests { t.Run(repository, func(t *testing.T) { t.Parallel() if _, _, errParse := GitHubRepositoryParts(repository); errParse == nil { t.Fatalf("GitHubRepositoryParts(%q) error = nil", repository) } }) }}
package pluginstore
import (
"strings"
"testing"
)
func TestParseRegistryValidatesRegistry(t *testing.T) {
t.Parallel()
registry, errParse := ParseRegistry([]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",
"logo": "https://example.com/logo.png",
"homepage": "https://github.com/author-name/cliproxy-sample-provider-plugin",
"license": "MIT",
"tags": ["provider"]
}]
}`))
if errParse != nil {
t.Fatalf("ParseRegistry() error = %v", errParse)
}
plugin, ok := registry.PluginByID("sample-provider")
if !ok {
t.Fatal("PluginByID(sample-provider) missing")
}
if plugin.Version != "0.1.0" {
t.Fatalf("plugin version = %q, want 0.1.0", plugin.Version)
}
}
func TestParseRegistryNormalizesPluginFields(t *testing.T) {
t.Parallel()
registry, errParse := ParseRegistry([]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 ",
"logo": " https://example.com/logo.png ",
"homepage": " https://github.com/author-name/cliproxy-sample-provider-plugin ",
"license": " MIT ",
"tags": [" provider "]
}]
}`))
if errParse != nil {
t.Fatalf("ParseRegistry() error = %v", errParse)
}
plugin, ok := registry.PluginByID("sample-provider")
if !ok {
t.Fatal("PluginByID(sample-provider) missing")
}
if plugin.ID != "sample-provider" || plugin.Version != "0.1.0" || plugin.Repository != "https://github.com/author-name/cliproxy-sample-provider-plugin" {
t.Fatalf("plugin not normalized: %#v", plugin)
}
if plugin.Name != "Sample Provider" || plugin.Tags[0] != "provider" {
t.Fatalf("plugin display fields not normalized: %#v", plugin)
}
}
func TestValidateRegistryAllowsMissingVersion(t *testing.T) {
t.Parallel()
registry := Registry{SchemaVersion: 1, Plugins: []Plugin{{
ID: "sample-provider",
Name: "Sample Provider",
Description: "Adds sample provider support.",
Author: "author-name",
Repository: "https://github.com/author-name/cliproxy-sample-provider-plugin",
}}}
if errValidate := ValidateRegistry(registry); errValidate != nil {
t.Fatalf("ValidateRegistry() error = %v, want nil for missing version", errValidate)
}
}
func TestParseRegistrySupportsDirectInstall(t *testing.T) {
t.Parallel()
registry, errParse := ParseRegistry([]byte(`{
"schema_version": 2,
"plugins": [{
"id": "sample-provider",
"name": "Sample Provider",
"description": "Adds sample provider support.",
"author": "author-name",
"version": "0.2.0",
"auth_required": true,
"install": {
"type": "direct",
"artifacts": [{
"goos": "windows",
"goarch": "x64",
"url": "https://downloads.example/sample-provider.zip",
"sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
}]
},
"versions": [{
"version": "0.1.0",
"install": {
"type": "direct",
"artifacts": [{
"goos": "linux",
"goarch": "aarch64",
"url": "https://downloads.example/sample-provider-0.1.0.zip",
"sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
}]
}
}]
}]
}`))
if errParse != nil {
t.Fatalf("ParseRegistry() error = %v", errParse)
}
plugin, ok := registry.PluginByID("sample-provider")
if !ok {
t.Fatal("PluginByID(sample-provider) missing")
}
if PluginInstallType(plugin) != InstallTypeDirect {
t.Fatalf("install type = %q, want direct", PluginInstallType(plugin))
}
if !plugin.AuthRequired {
t.Fatal("AuthRequired = false, want true")
}
if len(plugin.Versions) != 1 || plugin.Versions[0].Version != "0.1.0" {
t.Fatalf("versions = %#v, want normalized 0.1.0 entry", plugin.Versions)
}
platforms := PluginPlatforms(plugin)
if len(platforms) != 2 ||
platforms[0].GOOS != "windows" || platforms[0].GOARCH != "amd64" ||
platforms[1].GOOS != "linux" || platforms[1].GOARCH != "arm64" {
t.Fatalf("platforms = %#v, want normalized windows/amd64 and linux/arm64", platforms)
}
artifacts := PluginArtifacts(plugin)
if len(artifacts) != 2 ||
artifacts[0].GOOS != "windows" || artifacts[0].GOARCH != "amd64" ||
artifacts[1].GOOS != "linux" || artifacts[1].GOARCH != "arm64" {
t.Fatalf("artifacts = %#v, want normalized top-level and version artifacts", artifacts)
}
}
func TestValidateRegistryRejectsInvalidDirectInstall(t *testing.T) {
t.Parallel()
registry := Registry{SchemaVersion: SchemaVersionV2, Plugins: []Plugin{{
ID: "sample-provider",
Name: "Sample Provider",
Description: "Adds sample provider support.",
Author: "author-name",
Version: "0.2.0",
Install: InstallPlan{
Type: InstallTypeDirect,
Artifacts: []Artifact{{
GOOS: "linux",
GOARCH: "amd64",
URL: "https://downloads.example/sample.zip?token=secret",
SHA256: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
}},
},
}}}
errValidate := ValidateRegistry(registry)
if errValidate == nil {
t.Fatal("ValidateRegistry() error = nil")
}
if !strings.Contains(errValidate.Error(), "sensitive query") {
t.Fatalf("ValidateRegistry() error = %v, want sensitive query", errValidate)
}
}
func TestValidateRegistryRejectsDirectInstallInSchemaV1(t *testing.T) {
t.Parallel()
registry := Registry{SchemaVersion: SchemaVersion, Plugins: []Plugin{{
ID: "sample-provider",
Name: "Sample Provider",
Description: "Adds sample provider support.",
Author: "author-name",
Version: "0.2.0",
Install: InstallPlan{
Type: InstallTypeDirect,
Artifacts: []Artifact{{
GOOS: "linux",
GOARCH: "amd64",
URL: "https://downloads.example/sample.zip",
SHA256: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
}},
},
}}}
errValidate := ValidateRegistry(registry)
if errValidate == nil {
t.Fatal("ValidateRegistry() error = nil")
}
if !strings.Contains(errValidate.Error(), "schema_version 2") {
t.Fatalf("ValidateRegistry() error = %v, want schema_version 2", errValidate)
}
}
func TestValidateRegistryRejectsInvalidEntries(t *testing.T) {
t.Parallel()
valid := Plugin{
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",
}
tests := []struct {
name string
mutate func(*Registry)
wantErr string
}{
{
name: "schema version",
mutate: func(registry *Registry) {
registry.SchemaVersion = 3
},
wantErr: "unsupported schema_version",
},
{
name: "missing required field",
mutate: func(registry *Registry) {
registry.Plugins[0].Name = ""
},
wantErr: "missing required field name",
},
{
name: "duplicate id",
mutate: func(registry *Registry) {
registry.Plugins = append(registry.Plugins, valid)
},
wantErr: "duplicate plugin id",
},
{
name: "invalid id",
mutate: func(registry *Registry) {
registry.Plugins[0].ID = "../sample-provider"
},
wantErr: "invalid plugin id",
},
{
name: "v-prefixed version",
mutate: func(registry *Registry) {
registry.Plugins[0].Version = "v0.1.0"
},
wantErr: "invalid plugin version",
},
{
name: "invalid repository",
mutate: func(registry *Registry) {
registry.Plugins[0].Repository = "https://example.com/author/repo"
},
wantErr: "repository must be",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
registry := Registry{SchemaVersion: 1, Plugins: []Plugin{valid}}
tt.mutate(®istry)
errValidate := ValidateRegistry(registry)
if errValidate == nil {
t.Fatal("ValidateRegistry() error = nil")
}
if !strings.Contains(errValidate.Error(), tt.wantErr) {
t.Fatalf("ValidateRegistry() error = %v, want substring %q", errValidate, tt.wantErr)
}
})
}
}
func TestNormalizeSourcesAppendsURLsToDefaultSource(t *testing.T) {
t.Parallel()
sources, errNormalize := NormalizeSources([]string{" https://community.example/registry.json "})
if errNormalize != nil {
t.Fatalf("NormalizeSources() error = %v", errNormalize)
}
if len(sources) != 2 {
t.Fatalf("sources len = %d, want 2", len(sources))
}
if sources[0].ID != DefaultSourceID || sources[0].URL != DefaultRegistryURL {
t.Fatalf("default source = %#v", sources[0])
}
if sources[1].ID != SourceID("https://community.example/registry.json") ||
sources[1].Name != "community.example" ||
sources[1].URL != "https://community.example/registry.json" {
t.Fatalf("third-party source = %#v", sources[1])
}
}
func TestNormalizeSourcesSkipsDuplicates(t *testing.T) {
t.Parallel()
sources, errNormalize := NormalizeSources([]string{
DefaultRegistryURL,
"https://community.example/registry.json",
"https://community.example/registry.json",
})
if errNormalize != nil {
t.Fatalf("NormalizeSources() error = %v", errNormalize)
}
if len(sources) != 2 {
t.Fatalf("sources len = %d, want 2: %#v", len(sources), sources)
}
}
func TestGitHubRepositoryPartsRejectsNonRepositoryURLs(t *testing.T) {
t.Parallel()
tests := []string{
"http://github.com/owner/repo",
"https://github.com/owner",
"https://github.com/owner/repo/issues",
"https://github.com/owner/repo.git",
"https://github.com/owner/repo?tab=readme",
}
for _, repository := range tests {
t.Run(repository, func(t *testing.T) {
t.Parallel()
if _, _, errParse := GitHubRepositoryParts(repository); errParse == nil {
t.Fatalf("GitHubRepositoryParts(%q) error = nil", repository)
}
})
}
}