Add comprehensive test suite for current implementation
- Add tests for internal/config package (90.9% coverage) - Test all viperConfig getter methods - Test LoadConfig with default and environment-specific configs - Test error handling for missing config files - Add tests for internal/di package (88.1% coverage) - Test Container lifecycle (NewContainer, Start, Stop) - Test providers (ProvideConfig, ProvideLogger, CoreModule) - Test lifecycle hooks registration - Include mock implementations for testing - Add tests for internal/logger package (96.5% coverage) - Test zapLogger with JSON and console formats - Test all logging levels and methods - Test middleware (RequestIDMiddleware, LoggingMiddleware) - Test context helper functions - Include benchmark tests - Update CI workflow to skip tests when no test files exist - Add conditional test execution based on test file presence - Add timeout for test execution - Verify build when no tests are present All tests follow Go best practices with table-driven patterns, parallel execution where safe, and comprehensive coverage.
This commit is contained in:
543
internal/config/config_test.go
Normal file
543
internal/config/config_test.go
Normal file
@@ -0,0 +1,543 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.dcentral.systems/toolz/goplt/pkg/config"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func TestNewViperConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
v := viper.New()
|
||||
v.Set("test.key", "test.value")
|
||||
|
||||
cfg := NewViperConfig(v)
|
||||
|
||||
if cfg == nil {
|
||||
t.Fatal("NewViperConfig returned nil")
|
||||
}
|
||||
|
||||
// Verify it implements the interface
|
||||
var _ config.ConfigProvider = cfg
|
||||
}
|
||||
|
||||
func TestViperConfig_Get(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
key string
|
||||
setValue any
|
||||
want any
|
||||
}{
|
||||
{
|
||||
name: "string value",
|
||||
key: "test.string",
|
||||
setValue: "test",
|
||||
want: "test",
|
||||
},
|
||||
{
|
||||
name: "int value",
|
||||
key: "test.int",
|
||||
setValue: 42,
|
||||
want: 42,
|
||||
},
|
||||
{
|
||||
name: "bool value",
|
||||
key: "test.bool",
|
||||
setValue: true,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "non-existent key",
|
||||
key: "test.missing",
|
||||
setValue: nil,
|
||||
want: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
v := viper.New()
|
||||
if tt.setValue != nil {
|
||||
v.Set(tt.key, tt.setValue)
|
||||
}
|
||||
|
||||
cfg := NewViperConfig(v)
|
||||
got := cfg.Get(tt.key)
|
||||
|
||||
if got != tt.want {
|
||||
t.Errorf("Get(%q) = %v, want %v", tt.key, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestViperConfig_GetString(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
key string
|
||||
setValue string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "valid string",
|
||||
key: "test.string",
|
||||
setValue: "hello",
|
||||
want: "hello",
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
key: "test.empty",
|
||||
setValue: "",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "non-existent key",
|
||||
key: "test.missing",
|
||||
setValue: "",
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
v := viper.New()
|
||||
v.Set(tt.key, tt.setValue)
|
||||
|
||||
cfg := NewViperConfig(v)
|
||||
got := cfg.GetString(tt.key)
|
||||
|
||||
if got != tt.want {
|
||||
t.Errorf("GetString(%q) = %q, want %q", tt.key, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestViperConfig_GetInt(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
key string
|
||||
setValue int
|
||||
want int
|
||||
}{
|
||||
{
|
||||
name: "valid int",
|
||||
key: "test.int",
|
||||
setValue: 42,
|
||||
want: 42,
|
||||
},
|
||||
{
|
||||
name: "zero value",
|
||||
key: "test.zero",
|
||||
setValue: 0,
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
name: "non-existent key",
|
||||
key: "test.missing",
|
||||
setValue: 0,
|
||||
want: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
v := viper.New()
|
||||
v.Set(tt.key, tt.setValue)
|
||||
|
||||
cfg := NewViperConfig(v)
|
||||
got := cfg.GetInt(tt.key)
|
||||
|
||||
if got != tt.want {
|
||||
t.Errorf("GetInt(%q) = %d, want %d", tt.key, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestViperConfig_GetBool(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
key string
|
||||
setValue bool
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "true value",
|
||||
key: "test.bool",
|
||||
setValue: true,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "false value",
|
||||
key: "test.bool",
|
||||
setValue: false,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "non-existent key",
|
||||
key: "test.missing",
|
||||
setValue: false,
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
v := viper.New()
|
||||
v.Set(tt.key, tt.setValue)
|
||||
|
||||
cfg := NewViperConfig(v)
|
||||
got := cfg.GetBool(tt.key)
|
||||
|
||||
if got != tt.want {
|
||||
t.Errorf("GetBool(%q) = %v, want %v", tt.key, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestViperConfig_GetStringSlice(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
key string
|
||||
setValue []string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "valid slice",
|
||||
key: "test.slice",
|
||||
setValue: []string{"a", "b", "c"},
|
||||
want: []string{"a", "b", "c"},
|
||||
},
|
||||
{
|
||||
name: "empty slice",
|
||||
key: "test.empty",
|
||||
setValue: []string{},
|
||||
want: []string{},
|
||||
},
|
||||
{
|
||||
name: "non-existent key",
|
||||
key: "test.missing",
|
||||
setValue: nil,
|
||||
want: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
v := viper.New()
|
||||
v.Set(tt.key, tt.setValue)
|
||||
|
||||
cfg := NewViperConfig(v)
|
||||
got := cfg.GetStringSlice(tt.key)
|
||||
|
||||
if len(got) != len(tt.want) {
|
||||
t.Errorf("GetStringSlice(%q) length = %d, want %d", tt.key, len(got), len(tt.want))
|
||||
return
|
||||
}
|
||||
|
||||
for i := range got {
|
||||
if got[i] != tt.want[i] {
|
||||
t.Errorf("GetStringSlice(%q)[%d] = %q, want %q", tt.key, i, got[i], tt.want[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestViperConfig_GetDuration(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
key string
|
||||
setValue time.Duration
|
||||
want time.Duration
|
||||
}{
|
||||
{
|
||||
name: "valid duration",
|
||||
key: "test.duration",
|
||||
setValue: 30 * time.Second,
|
||||
want: 30 * time.Second,
|
||||
},
|
||||
{
|
||||
name: "zero duration",
|
||||
key: "test.zero",
|
||||
setValue: 0,
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
name: "non-existent key",
|
||||
key: "test.missing",
|
||||
setValue: 0,
|
||||
want: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
v := viper.New()
|
||||
v.Set(tt.key, tt.setValue)
|
||||
|
||||
cfg := NewViperConfig(v)
|
||||
got := cfg.GetDuration(tt.key)
|
||||
|
||||
if got != tt.want {
|
||||
t.Errorf("GetDuration(%q) = %v, want %v", tt.key, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestViperConfig_IsSet(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
key string
|
||||
setValue any
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "set key",
|
||||
key: "test.key",
|
||||
setValue: "value",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "non-existent key",
|
||||
key: "test.missing",
|
||||
setValue: nil,
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
v := viper.New()
|
||||
if tt.setValue != nil {
|
||||
v.Set(tt.key, tt.setValue)
|
||||
}
|
||||
|
||||
cfg := NewViperConfig(v)
|
||||
got := cfg.IsSet(tt.key)
|
||||
|
||||
if got != tt.want {
|
||||
t.Errorf("IsSet(%q) = %v, want %v", tt.key, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestViperConfig_Unmarshal(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type Config struct {
|
||||
Server struct {
|
||||
Port int `mapstructure:"port"`
|
||||
Host string `mapstructure:"host"`
|
||||
} `mapstructure:"server"`
|
||||
Logging struct {
|
||||
Level string `mapstructure:"level"`
|
||||
Format string `mapstructure:"format"`
|
||||
} `mapstructure:"logging"`
|
||||
}
|
||||
|
||||
v := viper.New()
|
||||
v.Set("server.port", 8080)
|
||||
v.Set("server.host", "localhost")
|
||||
v.Set("logging.level", "debug")
|
||||
v.Set("logging.format", "json")
|
||||
|
||||
cfg := NewViperConfig(v)
|
||||
|
||||
var result Config
|
||||
err := cfg.Unmarshal(&result)
|
||||
if err != nil {
|
||||
t.Fatalf("Unmarshal failed: %v", err)
|
||||
}
|
||||
|
||||
if result.Server.Port != 8080 {
|
||||
t.Errorf("Server.Port = %d, want 8080", result.Server.Port)
|
||||
}
|
||||
|
||||
if result.Server.Host != "localhost" {
|
||||
t.Errorf("Server.Host = %q, want %q", result.Server.Host, "localhost")
|
||||
}
|
||||
|
||||
if result.Logging.Level != "debug" {
|
||||
t.Errorf("Logging.Level = %q, want %q", result.Logging.Level, "debug")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfig_Default(t *testing.T) {
|
||||
// Note: Cannot run in parallel due to os.Chdir() being process-global
|
||||
|
||||
// Create a temporary config directory
|
||||
tmpDir := t.TempDir()
|
||||
configDir := filepath.Join(tmpDir, "config")
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
t.Fatalf("Failed to create config dir: %v", err)
|
||||
}
|
||||
|
||||
// Create a default.yaml file
|
||||
defaultYAML := `server:
|
||||
port: 8080
|
||||
host: "localhost"
|
||||
logging:
|
||||
level: "info"
|
||||
format: "json"
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(configDir, "default.yaml"), []byte(defaultYAML), 0644); err != nil {
|
||||
t.Fatalf("Failed to write default.yaml: %v", err)
|
||||
}
|
||||
|
||||
// Change to temp directory temporarily
|
||||
originalDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get current directory: %v", err)
|
||||
}
|
||||
defer os.Chdir(originalDir)
|
||||
|
||||
if err := os.Chdir(tmpDir); err != nil {
|
||||
t.Fatalf("Failed to change directory: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := LoadConfig("")
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig failed: %v", err)
|
||||
}
|
||||
|
||||
if cfg == nil {
|
||||
t.Fatal("LoadConfig returned nil")
|
||||
}
|
||||
|
||||
// Verify config values
|
||||
if cfg.GetString("server.host") != "localhost" {
|
||||
t.Errorf("server.host = %q, want %q", cfg.GetString("server.host"), "localhost")
|
||||
}
|
||||
|
||||
if cfg.GetInt("server.port") != 8080 {
|
||||
t.Errorf("server.port = %d, want 8080", cfg.GetInt("server.port"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfig_WithEnvironment(t *testing.T) {
|
||||
// Note: Cannot run in parallel due to os.Chdir() being process-global
|
||||
|
||||
// Create a temporary config directory
|
||||
tmpDir := t.TempDir()
|
||||
configDir := filepath.Join(tmpDir, "config")
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
t.Fatalf("Failed to create config dir: %v", err)
|
||||
}
|
||||
|
||||
// Create default.yaml
|
||||
defaultYAML := `server:
|
||||
port: 8080
|
||||
host: "localhost"
|
||||
logging:
|
||||
level: "info"
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(configDir, "default.yaml"), []byte(defaultYAML), 0644); err != nil {
|
||||
t.Fatalf("Failed to write default.yaml: %v", err)
|
||||
}
|
||||
|
||||
// Create development.yaml
|
||||
devYAML := `server:
|
||||
port: 3000
|
||||
logging:
|
||||
level: "debug"
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(configDir, "development.yaml"), []byte(devYAML), 0644); err != nil {
|
||||
t.Fatalf("Failed to write development.yaml: %v", err)
|
||||
}
|
||||
|
||||
// Change to temp directory temporarily
|
||||
originalDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get current directory: %v", err)
|
||||
}
|
||||
defer os.Chdir(originalDir)
|
||||
|
||||
if err := os.Chdir(tmpDir); err != nil {
|
||||
t.Fatalf("Failed to change directory: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := LoadConfig("development")
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig failed: %v", err)
|
||||
}
|
||||
|
||||
if cfg == nil {
|
||||
t.Fatal("LoadConfig returned nil")
|
||||
}
|
||||
|
||||
// Development config should override port
|
||||
if cfg.GetInt("server.port") != 3000 {
|
||||
t.Errorf("server.port = %d, want 3000", cfg.GetInt("server.port"))
|
||||
}
|
||||
|
||||
// Development config should override logging level
|
||||
if cfg.GetString("logging.level") != "debug" {
|
||||
t.Errorf("logging.level = %q, want %q", cfg.GetString("logging.level"), "debug")
|
||||
}
|
||||
|
||||
// Default host should still be present
|
||||
if cfg.GetString("server.host") != "localhost" {
|
||||
t.Errorf("server.host = %q, want %q", cfg.GetString("server.host"), "localhost")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfig_MissingDefaultFile(t *testing.T) {
|
||||
// Note: Cannot run in parallel due to os.Chdir() being process-global
|
||||
|
||||
// Create a temporary directory without config files
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Change to temp directory temporarily
|
||||
originalDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get current directory: %v", err)
|
||||
}
|
||||
defer os.Chdir(originalDir)
|
||||
|
||||
if err := os.Chdir(tmpDir); err != nil {
|
||||
t.Fatalf("Failed to change directory: %v", err)
|
||||
}
|
||||
|
||||
_, err = LoadConfig("")
|
||||
if err == nil {
|
||||
t.Fatal("LoadConfig should fail when default.yaml is missing")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user