Add comprehensive config validation and race testing target
Split config validation into validateFields() (pure logic) and validateFiles() (filesystem checks) for testability. New validations: TLS file existence, token TTL parseability/positivity, Argon2 params > 0, valid log level, non-empty listen addresses. Added 18 tests covering all validation paths. Added `make test-race` target. Resolves A-015 and A-017. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
240
internal/config/config_test.go
Normal file
240
internal/config/config_test.go
Normal file
@@ -0,0 +1,240 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// validConfig returns a Config with all fields set to valid values.
|
||||
// TLS cert/key paths must be overridden by the caller for file validation tests.
|
||||
func validConfig() Config {
|
||||
return Config{
|
||||
Server: ServerConfig{
|
||||
ListenAddr: ":8443",
|
||||
GRPCAddr: ":9443",
|
||||
TLSCert: "/tmp/cert.pem",
|
||||
TLSKey: "/tmp/key.pem",
|
||||
},
|
||||
Database: DatabaseConfig{
|
||||
Path: "/srv/eng-pad/eng-pad.db",
|
||||
},
|
||||
Auth: AuthConfig{
|
||||
TokenTTL: "24h",
|
||||
Argon2Memory: 65536,
|
||||
Argon2Time: 3,
|
||||
Argon2Threads: 4,
|
||||
},
|
||||
Log: LogConfig{
|
||||
Level: "info",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateFields_ValidConfig(t *testing.T) {
|
||||
cfg := validConfig()
|
||||
if err := cfg.validateFields(); err != nil {
|
||||
t.Fatalf("valid config should pass field validation: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateFields_MissingDatabasePath(t *testing.T) {
|
||||
cfg := validConfig()
|
||||
cfg.Database.Path = ""
|
||||
if err := cfg.validateFields(); err == nil {
|
||||
t.Fatal("expected error for missing database.path")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateFields_MissingTLSCert(t *testing.T) {
|
||||
cfg := validConfig()
|
||||
cfg.Server.TLSCert = ""
|
||||
if err := cfg.validateFields(); err == nil {
|
||||
t.Fatal("expected error for missing tls_cert")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateFields_MissingTLSKey(t *testing.T) {
|
||||
cfg := validConfig()
|
||||
cfg.Server.TLSKey = ""
|
||||
if err := cfg.validateFields(); err == nil {
|
||||
t.Fatal("expected error for missing tls_key")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateFields_MissingListenAddr(t *testing.T) {
|
||||
cfg := validConfig()
|
||||
cfg.Server.ListenAddr = ""
|
||||
if err := cfg.validateFields(); err == nil {
|
||||
t.Fatal("expected error for missing listen_addr")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateFields_MissingGRPCAddr(t *testing.T) {
|
||||
cfg := validConfig()
|
||||
cfg.Server.GRPCAddr = ""
|
||||
if err := cfg.validateFields(); err == nil {
|
||||
t.Fatal("expected error for missing grpc_addr")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateFields_InvalidTokenTTL(t *testing.T) {
|
||||
cfg := validConfig()
|
||||
cfg.Auth.TokenTTL = "not-a-duration"
|
||||
if err := cfg.validateFields(); err == nil {
|
||||
t.Fatal("expected error for invalid token_ttl")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateFields_NegativeTokenTTL(t *testing.T) {
|
||||
cfg := validConfig()
|
||||
cfg.Auth.TokenTTL = "-1h"
|
||||
if err := cfg.validateFields(); err == nil {
|
||||
t.Fatal("expected error for negative token_ttl")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateFields_ZeroTokenTTL(t *testing.T) {
|
||||
cfg := validConfig()
|
||||
cfg.Auth.TokenTTL = "0s"
|
||||
if err := cfg.validateFields(); err == nil {
|
||||
t.Fatal("expected error for zero token_ttl")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateFields_ZeroArgon2Memory(t *testing.T) {
|
||||
cfg := validConfig()
|
||||
cfg.Auth.Argon2Memory = 0
|
||||
if err := cfg.validateFields(); err == nil {
|
||||
t.Fatal("expected error for zero argon2_memory")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateFields_ZeroArgon2Time(t *testing.T) {
|
||||
cfg := validConfig()
|
||||
cfg.Auth.Argon2Time = 0
|
||||
if err := cfg.validateFields(); err == nil {
|
||||
t.Fatal("expected error for zero argon2_time")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateFields_ZeroArgon2Threads(t *testing.T) {
|
||||
cfg := validConfig()
|
||||
cfg.Auth.Argon2Threads = 0
|
||||
if err := cfg.validateFields(); err == nil {
|
||||
t.Fatal("expected error for zero argon2_threads")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateFields_InvalidLogLevel(t *testing.T) {
|
||||
cfg := validConfig()
|
||||
cfg.Log.Level = "trace"
|
||||
if err := cfg.validateFields(); err == nil {
|
||||
t.Fatal("expected error for invalid log level")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateFields_AllLogLevels(t *testing.T) {
|
||||
for _, level := range []string{"debug", "info", "warn", "error"} {
|
||||
t.Run(level, func(t *testing.T) {
|
||||
cfg := validConfig()
|
||||
cfg.Log.Level = level
|
||||
if err := cfg.validateFields(); err != nil {
|
||||
t.Fatalf("log level %q should be valid: %v", level, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateFiles_CertAndKeyExist(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
certPath := filepath.Join(dir, "cert.pem")
|
||||
keyPath := filepath.Join(dir, "key.pem")
|
||||
if err := os.WriteFile(certPath, []byte("cert"), 0600); err != nil {
|
||||
t.Fatalf("write cert: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(keyPath, []byte("key"), 0600); err != nil {
|
||||
t.Fatalf("write key: %v", err)
|
||||
}
|
||||
|
||||
cfg := validConfig()
|
||||
cfg.Server.TLSCert = certPath
|
||||
cfg.Server.TLSKey = keyPath
|
||||
if err := cfg.validateFiles(); err != nil {
|
||||
t.Fatalf("expected no error when cert/key files exist: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateFiles_MissingCertFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
keyPath := filepath.Join(dir, "key.pem")
|
||||
if err := os.WriteFile(keyPath, []byte("key"), 0600); err != nil {
|
||||
t.Fatalf("write key: %v", err)
|
||||
}
|
||||
|
||||
cfg := validConfig()
|
||||
cfg.Server.TLSCert = filepath.Join(dir, "nonexistent-cert.pem")
|
||||
cfg.Server.TLSKey = keyPath
|
||||
if err := cfg.validateFiles(); err == nil {
|
||||
t.Fatal("expected error for missing cert file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateFiles_MissingKeyFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
certPath := filepath.Join(dir, "cert.pem")
|
||||
if err := os.WriteFile(certPath, []byte("cert"), 0600); err != nil {
|
||||
t.Fatalf("write cert: %v", err)
|
||||
}
|
||||
|
||||
cfg := validConfig()
|
||||
cfg.Server.TLSCert = certPath
|
||||
cfg.Server.TLSKey = filepath.Join(dir, "nonexistent-key.pem")
|
||||
if err := cfg.validateFiles(); err == nil {
|
||||
t.Fatal("expected error for missing key file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad_ValidTOML(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
certPath := filepath.Join(dir, "cert.pem")
|
||||
keyPath := filepath.Join(dir, "key.pem")
|
||||
if err := os.WriteFile(certPath, []byte("cert"), 0600); err != nil {
|
||||
t.Fatalf("write cert: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(keyPath, []byte("key"), 0600); err != nil {
|
||||
t.Fatalf("write key: %v", err)
|
||||
}
|
||||
|
||||
toml := `
|
||||
[server]
|
||||
listen_addr = ":8443"
|
||||
grpc_addr = ":9443"
|
||||
tls_cert = "` + certPath + `"
|
||||
tls_key = "` + keyPath + `"
|
||||
|
||||
[database]
|
||||
path = "/srv/eng-pad/eng-pad.db"
|
||||
|
||||
[auth]
|
||||
token_ttl = "24h"
|
||||
argon2_memory = 65536
|
||||
argon2_time = 3
|
||||
argon2_threads = 4
|
||||
|
||||
[log]
|
||||
level = "info"
|
||||
`
|
||||
configPath := filepath.Join(dir, "config.toml")
|
||||
if err := os.WriteFile(configPath, []byte(toml), 0600); err != nil {
|
||||
t.Fatalf("write config: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := Load(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("load valid config: %v", err)
|
||||
}
|
||||
if cfg.Server.ListenAddr != ":8443" {
|
||||
t.Fatalf("got listen_addr %q, want %q", cfg.Server.ListenAddr, ":8443")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user