claw-code/.guardrails/examples/go/config_test.go

421 lines
10 KiB
Go

package main
import (
"errors"
"os"
"path/filepath"
"testing"
)
// testdataDir returns the path to the testdata directory.
func testdataDir() string {
return filepath.Join(".", "testdata")
}
// TestLoadConfig_TableDriven tests LoadConfig with various environments.
func TestLoadConfig_TableDriven(t *testing.T) {
tests := []struct {
name string
env string
wantDBHost string
wantDBName string
wantAPIURL string
wantTimeout int
}{
{
name: "production environment",
env: "production",
wantDBHost: "prod-db.example.com",
wantDBName: "production_db",
wantAPIURL: "https://api.example.com",
wantTimeout: 30,
},
{
name: "test environment",
env: "test",
wantDBHost: "localhost",
wantDBName: "test_db",
wantAPIURL: "http://localhost:8080",
wantTimeout: 5,
},
{
name: "development environment",
env: "development",
wantDBHost: "localhost",
wantDBName: "dev_db",
wantAPIURL: "http://localhost:3000",
wantTimeout: 10,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Set environment variable for this test
t.Setenv("APP_ENV", tt.env)
config, err := LoadConfig(testdataDir())
if err != nil {
t.Fatalf("LoadConfig() error = %v", err)
}
// Verify database config
if config.Database.Host != tt.wantDBHost {
t.Errorf("Database.Host = %v, want %v", config.Database.Host, tt.wantDBHost)
}
if config.Database.Name != tt.wantDBName {
t.Errorf("Database.Name = %v, want %v", config.Database.Name, tt.wantDBName)
}
// Verify services config
if config.Services.APIURL != tt.wantAPIURL {
t.Errorf("Services.APIURL = %v, want %v", config.Services.APIURL, tt.wantAPIURL)
}
if config.Services.TimeoutSeconds != tt.wantTimeout {
t.Errorf("Services.TimeoutSeconds = %v, want %v", config.Services.TimeoutSeconds, tt.wantTimeout)
}
})
}
}
// TestLoadConfig_MissingEnvironment tests behavior when APP_ENV is not set.
func TestLoadConfig_MissingEnvironment(t *testing.T) {
// Ensure APP_ENV is not set
t.Setenv("APP_ENV", "")
os.Unsetenv("APP_ENV")
_, err := LoadConfig(testdataDir())
if err == nil {
t.Fatal("LoadConfig() expected error for missing APP_ENV")
}
var missingEnvErr *ErrMissingEnvironment
if !errors.As(err, &missingEnvErr) {
t.Errorf("expected ErrMissingEnvironment, got %T: %v", err, err)
}
}
// TestLoadConfig_InvalidEnvironment tests behavior with invalid APP_ENV values.
func TestLoadConfig_InvalidEnvironment(t *testing.T) {
invalidEnvs := []string{
"staging",
"prod",
"dev",
"PRODUCTION",
"Testing",
"invalid",
"",
}
for _, env := range invalidEnvs {
t.Run("env="+env, func(t *testing.T) {
if env == "" {
os.Unsetenv("APP_ENV")
} else {
t.Setenv("APP_ENV", env)
}
_, err := LoadConfig(testdataDir())
if err == nil {
t.Fatal("LoadConfig() expected error for invalid APP_ENV")
}
// Check for appropriate error type
var invalidEnvErr *ErrInvalidEnvironment
var missingEnvErr *ErrMissingEnvironment
if env == "" {
if !errors.As(err, &missingEnvErr) {
t.Errorf("expected ErrMissingEnvironment for empty env, got %T", err)
}
} else {
if !errors.As(err, &invalidEnvErr) {
t.Errorf("expected ErrInvalidEnvironment for %q, got %T", env, err)
}
}
})
}
}
// TestLoadConfig_MissingConfigFile tests behavior when config file doesn't exist.
func TestLoadConfig_MissingConfigFile(t *testing.T) {
t.Setenv("APP_ENV", "production")
// Use a non-existent directory
_, err := LoadConfig("/nonexistent/path/to/configs")
if err == nil {
t.Fatal("LoadConfig() expected error for missing config file")
}
var missingConfigErr *ErrMissingConfig
if !errors.As(err, &missingConfigErr) {
t.Errorf("expected ErrMissingConfig, got %T: %v", err, err)
}
}
// TestLoadConfig_InvalidYAML tests behavior with malformed YAML.
func TestLoadConfig_InvalidYAML(t *testing.T) {
// Create a temporary directory with invalid YAML
tmpDir := t.TempDir()
invalidYAML := []byte("invalid: yaml: content: [unclosed")
if err := os.WriteFile(filepath.Join(tmpDir, "test.yaml"), invalidYAML, 0644); err != nil {
t.Fatalf("failed to create test file: %v", err)
}
t.Setenv("APP_ENV", "test")
_, err := LoadConfig(tmpDir)
if err == nil {
t.Fatal("LoadConfig() expected error for invalid YAML")
}
var invalidConfigErr *ErrInvalidConfig
if !errors.As(err, &invalidConfigErr) {
t.Errorf("expected ErrInvalidConfig, got %T: %v", err, err)
}
}
// TestLoadConfigWithEnv tests the explicit environment loading function.
func TestLoadConfigWithEnv(t *testing.T) {
tests := []struct {
name string
env string
wantDBName string
wantErr bool
errType interface{}
}{
{
name: "valid production",
env: "production",
wantDBName: "production_db",
wantErr: false,
},
{
name: "valid test",
env: "test",
wantDBName: "test_db",
wantErr: false,
},
{
name: "valid development",
env: "development",
wantDBName: "dev_db",
wantErr: false,
},
{
name: "empty environment",
env: "",
wantErr: true,
errType: &ErrMissingEnvironment{},
},
{
name: "invalid environment",
env: "staging",
wantErr: true,
errType: &ErrInvalidEnvironment{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config, err := LoadConfigWithEnv(testdataDir(), tt.env)
if tt.wantErr {
if err == nil {
t.Fatal("LoadConfigWithEnv() expected error")
}
return
}
if err != nil {
t.Fatalf("LoadConfigWithEnv() error = %v", err)
}
if config.Database.Name != tt.wantDBName {
t.Errorf("Database.Name = %v, want %v", config.Database.Name, tt.wantDBName)
}
})
}
}
// TestDatabaseConfig_FullValidation validates all database config fields.
func TestDatabaseConfig_FullValidation(t *testing.T) {
t.Setenv("APP_ENV", "production")
config, err := LoadConfig(testdataDir())
if err != nil {
t.Fatalf("LoadConfig() error = %v", err)
}
db := config.Database
// Validate all fields are populated
if db.Host == "" {
t.Error("Database.Host should not be empty")
}
if db.Port == 0 {
t.Error("Database.Port should not be zero")
}
if db.Name == "" {
t.Error("Database.Name should not be empty")
}
if db.SSLMode == "" {
t.Error("Database.SSLMode should not be empty")
}
if db.MaxConnections == 0 {
t.Error("Database.MaxConnections should not be zero")
}
// Validate specific production values
if db.Port != 5432 {
t.Errorf("Database.Port = %d, want 5432", db.Port)
}
if db.SSLMode != "require" {
t.Errorf("Database.SSLMode = %s, want require", db.SSLMode)
}
if db.MaxConnections != 100 {
t.Errorf("Database.MaxConnections = %d, want 100", db.MaxConnections)
}
}
// TestServicesConfig_FullValidation validates all services config fields.
func TestServicesConfig_FullValidation(t *testing.T) {
t.Setenv("APP_ENV", "production")
config, err := LoadConfig(testdataDir())
if err != nil {
t.Fatalf("LoadConfig() error = %v", err)
}
svc := config.Services
// Validate all fields are populated
if svc.APIURL == "" {
t.Error("Services.APIURL should not be empty")
}
if svc.CacheHost == "" {
t.Error("Services.CacheHost should not be empty")
}
if svc.TimeoutSeconds == 0 {
t.Error("Services.TimeoutSeconds should not be zero")
}
if svc.RetryAttempts == 0 {
t.Error("Services.RetryAttempts should not be zero")
}
// Validate specific production values
if svc.RetryAttempts != 3 {
t.Errorf("Services.RetryAttempts = %d, want 3", svc.RetryAttempts)
}
}
// TestEnvironmentIsolation verifies test and production configs are different.
func TestEnvironmentIsolation(t *testing.T) {
prodConfig, err := LoadConfigWithEnv(testdataDir(), "production")
if err != nil {
t.Fatalf("LoadConfigWithEnv(production) error = %v", err)
}
testConfig, err := LoadConfigWithEnv(testdataDir(), "test")
if err != nil {
t.Fatalf("LoadConfigWithEnv(test) error = %v", err)
}
// Database hosts must be different
if prodConfig.Database.Host == testConfig.Database.Host {
t.Error("production and test should have different database hosts")
}
// Database names must be different
if prodConfig.Database.Name == testConfig.Database.Name {
t.Error("production and test should have different database names")
}
// API URLs must be different
if prodConfig.Services.APIURL == testConfig.Services.APIURL {
t.Error("production and test should have different API URLs")
}
// SSL mode should be stricter in production
if prodConfig.Database.SSLMode == "disable" {
t.Error("production database should not have SSL disabled")
}
}
// TestValidEnvironments verifies the list of valid environments.
func TestValidEnvironments(t *testing.T) {
expected := []string{"production", "test", "development"}
if len(ValidEnvironments) != len(expected) {
t.Errorf("ValidEnvironments length = %d, want %d", len(ValidEnvironments), len(expected))
}
for _, env := range expected {
found := false
for _, valid := range ValidEnvironments {
if env == valid {
found = true
break
}
}
if !found {
t.Errorf("ValidEnvironments should contain %q", env)
}
}
}
// TestErrorMessages verifies error messages are informative.
func TestErrorMessages(t *testing.T) {
tests := []struct {
name string
err error
contains string
}{
{
name: "missing environment",
err: &ErrMissingEnvironment{},
contains: "APP_ENV",
},
{
name: "invalid environment",
err: &ErrInvalidEnvironment{Value: "staging"},
contains: "staging",
},
{
name: "missing config",
err: &ErrMissingConfig{Path: "/path/to/config.yaml"},
contains: "/path/to/config.yaml",
},
{
name: "invalid config",
err: &ErrInvalidConfig{Path: "/path/to/config.yaml", Err: errors.New("parse error")},
contains: "parse error",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
msg := tt.err.Error()
if msg == "" {
t.Error("error message should not be empty")
}
if !contains(msg, tt.contains) {
t.Errorf("error message %q should contain %q", msg, tt.contains)
}
})
}
}
// contains checks if s contains substr.
func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(substr) == 0 ||
(len(s) > 0 && len(substr) > 0 && findSubstring(s, substr)))
}
func findSubstring(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}