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>
241 lines
6.0 KiB
Go
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")
|
|
}
|
|
}
|