267 lines
7.2 KiB
Go
267 lines
7.2 KiB
Go
package pack
|
|
|
|
import (
|
|
"archive/zip"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestValidateManifestRequiredFields(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
manifest Manifest
|
|
wantErr string
|
|
}{
|
|
{
|
|
name: "empty pack id",
|
|
manifest: Manifest{
|
|
Version: "1.0.0",
|
|
TargetHost: "sub2api",
|
|
ProvidersDir: "providers",
|
|
ChecksumFile: "checksums.txt",
|
|
},
|
|
wantErr: "pack_id is required",
|
|
},
|
|
{
|
|
name: "empty version",
|
|
manifest: Manifest{
|
|
PackID: "openai-cn-pack",
|
|
TargetHost: "sub2api",
|
|
ProvidersDir: "providers",
|
|
ChecksumFile: "checksums.txt",
|
|
},
|
|
wantErr: "version is required",
|
|
},
|
|
{
|
|
name: "empty target host",
|
|
manifest: Manifest{
|
|
PackID: "openai-cn-pack",
|
|
Version: "1.0.0",
|
|
ProvidersDir: "providers",
|
|
ChecksumFile: "checksums.txt",
|
|
},
|
|
wantErr: "target_host is required",
|
|
},
|
|
{
|
|
name: "empty providers dir",
|
|
manifest: Manifest{
|
|
PackID: "openai-cn-pack",
|
|
Version: "1.0.0",
|
|
TargetHost: "sub2api",
|
|
ChecksumFile: "checksums.txt",
|
|
},
|
|
wantErr: "providers_dir is required",
|
|
},
|
|
{
|
|
name: "empty checksum file",
|
|
manifest: Manifest{
|
|
PackID: "openai-cn-pack",
|
|
Version: "1.0.0",
|
|
TargetHost: "sub2api",
|
|
ProvidersDir: "providers",
|
|
},
|
|
wantErr: "checksum_file is required",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := validateManifest(tt.manifest)
|
|
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
|
|
t.Fatalf("validateManifest() error = %v, want substring %q", err, tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateProvidersRejectsInvalidProviderFields(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
providers []ProviderManifest
|
|
wantErr string
|
|
}{
|
|
{
|
|
name: "empty provider id",
|
|
providers: []ProviderManifest{{
|
|
DisplayName: "DeepSeek",
|
|
BaseURL: "https://api.deepseek.com",
|
|
Platform: "openai",
|
|
AccountType: "apikey",
|
|
DefaultModels: []string{"deepseek-chat"},
|
|
SmokeTestModel: "deepseek-chat",
|
|
GroupTemplate: GroupTemplate{Name: "g"},
|
|
ChannelTemplate: ChannelTemplate{
|
|
Name: "c",
|
|
ModelMapping: map[string]string{"deepseek-chat": "deepseek-chat"},
|
|
},
|
|
PlanTemplate: PlanTemplate{Name: "p", ValidityDays: 30},
|
|
}},
|
|
wantErr: "provider_id is required",
|
|
},
|
|
{
|
|
name: "empty base url",
|
|
providers: []ProviderManifest{{
|
|
ProviderID: "deepseek",
|
|
DisplayName: "DeepSeek",
|
|
BaseURL: "",
|
|
Platform: "openai",
|
|
AccountType: "apikey",
|
|
DefaultModels: []string{"deepseek-chat"},
|
|
SmokeTestModel: "deepseek-chat",
|
|
GroupTemplate: GroupTemplate{Name: "g"},
|
|
ChannelTemplate: ChannelTemplate{
|
|
Name: "c",
|
|
ModelMapping: map[string]string{"deepseek-chat": "deepseek-chat"},
|
|
},
|
|
PlanTemplate: PlanTemplate{Name: "p", ValidityDays: 30},
|
|
}},
|
|
wantErr: "base_url must use https",
|
|
},
|
|
{
|
|
name: "missing display name",
|
|
providers: []ProviderManifest{{
|
|
ProviderID: "deepseek",
|
|
DisplayName: "",
|
|
BaseURL: "https://api.deepseek.com",
|
|
Platform: "openai",
|
|
AccountType: "apikey",
|
|
DefaultModels: []string{"deepseek-chat"},
|
|
SmokeTestModel: "deepseek-chat",
|
|
GroupTemplate: GroupTemplate{Name: "g"},
|
|
ChannelTemplate: ChannelTemplate{
|
|
Name: "c",
|
|
ModelMapping: map[string]string{"deepseek-chat": "deepseek-chat"},
|
|
},
|
|
PlanTemplate: PlanTemplate{Name: "p", ValidityDays: 30},
|
|
}},
|
|
wantErr: "display_name is required",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := validateProviders(t.TempDir(), tt.providers)
|
|
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
|
|
t.Fatalf("validateProviders() error = %v, want substring %q", err, tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExtractZipToTempRejectsEmptyArchive(t *testing.T) {
|
|
archivePath := filepath.Join(t.TempDir(), "empty.zip")
|
|
file, err := os.Create(archivePath)
|
|
if err != nil {
|
|
t.Fatalf("os.Create() error = %v", err)
|
|
}
|
|
writer := zip.NewWriter(file)
|
|
if err := writer.Close(); err != nil {
|
|
t.Fatalf("writer.Close() error = %v", err)
|
|
}
|
|
if err := file.Close(); err != nil {
|
|
t.Fatalf("file.Close() error = %v", err)
|
|
}
|
|
|
|
_, cleanup, err := extractZipToTemp(archivePath)
|
|
if cleanup != nil {
|
|
cleanup()
|
|
}
|
|
if err == nil || !strings.Contains(err.Error(), "pack archive is empty") {
|
|
t.Fatalf("extractZipToTemp() error = %v, want empty archive error", err)
|
|
}
|
|
}
|
|
|
|
func TestExtractZipToTempRejectsPathTraversal(t *testing.T) {
|
|
archivePath := filepath.Join(t.TempDir(), "traversal.zip")
|
|
file, err := os.Create(archivePath)
|
|
if err != nil {
|
|
t.Fatalf("os.Create() error = %v", err)
|
|
}
|
|
writer := zip.NewWriter(file)
|
|
entry, err := writer.Create("../../../evil.txt")
|
|
if err != nil {
|
|
t.Fatalf("writer.Create() error = %v", err)
|
|
}
|
|
if _, err := entry.Write([]byte("evil")); err != nil {
|
|
t.Fatalf("entry.Write() error = %v", err)
|
|
}
|
|
if err := writer.Close(); err != nil {
|
|
t.Fatalf("writer.Close() error = %v", err)
|
|
}
|
|
if err := file.Close(); err != nil {
|
|
t.Fatalf("file.Close() error = %v", err)
|
|
}
|
|
|
|
_, cleanup, err := extractZipToTemp(archivePath)
|
|
if cleanup != nil {
|
|
cleanup()
|
|
}
|
|
if err == nil || !strings.Contains(err.Error(), "invalid path") {
|
|
t.Fatalf("extractZipToTemp() error = %v, want invalid path error", err)
|
|
}
|
|
}
|
|
|
|
func TestMatchesMaxConstraintCases(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
hostVersion string
|
|
maxVersion string
|
|
want bool
|
|
}{
|
|
{name: "exact version match", hostVersion: "1.2.3", maxVersion: "1.2.3", want: true},
|
|
{name: "wildcard x accepts same minor", hostVersion: "0.2.9", maxVersion: "0.2.x", want: true},
|
|
{name: "non matching version", hostVersion: "1.2.4", maxVersion: "1.2.3", want: false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := matchesMaxConstraint(tt.hostVersion, tt.maxVersion)
|
|
if err != nil {
|
|
t.Fatalf("matchesMaxConstraint() error = %v", err)
|
|
}
|
|
if got != tt.want {
|
|
t.Fatalf("matchesMaxConstraint() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMatchesMaxConstraintRejectsWildcardStar(t *testing.T) {
|
|
_, err := matchesMaxConstraint("1.2.3", "1.2.*")
|
|
if err == nil || !strings.Contains(err.Error(), `parse version segment "*"`) {
|
|
t.Fatalf("matchesMaxConstraint() error = %v, want parse failure for wildcard star", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadPathRejectsEmptyAndMissingPath(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
path string
|
|
wantErr string
|
|
}{
|
|
{name: "empty path", path: " ", wantErr: "pack path is required"},
|
|
{name: "missing path", path: filepath.Join(t.TempDir(), "missing-pack"), wantErr: "stat pack path"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
_, err := LoadPath(tt.path)
|
|
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
|
|
t.Fatalf("LoadPath() error = %v, want substring %q", err, tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLoadArchiveRejectsNonZipFile(t *testing.T) {
|
|
filePath := filepath.Join(t.TempDir(), "not-a-zip.zip")
|
|
mustWrite(t, filePath, "plain text, not a zip archive")
|
|
|
|
_, err := LoadArchive(filePath)
|
|
if err == nil || !strings.Contains(err.Error(), "open pack archive") {
|
|
t.Fatalf("LoadArchive() error = %v, want open archive error", err)
|
|
}
|
|
}
|