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) } }