mirror of
https://github.com/turbot/steampipe.git
synced 2025-12-19 18:12:43 -05:00
681 lines
15 KiB
Go
681 lines
15 KiB
Go
package cmdconfig
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/spf13/viper"
|
|
pconstants "github.com/turbot/pipe-fittings/v2/constants"
|
|
"github.com/turbot/steampipe/v2/pkg/constants"
|
|
)
|
|
|
|
func TestViper(t *testing.T) {
|
|
v := Viper()
|
|
if v == nil {
|
|
t.Fatal("Viper() returned nil")
|
|
}
|
|
// Should return the global viper instance
|
|
if v != viper.GetViper() {
|
|
t.Error("Viper() should return the global viper instance")
|
|
}
|
|
}
|
|
|
|
func TestSetBaseDefaults(t *testing.T) {
|
|
// Save original viper state
|
|
origTelemetry := viper.Get(pconstants.ArgTelemetry)
|
|
origUpdateCheck := viper.Get(pconstants.ArgUpdateCheck)
|
|
origPort := viper.Get(pconstants.ArgDatabasePort)
|
|
defer func() {
|
|
// Restore original state
|
|
if origTelemetry != nil {
|
|
viper.Set(pconstants.ArgTelemetry, origTelemetry)
|
|
}
|
|
if origUpdateCheck != nil {
|
|
viper.Set(pconstants.ArgUpdateCheck, origUpdateCheck)
|
|
}
|
|
if origPort != nil {
|
|
viper.Set(pconstants.ArgDatabasePort, origPort)
|
|
}
|
|
}()
|
|
|
|
err := setBaseDefaults()
|
|
if err != nil {
|
|
t.Fatalf("setBaseDefaults() returned error: %v", err)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
key string
|
|
expected interface{}
|
|
}{
|
|
{
|
|
name: "telemetry_default",
|
|
key: pconstants.ArgTelemetry,
|
|
expected: constants.TelemetryInfo,
|
|
},
|
|
{
|
|
name: "update_check_default",
|
|
key: pconstants.ArgUpdateCheck,
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "database_port_default",
|
|
key: pconstants.ArgDatabasePort,
|
|
expected: constants.DatabaseDefaultPort,
|
|
},
|
|
{
|
|
name: "autocomplete_default",
|
|
key: pconstants.ArgAutoComplete,
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "cache_enabled_default",
|
|
key: pconstants.ArgServiceCacheEnabled,
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "cache_max_ttl_default",
|
|
key: pconstants.ArgCacheMaxTtl,
|
|
expected: 300,
|
|
},
|
|
{
|
|
name: "memory_max_mb_plugin_default",
|
|
key: pconstants.ArgMemoryMaxMbPlugin,
|
|
expected: 1024,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
val := viper.Get(tt.key)
|
|
if val != tt.expected {
|
|
t.Errorf("Expected %v for %s, got %v", tt.expected, tt.key, val)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSetDefaultFromEnv_String(t *testing.T) {
|
|
// Clean up viper state
|
|
viper.Reset()
|
|
defer viper.Reset()
|
|
|
|
testKey := "TEST_ENV_VAR_STRING"
|
|
configVar := "test-config-var-string"
|
|
testValue := "test-value"
|
|
|
|
// Set environment variable
|
|
os.Setenv(testKey, testValue)
|
|
defer os.Unsetenv(testKey)
|
|
|
|
SetDefaultFromEnv(testKey, configVar, String)
|
|
|
|
result := viper.GetString(configVar)
|
|
if result != testValue {
|
|
t.Errorf("Expected %s, got %s", testValue, result)
|
|
}
|
|
}
|
|
|
|
func TestSetDefaultFromEnv_Bool(t *testing.T) {
|
|
// Clean up viper state
|
|
viper.Reset()
|
|
defer viper.Reset()
|
|
|
|
tests := []struct {
|
|
name string
|
|
envValue string
|
|
expected bool
|
|
shouldSet bool
|
|
}{
|
|
{
|
|
name: "true_value",
|
|
envValue: "true",
|
|
expected: true,
|
|
shouldSet: true,
|
|
},
|
|
{
|
|
name: "false_value",
|
|
envValue: "false",
|
|
expected: false,
|
|
shouldSet: true,
|
|
},
|
|
{
|
|
name: "1_value",
|
|
envValue: "1",
|
|
expected: true,
|
|
shouldSet: true,
|
|
},
|
|
{
|
|
name: "0_value",
|
|
envValue: "0",
|
|
expected: false,
|
|
shouldSet: true,
|
|
},
|
|
{
|
|
name: "invalid_value",
|
|
envValue: "invalid",
|
|
expected: false,
|
|
shouldSet: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
viper.Reset()
|
|
testKey := "TEST_ENV_VAR_BOOL"
|
|
configVar := "test-config-var-bool"
|
|
|
|
os.Setenv(testKey, tt.envValue)
|
|
defer os.Unsetenv(testKey)
|
|
|
|
SetDefaultFromEnv(testKey, configVar, Bool)
|
|
|
|
if tt.shouldSet {
|
|
result := viper.GetBool(configVar)
|
|
if result != tt.expected {
|
|
t.Errorf("Expected %v, got %v", tt.expected, result)
|
|
}
|
|
} else {
|
|
// For invalid values, viper should return the zero value
|
|
result := viper.GetBool(configVar)
|
|
if result != false {
|
|
t.Errorf("Expected false for invalid bool value, got %v", result)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSetDefaultFromEnv_Int(t *testing.T) {
|
|
// Clean up viper state
|
|
viper.Reset()
|
|
defer viper.Reset()
|
|
|
|
tests := []struct {
|
|
name string
|
|
envValue string
|
|
expected int64
|
|
shouldSet bool
|
|
}{
|
|
{
|
|
name: "positive_int",
|
|
envValue: "42",
|
|
expected: 42,
|
|
shouldSet: true,
|
|
},
|
|
{
|
|
name: "negative_int",
|
|
envValue: "-10",
|
|
expected: -10,
|
|
shouldSet: true,
|
|
},
|
|
{
|
|
name: "zero",
|
|
envValue: "0",
|
|
expected: 0,
|
|
shouldSet: true,
|
|
},
|
|
{
|
|
name: "invalid_value",
|
|
envValue: "not-a-number",
|
|
expected: 0,
|
|
shouldSet: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
viper.Reset()
|
|
testKey := "TEST_ENV_VAR_INT"
|
|
configVar := "test-config-var-int"
|
|
|
|
os.Setenv(testKey, tt.envValue)
|
|
defer os.Unsetenv(testKey)
|
|
|
|
SetDefaultFromEnv(testKey, configVar, Int)
|
|
|
|
if tt.shouldSet {
|
|
result := viper.GetInt64(configVar)
|
|
if result != tt.expected {
|
|
t.Errorf("Expected %d, got %d", tt.expected, result)
|
|
}
|
|
} else {
|
|
// For invalid values, viper should return the zero value
|
|
result := viper.GetInt64(configVar)
|
|
if result != 0 {
|
|
t.Errorf("Expected 0 for invalid int value, got %d", result)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSetDefaultFromEnv_MissingEnvVar(t *testing.T) {
|
|
// Clean up viper state
|
|
viper.Reset()
|
|
defer viper.Reset()
|
|
|
|
testKey := "NONEXISTENT_ENV_VAR"
|
|
configVar := "test-config-var"
|
|
|
|
// Ensure the env var doesn't exist
|
|
os.Unsetenv(testKey)
|
|
|
|
// This should not panic or error, just not set anything
|
|
SetDefaultFromEnv(testKey, configVar, String)
|
|
|
|
// The config var should not be set
|
|
if viper.IsSet(configVar) {
|
|
t.Error("Config var should not be set when env var doesn't exist")
|
|
}
|
|
}
|
|
|
|
func TestSetDefaultsFromConfig(t *testing.T) {
|
|
// Clean up viper state
|
|
viper.Reset()
|
|
defer viper.Reset()
|
|
|
|
configMap := map[string]interface{}{
|
|
"key1": "value1",
|
|
"key2": 42,
|
|
"key3": true,
|
|
}
|
|
|
|
SetDefaultsFromConfig(configMap)
|
|
|
|
if viper.GetString("key1") != "value1" {
|
|
t.Errorf("Expected key1 to be 'value1', got %s", viper.GetString("key1"))
|
|
}
|
|
if viper.GetInt("key2") != 42 {
|
|
t.Errorf("Expected key2 to be 42, got %d", viper.GetInt("key2"))
|
|
}
|
|
if viper.GetBool("key3") != true {
|
|
t.Errorf("Expected key3 to be true, got %v", viper.GetBool("key3"))
|
|
}
|
|
}
|
|
|
|
func TestTildefyPaths(t *testing.T) {
|
|
// Save original viper state
|
|
viper.Reset()
|
|
defer viper.Reset()
|
|
|
|
// Test with a path that doesn't contain tilde
|
|
viper.Set(pconstants.ArgModLocation, "/absolute/path")
|
|
viper.Set(pconstants.ArgInstallDir, "/another/absolute/path")
|
|
|
|
err := tildefyPaths()
|
|
if err != nil {
|
|
t.Fatalf("tildefyPaths() returned error: %v", err)
|
|
}
|
|
|
|
// Paths without tilde should remain unchanged
|
|
if viper.GetString(pconstants.ArgModLocation) != "/absolute/path" {
|
|
t.Error("Absolute path should remain unchanged")
|
|
}
|
|
}
|
|
|
|
func TestSetConfigFromEnv(t *testing.T) {
|
|
viper.Reset()
|
|
defer viper.Reset()
|
|
|
|
testKey := "TEST_MULTI_CONFIG_VAR"
|
|
testValue := "test-value"
|
|
configs := []string{"config1", "config2", "config3"}
|
|
|
|
os.Setenv(testKey, testValue)
|
|
defer os.Unsetenv(testKey)
|
|
|
|
setConfigFromEnv(testKey, configs, String)
|
|
|
|
// All configs should be set to the same value
|
|
for _, config := range configs {
|
|
if viper.GetString(config) != testValue {
|
|
t.Errorf("Expected %s to be set to %s, got %s", config, testValue, viper.GetString(config))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Concurrency and race condition tests
|
|
|
|
func TestViperGlobalState_ConcurrentReads(t *testing.T) {
|
|
// Test concurrent reads from viper - should be safe
|
|
viper.Reset()
|
|
defer viper.Reset()
|
|
|
|
viper.Set("test-key", "test-value")
|
|
|
|
done := make(chan bool)
|
|
errors := make(chan string, 100)
|
|
numGoroutines := 10
|
|
|
|
for i := 0; i < numGoroutines; i++ {
|
|
go func(id int) {
|
|
defer func() { done <- true }()
|
|
for j := 0; j < 100; j++ {
|
|
val := viper.GetString("test-key")
|
|
if val != "test-value" {
|
|
errors <- fmt.Sprintf("Goroutine %d: Expected 'test-value', got '%s'", id, val)
|
|
}
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
for i := 0; i < numGoroutines; i++ {
|
|
<-done
|
|
}
|
|
close(errors)
|
|
|
|
for err := range errors {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
|
|
func TestViperGlobalState_ConcurrentWrites(t *testing.T) {
|
|
// t.Skip("Demonstrates bugs #4756, #4757 - Viper global state has race conditions on concurrent writes. Remove this skip in bug fix PR commit 1, then fix in commit 2.")
|
|
// Test concurrent writes to viper with mutex protection
|
|
viperMutex.Lock()
|
|
viper.Reset()
|
|
viperMutex.Unlock()
|
|
defer func() {
|
|
viperMutex.Lock()
|
|
viper.Reset()
|
|
viperMutex.Unlock()
|
|
}()
|
|
|
|
done := make(chan bool)
|
|
numGoroutines := 5
|
|
|
|
for i := 0; i < numGoroutines; i++ {
|
|
go func(id int) {
|
|
defer func() { done <- true }()
|
|
for j := 0; j < 50; j++ {
|
|
viperMutex.Lock()
|
|
viper.Set("concurrent-key", id)
|
|
viperMutex.Unlock()
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
for i := 0; i < numGoroutines; i++ {
|
|
<-done
|
|
}
|
|
|
|
// The final value is now deterministic with mutex protection
|
|
viperMutex.RLock()
|
|
finalVal := viper.GetInt("concurrent-key")
|
|
viperMutex.RUnlock()
|
|
t.Logf("Final value after concurrent writes: %d", finalVal)
|
|
}
|
|
|
|
func TestViperGlobalState_ConcurrentReadWrite(t *testing.T) {
|
|
// t.Skip("Demonstrates bugs #4756, #4757 - Viper global state has race conditions on concurrent read/write. Remove this skip in bug fix PR commit 1, then fix in commit 2.")
|
|
// Test concurrent reads and writes with mutex protection
|
|
viperMutex.Lock()
|
|
viper.Reset()
|
|
viper.Set("race-key", "initial")
|
|
viperMutex.Unlock()
|
|
defer func() {
|
|
viperMutex.Lock()
|
|
viper.Reset()
|
|
viperMutex.Unlock()
|
|
}()
|
|
|
|
done := make(chan bool)
|
|
numReaders := 5
|
|
numWriters := 5
|
|
|
|
// Start readers
|
|
for i := 0; i < numReaders; i++ {
|
|
go func(id int) {
|
|
defer func() { done <- true }()
|
|
for j := 0; j < 100; j++ {
|
|
viperMutex.RLock()
|
|
_ = viper.GetString("race-key")
|
|
viperMutex.RUnlock()
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
// Start writers
|
|
for i := 0; i < numWriters; i++ {
|
|
go func(id int) {
|
|
defer func() { done <- true }()
|
|
for j := 0; j < 50; j++ {
|
|
viperMutex.Lock()
|
|
viper.Set("race-key", id)
|
|
viperMutex.Unlock()
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
// Wait for all goroutines
|
|
for i := 0; i < numReaders+numWriters; i++ {
|
|
<-done
|
|
}
|
|
|
|
t.Log("Concurrent read/write completed successfully with mutex protection")
|
|
}
|
|
|
|
func TestSetDefaultFromEnv_ConcurrentAccess(t *testing.T) {
|
|
// t.Skip("Demonstrates bugs #4756, #4757 - SetDefaultFromEnv has race conditions on concurrent access. Remove this skip in bug fix PR commit 1, then fix in commit 2.")
|
|
// BUG?: Test concurrent access to SetDefaultFromEnv
|
|
viper.Reset()
|
|
defer viper.Reset()
|
|
|
|
// Set up multiple env vars
|
|
envVars := make(map[string]string)
|
|
for i := 0; i < 10; i++ {
|
|
key := "TEST_CONCURRENT_ENV_" + string(rune('A'+i))
|
|
val := "value" + string(rune('0'+i))
|
|
envVars[key] = val
|
|
os.Setenv(key, val)
|
|
defer os.Unsetenv(key)
|
|
}
|
|
|
|
done := make(chan bool)
|
|
numGoroutines := 10
|
|
|
|
// Concurrently set defaults from env
|
|
i := 0
|
|
for key := range envVars {
|
|
go func(envKey string, configVar string) {
|
|
defer func() { done <- true }()
|
|
SetDefaultFromEnv(envKey, configVar, String)
|
|
}(key, "config-var-"+string(rune('A'+i)))
|
|
i++
|
|
}
|
|
|
|
for i := 0; i < numGoroutines; i++ {
|
|
<-done
|
|
}
|
|
|
|
t.Log("Concurrent SetDefaultFromEnv completed")
|
|
}
|
|
|
|
func TestSetDefaultsFromConfig_ConcurrentCalls(t *testing.T) {
|
|
// t.Skip("Demonstrates bugs #4756, #4757 - SetDefaultsFromConfig has race conditions on concurrent calls. Remove this skip in bug fix PR commit 1, then fix in commit 2.")
|
|
// BUG?: Test concurrent calls to SetDefaultsFromConfig
|
|
viper.Reset()
|
|
defer viper.Reset()
|
|
|
|
done := make(chan bool)
|
|
numGoroutines := 5
|
|
|
|
for i := 0; i < numGoroutines; i++ {
|
|
go func(id int) {
|
|
defer func() { done <- true }()
|
|
configMap := map[string]interface{}{
|
|
"key-" + string(rune('A'+id)): "value-" + string(rune('0'+id)),
|
|
}
|
|
SetDefaultsFromConfig(configMap)
|
|
}(i)
|
|
}
|
|
|
|
for i := 0; i < numGoroutines; i++ {
|
|
<-done
|
|
}
|
|
|
|
t.Log("Concurrent SetDefaultsFromConfig completed")
|
|
}
|
|
|
|
func TestSetBaseDefaults_MultipleCalls(t *testing.T) {
|
|
// Test calling setBaseDefaults multiple times
|
|
viper.Reset()
|
|
defer viper.Reset()
|
|
|
|
err := setBaseDefaults()
|
|
if err != nil {
|
|
t.Fatalf("First call to setBaseDefaults failed: %v", err)
|
|
}
|
|
|
|
// Call again - should be idempotent
|
|
err = setBaseDefaults()
|
|
if err != nil {
|
|
t.Fatalf("Second call to setBaseDefaults failed: %v", err)
|
|
}
|
|
|
|
// Verify values are still correct
|
|
if viper.GetString(pconstants.ArgTelemetry) != constants.TelemetryInfo {
|
|
t.Error("Telemetry default changed after second call")
|
|
}
|
|
}
|
|
|
|
func TestViperReset_StateCleanup(t *testing.T) {
|
|
// Test that viper.Reset() properly cleans up state
|
|
viper.Reset()
|
|
defer viper.Reset()
|
|
|
|
// Set some values
|
|
viper.Set("test-key-1", "value1")
|
|
viper.Set("test-key-2", 42)
|
|
viper.Set("test-key-3", true)
|
|
|
|
// Verify values are set
|
|
if viper.GetString("test-key-1") != "value1" {
|
|
t.Error("Value not set correctly")
|
|
}
|
|
|
|
// Reset viper
|
|
viper.Reset()
|
|
|
|
// Verify values are cleared
|
|
if viper.GetString("test-key-1") != "" {
|
|
t.Error("BUG?: Viper.Reset() did not clear string value")
|
|
}
|
|
if viper.GetInt("test-key-2") != 0 {
|
|
t.Error("BUG?: Viper.Reset() did not clear int value")
|
|
}
|
|
if viper.GetBool("test-key-3") != false {
|
|
t.Error("BUG?: Viper.Reset() did not clear bool value")
|
|
}
|
|
}
|
|
|
|
func TestSetDefaultFromEnv_TypeConversionErrors(t *testing.T) {
|
|
// Test that type conversion errors are handled gracefully
|
|
viper.Reset()
|
|
defer viper.Reset()
|
|
|
|
tests := []struct {
|
|
name string
|
|
envValue string
|
|
varType EnvVarType
|
|
configVar string
|
|
desc string
|
|
}{
|
|
{
|
|
name: "invalid_bool",
|
|
envValue: "not-a-bool",
|
|
varType: Bool,
|
|
configVar: "test-invalid-bool",
|
|
desc: "Invalid bool value should not panic",
|
|
},
|
|
{
|
|
name: "invalid_int",
|
|
envValue: "not-a-number",
|
|
varType: Int,
|
|
configVar: "test-invalid-int",
|
|
desc: "Invalid int value should not panic",
|
|
},
|
|
{
|
|
name: "empty_string_as_bool",
|
|
envValue: "",
|
|
varType: Bool,
|
|
configVar: "test-empty-bool",
|
|
desc: "Empty string as bool should not panic",
|
|
},
|
|
{
|
|
name: "empty_string_as_int",
|
|
envValue: "",
|
|
varType: Int,
|
|
configVar: "test-empty-int",
|
|
desc: "Empty string as int should not panic",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
testKey := "TEST_TYPE_CONVERSION_" + tt.name
|
|
os.Setenv(testKey, tt.envValue)
|
|
defer os.Unsetenv(testKey)
|
|
|
|
// This should not panic
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("%s: Panicked with: %v", tt.desc, r)
|
|
}
|
|
}()
|
|
|
|
SetDefaultFromEnv(testKey, tt.configVar, tt.varType)
|
|
|
|
t.Logf("%s: Handled gracefully", tt.desc)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTildefyPaths_InvalidPaths(t *testing.T) {
|
|
// Test tildefyPaths with various invalid paths
|
|
viper.Reset()
|
|
defer viper.Reset()
|
|
|
|
tests := []struct {
|
|
name string
|
|
modLoc string
|
|
installDir string
|
|
shouldErr bool
|
|
desc string
|
|
}{
|
|
{
|
|
name: "empty_paths",
|
|
modLoc: "",
|
|
installDir: "",
|
|
shouldErr: false,
|
|
desc: "Empty paths should be handled gracefully",
|
|
},
|
|
{
|
|
name: "valid_absolute_paths",
|
|
modLoc: "/tmp/test",
|
|
installDir: "/tmp/install",
|
|
shouldErr: false,
|
|
desc: "Valid absolute paths should work",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
viper.Reset()
|
|
viper.Set(pconstants.ArgModLocation, tt.modLoc)
|
|
viper.Set(pconstants.ArgInstallDir, tt.installDir)
|
|
|
|
err := tildefyPaths()
|
|
|
|
if tt.shouldErr && err == nil {
|
|
t.Errorf("%s: Expected error but got nil", tt.desc)
|
|
}
|
|
if !tt.shouldErr && err != nil {
|
|
t.Errorf("%s: Expected no error but got: %v", tt.desc, err)
|
|
}
|
|
})
|
|
}
|
|
}
|