- Imported packages from separate repos: - github.com/Snider/config -> pkg/config - github.com/Snider/display -> pkg/display - github.com/Snider/help -> pkg/help - github.com/Snider/i18n -> pkg/i18n - github.com/Snider/updater -> pkg/updater - Moved core code from root to pkg/core - Flattened nested package structures - Updated all import paths to github.com/Snider/Core/pkg/* - Added Display interface to Core - Updated go.work for workspace modules Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
349 lines
8.5 KiB
Go
349 lines
8.5 KiB
Go
package config
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/Snider/Core/pkg/core"
|
|
)
|
|
|
|
// setupTestEnv creates a temporary home directory for testing and ensures a clean environment.
|
|
func setupTestEnv(t *testing.T) (string, func()) {
|
|
tempHomeDir, err := os.MkdirTemp("", "test_home_*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp home directory: %v", err)
|
|
}
|
|
|
|
oldHome := os.Getenv("HOME")
|
|
os.Setenv("HOME", tempHomeDir)
|
|
|
|
// Unset XDG vars to ensure HOME is used for path resolution, creating a hermetic test.
|
|
oldXdgData := os.Getenv("XDG_DATA_HOME")
|
|
oldXdgCache := os.Getenv("XDG_CACHE_HOME")
|
|
os.Unsetenv("XDG_DATA_HOME")
|
|
os.Unsetenv("XDG_CACHE_HOME")
|
|
|
|
cleanup := func() {
|
|
os.Setenv("HOME", oldHome)
|
|
os.Setenv("XDG_DATA_HOME", oldXdgData)
|
|
os.Setenv("XDG_CACHE_HOME", oldXdgCache)
|
|
os.RemoveAll(tempHomeDir)
|
|
}
|
|
|
|
return tempHomeDir, cleanup
|
|
}
|
|
|
|
// newTestCore creates a new, empty core instance for testing.
|
|
func newTestCore(t *testing.T) *core.Core {
|
|
c, err := core.New()
|
|
if err != nil {
|
|
t.Fatalf("core.New() failed: %v", err)
|
|
}
|
|
if c == nil {
|
|
t.Fatalf("core.New() returned a nil instance")
|
|
}
|
|
return c
|
|
}
|
|
|
|
func TestConfigServiceGood(t *testing.T) {
|
|
t.Run("New service creates default config", func(t *testing.T) {
|
|
_, cleanup := setupTestEnv(t)
|
|
defer cleanup()
|
|
|
|
serviceInstance, err := New()
|
|
if err != nil {
|
|
t.Fatalf("New() failed: %v", err)
|
|
}
|
|
|
|
// Check that the config file was created
|
|
if _, err := os.Stat(serviceInstance.ConfigPath); os.IsNotExist(err) {
|
|
t.Errorf("config.json was not created at %s", serviceInstance.ConfigPath)
|
|
}
|
|
|
|
// Check default values
|
|
if serviceInstance.Language != "en" {
|
|
t.Errorf("Expected default language 'en', got '%s'", serviceInstance.Language)
|
|
}
|
|
})
|
|
|
|
t.Run("New service loads existing config", func(t *testing.T) {
|
|
tempHomeDir, cleanup := setupTestEnv(t)
|
|
defer cleanup()
|
|
|
|
// Manually create a config file with non-default values
|
|
configDir := filepath.Join(tempHomeDir, appName, "config")
|
|
if err := os.MkdirAll(configDir, os.ModePerm); err != nil {
|
|
t.Fatalf("Failed to create test config dir: %v", err)
|
|
}
|
|
configPath := filepath.Join(configDir, configFileName)
|
|
|
|
customConfig := `{"language": "fr", "features": ["beta-testing"]}`
|
|
if err := os.WriteFile(configPath, []byte(customConfig), 0644); err != nil {
|
|
t.Fatalf("Failed to write custom config file: %v", err)
|
|
}
|
|
|
|
serviceInstance, err := New()
|
|
if err != nil {
|
|
t.Fatalf("New() failed while loading existing config: %v", err)
|
|
}
|
|
|
|
if serviceInstance.Language != "fr" {
|
|
t.Errorf("Expected language 'fr', got '%s'", serviceInstance.Language)
|
|
}
|
|
// A check for IsFeatureEnabled would require a proper core instance and service registration.
|
|
// This is a simplified check for now.
|
|
found := false
|
|
for _, f := range serviceInstance.Features {
|
|
if f == "beta-testing" {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("Expected 'beta-testing' feature to be enabled")
|
|
}
|
|
})
|
|
|
|
t.Run("Set and Get", func(t *testing.T) {
|
|
_, cleanup := setupTestEnv(t)
|
|
defer cleanup()
|
|
|
|
s, err := New()
|
|
if err != nil {
|
|
t.Fatalf("New() failed: %v", err)
|
|
}
|
|
|
|
key := "language"
|
|
expectedValue := "de"
|
|
if err := s.Set(key, expectedValue); err != nil {
|
|
t.Fatalf("Set() failed: %v", err)
|
|
}
|
|
|
|
var actualValue string
|
|
if err := s.Get(key, &actualValue); err != nil {
|
|
t.Fatalf("Get() failed: %v", err)
|
|
}
|
|
|
|
if actualValue != expectedValue {
|
|
t.Errorf("Expected value '%s', got '%s'", expectedValue, actualValue)
|
|
}
|
|
})
|
|
|
|
t.Run("Save and Load Struct", func(t *testing.T) {
|
|
_, cleanup := setupTestEnv(t)
|
|
defer cleanup()
|
|
|
|
s, err := New()
|
|
if err != nil {
|
|
t.Fatalf("New() failed: %v", err)
|
|
}
|
|
|
|
type CustomConfig struct {
|
|
APIKey string `json:"apiKey"`
|
|
Timeout int `json:"timeout"`
|
|
}
|
|
|
|
key := "custom"
|
|
expectedConfig := CustomConfig{
|
|
APIKey: "12345",
|
|
Timeout: 30,
|
|
}
|
|
|
|
if err := s.SaveStruct(key, expectedConfig); err != nil {
|
|
t.Fatalf("SaveStruct() failed: %v", err)
|
|
}
|
|
|
|
var actualConfig CustomConfig
|
|
if err := s.LoadStruct(key, &actualConfig); err != nil {
|
|
t.Fatalf("LoadStruct() failed: %v", err)
|
|
}
|
|
|
|
if actualConfig.APIKey != expectedConfig.APIKey {
|
|
t.Errorf("Expected APIKey '%s', got '%s'", expectedConfig.APIKey, actualConfig.APIKey)
|
|
}
|
|
if actualConfig.Timeout != expectedConfig.Timeout {
|
|
t.Errorf("Expected Timeout '%d', got '%d'", expectedConfig.Timeout, actualConfig.Timeout)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestConfigServiceUgly(t *testing.T) {
|
|
t.Run("LoadStruct with nil value", func(t *testing.T) {
|
|
_, cleanup := setupTestEnv(t)
|
|
defer cleanup()
|
|
|
|
s, err := New()
|
|
if err != nil {
|
|
t.Fatalf("New() failed: %v", err)
|
|
}
|
|
|
|
key := "nil-value"
|
|
filePath := filepath.Join(s.ConfigDir, key+".json")
|
|
if err := os.WriteFile(filePath, []byte("null"), 0644); err != nil {
|
|
t.Fatalf("Failed to write nil value file: %v", err)
|
|
}
|
|
|
|
type CustomConfig struct {
|
|
APIKey string `json:"apiKey"`
|
|
Timeout int `json:"timeout"`
|
|
}
|
|
|
|
var actualConfig CustomConfig
|
|
err = s.LoadStruct(key, &actualConfig)
|
|
if err != nil {
|
|
t.Fatalf("LoadStruct() should not have failed with a nil value, but it did: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("Concurrent access", func(t *testing.T) {
|
|
_, cleanup := setupTestEnv(t)
|
|
defer cleanup()
|
|
|
|
s, err := New()
|
|
if err != nil {
|
|
t.Fatalf("New() failed: %v", err)
|
|
}
|
|
|
|
// Run concurrent Set and Get operations
|
|
done := make(chan bool)
|
|
for i := 0; i < 10; i++ {
|
|
go func() {
|
|
s.Set("language", "en")
|
|
done <- true
|
|
}()
|
|
go func() {
|
|
var lang string
|
|
s.Get("language", &lang)
|
|
done <- true
|
|
}()
|
|
}
|
|
|
|
for i := 0; i < 20; i++ {
|
|
<-done
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestConfigServiceBad(t *testing.T) {
|
|
t.Run("Load non-existent struct", func(t *testing.T) {
|
|
_, cleanup := setupTestEnv(t)
|
|
defer cleanup()
|
|
|
|
s, err := New()
|
|
if err != nil {
|
|
t.Fatalf("New() failed: %v", err)
|
|
}
|
|
|
|
type CustomConfig struct {
|
|
APIKey string `json:"apiKey"`
|
|
Timeout int `json:"timeout"`
|
|
}
|
|
|
|
var actualConfig CustomConfig
|
|
if err := s.LoadStruct("non-existent", &actualConfig); err != nil {
|
|
t.Fatalf("LoadStruct() failed: %v", err)
|
|
}
|
|
|
|
// Expect the struct to be zero-valued
|
|
if actualConfig.APIKey != "" {
|
|
t.Errorf("Expected empty APIKey, got '%s'", actualConfig.APIKey)
|
|
}
|
|
if actualConfig.Timeout != 0 {
|
|
t.Errorf("Expected zero Timeout, got '%d'", actualConfig.Timeout)
|
|
}
|
|
})
|
|
|
|
t.Run("Get non-existent key", func(t *testing.T) {
|
|
_, cleanup := setupTestEnv(t)
|
|
defer cleanup()
|
|
|
|
s, err := New()
|
|
if err != nil {
|
|
t.Fatalf("New() failed: %v", err)
|
|
}
|
|
|
|
var value string
|
|
err = s.Get("non-existent", &value)
|
|
if err == nil {
|
|
t.Errorf("Expected an error for non-existent key, but got nil")
|
|
}
|
|
})
|
|
|
|
t.Run("Set non-existent key", func(t *testing.T) {
|
|
_, cleanup := setupTestEnv(t)
|
|
defer cleanup()
|
|
|
|
s, err := New()
|
|
if err != nil {
|
|
t.Fatalf("New() failed: %v", err)
|
|
}
|
|
|
|
err = s.Set("non-existent", "value")
|
|
if err == nil {
|
|
t.Errorf("Expected an error for non-existent key, but got nil")
|
|
}
|
|
})
|
|
|
|
t.Run("SaveStruct with unmarshallable type", func(t *testing.T) {
|
|
_, cleanup := setupTestEnv(t)
|
|
defer cleanup()
|
|
|
|
s, err := New()
|
|
if err != nil {
|
|
t.Fatalf("New() failed: %v", err)
|
|
}
|
|
|
|
err = s.SaveStruct("test", make(chan int))
|
|
if err == nil {
|
|
t.Errorf("Expected an error for unmarshallable type, but got nil")
|
|
}
|
|
})
|
|
|
|
t.Run("LoadStruct with invalid JSON", func(t *testing.T) {
|
|
_, cleanup := setupTestEnv(t)
|
|
defer cleanup()
|
|
|
|
s, err := New()
|
|
if err != nil {
|
|
t.Fatalf("New() failed: %v", err)
|
|
}
|
|
|
|
key := "invalid"
|
|
filePath := filepath.Join(s.ConfigDir, key+".json")
|
|
if err := os.WriteFile(filePath, []byte("invalid json"), 0644); err != nil {
|
|
t.Fatalf("Failed to write invalid json file: %v", err)
|
|
}
|
|
|
|
type CustomConfig struct {
|
|
APIKey string `json:"apiKey"`
|
|
Timeout int `json:"timeout"`
|
|
}
|
|
|
|
var actualConfig CustomConfig
|
|
err = s.LoadStruct(key, &actualConfig)
|
|
if err == nil {
|
|
t.Errorf("Expected an error for invalid JSON, but got nil")
|
|
}
|
|
})
|
|
|
|
t.Run("New service with empty config file", func(t *testing.T) {
|
|
tempHomeDir, cleanup := setupTestEnv(t)
|
|
defer cleanup()
|
|
|
|
// Manually create an empty config file
|
|
configDir := filepath.Join(tempHomeDir, appName, "config")
|
|
if err := os.MkdirAll(configDir, os.ModePerm); err != nil {
|
|
t.Fatalf("Failed to create test config dir: %v", err)
|
|
}
|
|
configPath := filepath.Join(configDir, configFileName)
|
|
if err := os.WriteFile(configPath, []byte(""), 0644); err != nil {
|
|
t.Fatalf("Failed to write empty config file: %v", err)
|
|
}
|
|
|
|
_, err := New()
|
|
if err == nil {
|
|
t.Fatalf("New() should have failed with an empty config file, but it did not")
|
|
}
|
|
})
|
|
}
|