Files
eng-pad-server/internal/config/config_test.go
Kyle Isom 41839b7284 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>
2026-03-24 20:49:16 -07:00

241 lines
6.0 KiB
Go

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")
}
}