247 lines
6.2 KiB
Go
247 lines
6.2 KiB
Go
package config
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
type testProfile struct {
|
|
BaseURL string `json:"base_url"`
|
|
StreamID string `json:"stream_id"`
|
|
}
|
|
|
|
func TestLoadMissingReturnsDefault(t *testing.T) {
|
|
store := NewStore[testProfile]("mcp-framework-test")
|
|
path := filepath.Join(t.TempDir(), "missing.json")
|
|
|
|
cfg, err := store.Load(path)
|
|
if err != nil {
|
|
t.Fatalf("Load returned error: %v", err)
|
|
}
|
|
|
|
if cfg.Version != CurrentVersion {
|
|
t.Fatalf("Version = %d, want %d", cfg.Version, CurrentVersion)
|
|
}
|
|
if len(cfg.Profiles) != 0 {
|
|
t.Fatalf("Profiles = %v, want empty", cfg.Profiles)
|
|
}
|
|
}
|
|
|
|
func TestLoadMigratesLegacyConfigAndPreservesProfiles(t *testing.T) {
|
|
store := NewStore[testProfile]("mcp-framework-test")
|
|
path := filepath.Join(t.TempDir(), "config.json")
|
|
|
|
data := []byte(`{
|
|
"current_profile": "prod",
|
|
"profiles": {
|
|
"prod": {
|
|
"base_url": "https://api.example.com",
|
|
"stream_id": "stream-1"
|
|
}
|
|
}
|
|
}
|
|
`)
|
|
if err := os.WriteFile(path, data, 0o600); err != nil {
|
|
t.Fatalf("WriteFile returned error: %v", err)
|
|
}
|
|
|
|
cfg, err := store.Load(path)
|
|
if err != nil {
|
|
t.Fatalf("Load returned error: %v", err)
|
|
}
|
|
|
|
if cfg.Version != CurrentVersion {
|
|
t.Fatalf("Version = %d, want %d", cfg.Version, CurrentVersion)
|
|
}
|
|
if cfg.CurrentProfile != "prod" {
|
|
t.Fatalf("CurrentProfile = %q, want prod", cfg.CurrentProfile)
|
|
}
|
|
if cfg.Profiles["prod"].StreamID != "stream-1" {
|
|
t.Fatalf("StreamID = %q, want stream-1", cfg.Profiles["prod"].StreamID)
|
|
}
|
|
}
|
|
|
|
func TestSaveAndLoadRoundTrip(t *testing.T) {
|
|
store := NewStore[testProfile]("mcp-framework-test")
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "mcp-framework", "config.json")
|
|
|
|
input := FileConfig[testProfile]{
|
|
Version: CurrentVersion,
|
|
CurrentProfile: "prod",
|
|
Profiles: map[string]testProfile{
|
|
"prod": {
|
|
BaseURL: "https://api.example.com",
|
|
StreamID: "stream-1",
|
|
},
|
|
},
|
|
}
|
|
|
|
if err := store.Save(path, input); err != nil {
|
|
t.Fatalf("Save returned error: %v", err)
|
|
}
|
|
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
t.Fatalf("Stat returned error: %v", err)
|
|
}
|
|
if info.Mode().Perm() != 0o600 {
|
|
t.Fatalf("file mode = %o, want 600", info.Mode().Perm())
|
|
}
|
|
|
|
dirInfo, err := os.Stat(filepath.Dir(path))
|
|
if err != nil {
|
|
t.Fatalf("Stat dir returned error: %v", err)
|
|
}
|
|
if dirInfo.Mode().Perm() != 0o700 {
|
|
t.Fatalf("dir mode = %o, want 700", dirInfo.Mode().Perm())
|
|
}
|
|
|
|
cfg, err := store.Load(path)
|
|
if err != nil {
|
|
t.Fatalf("Load returned error: %v", err)
|
|
}
|
|
|
|
if cfg.CurrentProfile != "prod" {
|
|
t.Fatalf("CurrentProfile = %q, want prod", cfg.CurrentProfile)
|
|
}
|
|
if cfg.Profiles["prod"].BaseURL != "https://api.example.com" {
|
|
t.Fatalf("BaseURL = %q", cfg.Profiles["prod"].BaseURL)
|
|
}
|
|
}
|
|
|
|
func TestLoadAppliesRegisteredMigrations(t *testing.T) {
|
|
store := NewStoreWithOptions[testProfile]("mcp-framework-test", Options[testProfile]{
|
|
Version: 2,
|
|
Migrations: map[int]Migration{
|
|
1: func(doc map[string]json.RawMessage) error {
|
|
doc["migrated"] = json.RawMessage(`true`)
|
|
return nil
|
|
},
|
|
},
|
|
})
|
|
path := filepath.Join(t.TempDir(), "config.json")
|
|
|
|
data := []byte(`{
|
|
"version": 1,
|
|
"current_profile": "prod",
|
|
"profiles": {
|
|
"prod": {
|
|
"base_url": "https://api.example.com",
|
|
"stream_id": "stream-1"
|
|
}
|
|
}
|
|
}
|
|
`)
|
|
if err := os.WriteFile(path, data, 0o600); err != nil {
|
|
t.Fatalf("WriteFile returned error: %v", err)
|
|
}
|
|
|
|
cfg, err := store.Load(path)
|
|
if err != nil {
|
|
t.Fatalf("Load returned error: %v", err)
|
|
}
|
|
|
|
if cfg.Version != 2 {
|
|
t.Fatalf("Version = %d, want 2", cfg.Version)
|
|
}
|
|
if cfg.CurrentProfile != "prod" {
|
|
t.Fatalf("CurrentProfile = %q, want prod", cfg.CurrentProfile)
|
|
}
|
|
if cfg.Profiles["prod"].BaseURL != "https://api.example.com" {
|
|
t.Fatalf("BaseURL = %q, want https://api.example.com", cfg.Profiles["prod"].BaseURL)
|
|
}
|
|
}
|
|
|
|
func TestLoadRejectsFutureVersion(t *testing.T) {
|
|
store := NewStore[testProfile]("mcp-framework-test")
|
|
path := filepath.Join(t.TempDir(), "config.json")
|
|
|
|
data := []byte(`{
|
|
"version": 2,
|
|
"profiles": {}
|
|
}
|
|
`)
|
|
if err := os.WriteFile(path, data, 0o600); err != nil {
|
|
t.Fatalf("WriteFile returned error: %v", err)
|
|
}
|
|
|
|
_, err := store.Load(path)
|
|
if !errors.Is(err, ErrFutureVersion) {
|
|
t.Fatalf("Load error = %v, want ErrFutureVersion", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadRejectsMissingMigrationPath(t *testing.T) {
|
|
store := NewStoreWithOptions[testProfile]("mcp-framework-test", Options[testProfile]{Version: 2})
|
|
path := filepath.Join(t.TempDir(), "config.json")
|
|
|
|
data := []byte(`{
|
|
"version": 1,
|
|
"profiles": {}
|
|
}
|
|
`)
|
|
if err := os.WriteFile(path, data, 0o600); err != nil {
|
|
t.Fatalf("WriteFile returned error: %v", err)
|
|
}
|
|
|
|
_, err := store.Load(path)
|
|
if !errors.Is(err, ErrUnsupportedVersion) {
|
|
t.Fatalf("Load error = %v, want ErrUnsupportedVersion", err)
|
|
}
|
|
if !strings.Contains(err.Error(), "no migration registered from version 1 to 2") {
|
|
t.Fatalf("Load error = %v, want missing migration detail", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadValidatesConfig(t *testing.T) {
|
|
store := NewStoreWithOptions[testProfile]("mcp-framework-test", Options[testProfile]{
|
|
Validator: func(cfg FileConfig[testProfile]) []ValidationIssue {
|
|
issues := make([]ValidationIssue, 0, len(cfg.Profiles))
|
|
for name, profile := range cfg.Profiles {
|
|
if profile.BaseURL == "" {
|
|
issues = append(issues, ValidationIssue{
|
|
Path: "profiles." + name + ".base_url",
|
|
Message: "must not be empty",
|
|
})
|
|
}
|
|
}
|
|
return issues
|
|
},
|
|
})
|
|
path := filepath.Join(t.TempDir(), "config.json")
|
|
|
|
data := []byte(`{
|
|
"version": 1,
|
|
"current_profile": "prod",
|
|
"profiles": {
|
|
"prod": {
|
|
"stream_id": "stream-1"
|
|
}
|
|
}
|
|
}
|
|
`)
|
|
if err := os.WriteFile(path, data, 0o600); err != nil {
|
|
t.Fatalf("WriteFile returned error: %v", err)
|
|
}
|
|
|
|
_, err := store.Load(path)
|
|
if !errors.Is(err, ErrInvalidConfig) {
|
|
t.Fatalf("Load error = %v, want ErrInvalidConfig", err)
|
|
}
|
|
|
|
var validationErr *ValidationError
|
|
if !errors.As(err, &validationErr) {
|
|
t.Fatalf("Load error = %v, want ValidationError", err)
|
|
}
|
|
if len(validationErr.Issues) != 1 {
|
|
t.Fatalf("Validation issues = %v, want 1 issue", validationErr.Issues)
|
|
}
|
|
if validationErr.Issues[0].Path != "profiles.prod.base_url" {
|
|
t.Fatalf("Validation issue path = %q, want profiles.prod.base_url", validationErr.Issues[0].Path)
|
|
}
|
|
}
|