289 lines
12 KiB
Go
289 lines
12 KiB
Go
package config
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// writeConfig writes body to <tmpdir>/config.toml and returns its path.
|
|
func writeConfig(t *testing.T, body string) string {
|
|
t.Helper()
|
|
path := filepath.Join(t.TempDir(), "config.toml")
|
|
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return path
|
|
}
|
|
|
|
// TOML: top-level keys must precede any [table], and a table may be defined
|
|
// only once — fixtures are therefore assembled as top + ... + redisStatic.
|
|
const (
|
|
top = "domains_cdb = \"/tmp/rspamd_domains_db\"\nauth_token = \"t\"\n"
|
|
redisStatic = "[redis]\nwrite_addr = \"127.0.0.1:6379\"\n"
|
|
minimal = top + redisStatic
|
|
)
|
|
|
|
func mustLoad(t *testing.T, body string) *Config {
|
|
t.Helper()
|
|
cfg, err := Load(writeConfig(t, body))
|
|
if err != nil {
|
|
t.Fatalf("Load: %v\n--- config ---\n%s", err, body)
|
|
}
|
|
return cfg
|
|
}
|
|
|
|
func TestLoad_Defaults(t *testing.T) {
|
|
cfg := mustLoad(t, minimal)
|
|
checks := []struct {
|
|
name string
|
|
got any
|
|
want any
|
|
}{
|
|
{"addr", cfg.Addr, ":5555"},
|
|
{"admin_addr", cfg.AdminAddr, ":9095"},
|
|
{"auth_token", cfg.AuthToken, "t"},
|
|
{"queue_size", cfg.QueueSize, 10000},
|
|
{"workers", cfg.Workers, 8},
|
|
{"max_cached_domains", cfg.MaxCachedDomains, 20000},
|
|
{"count_junk", cfg.CountJunk, false},
|
|
{"read_header_timeout", cfg.ReadHeaderTimeout, 5},
|
|
{"write_timeout", cfg.WriteTimeout, 30},
|
|
{"idle_timeout", cfg.IdleTimeout, 60},
|
|
{"handler_timeout", cfg.HandlerTimeout, 4},
|
|
{"logging.output", cfg.Logging.Output, "stderr"},
|
|
{"logging.format", cfg.Logging.Format, "console"},
|
|
{"logging.level", cfg.Logging.Level, "info"},
|
|
{"logging.counting_output", cfg.Logging.CountingOutput, "stderr"},
|
|
{"logging.counting_format", cfg.Logging.CountingFormat, "text"},
|
|
{"redis.db", cfg.Redis.DB, 0},
|
|
{"redis.pool_size", cfg.Redis.PoolSize, 32},
|
|
{"redis.timeout_ms", cfg.Redis.TimeoutMS, 500},
|
|
{"redis.sentinel_mode", cfg.Redis.SentinelMode(), false},
|
|
{"notify.timeout_seconds", cfg.Notify.TimeoutSeconds, 5},
|
|
{"redis timeout", cfg.Redis.Timeout(), 500 * time.Millisecond},
|
|
{"notify timeout", cfg.Notify.Timeout(), 5 * time.Second},
|
|
{"handler timeout", cfg.HandlerTimeoutDuration(), 4 * time.Second},
|
|
}
|
|
for _, c := range checks {
|
|
if c.got != c.want {
|
|
t.Errorf("%s = %v, want %v", c.name, c.got, c.want)
|
|
}
|
|
}
|
|
if got := cfg.Redis.ReadAddrs; len(got) != 1 || got[0] != "127.0.0.1:6379" {
|
|
t.Errorf("read_addrs = %v, want [write_addr]", got)
|
|
}
|
|
if got := cfg.ExemptSet(); len(got) != 2 || !got["support@migadu.com"] || !got["contact@migadu.com"] {
|
|
t.Errorf("ExemptSet() = %v, want support@ and contact@", got)
|
|
}
|
|
if len(cfg.AllowedPrefixes()) != 0 {
|
|
t.Errorf("AllowedPrefixes() = %v, want none", cfg.AllowedPrefixes())
|
|
}
|
|
if !cfg.Logging.CountingEnabled() {
|
|
t.Error("counting log should be enabled by default")
|
|
}
|
|
}
|
|
|
|
// The shipped example must always load, validate and produce no warnings.
|
|
func TestLoad_Example(t *testing.T) {
|
|
cfg, err := Load("../../config.toml.example")
|
|
if err != nil {
|
|
t.Fatalf("Load(config.toml.example): %v", err)
|
|
}
|
|
if !cfg.Redis.SentinelMode() || cfg.Redis.MasterName != "mymaster" || len(cfg.Redis.Sentinels) != 3 {
|
|
t.Errorf("example should be in sentinel mode with 3 sentinels: %+v", cfg.Redis)
|
|
}
|
|
if cfg.Redis.DB != 0 {
|
|
t.Errorf("example redis.db = %d, must be 0", cfg.Redis.DB)
|
|
}
|
|
if cfg.Notify.OrgURL == "" || cfg.Notify.AddressURL == "" {
|
|
t.Error("example should set org_url and address_url")
|
|
}
|
|
if cfg.Notify.DomainURL != "" {
|
|
t.Error("example should leave domain_url empty (web app D branch is a TODO)")
|
|
}
|
|
for _, w := range cfg.Warnings() {
|
|
t.Errorf("example produced warning: %s", w)
|
|
}
|
|
}
|
|
|
|
func TestLoad_MissingFile(t *testing.T) {
|
|
if _, err := Load(filepath.Join(t.TempDir(), "nope.toml")); err == nil {
|
|
t.Fatal("expected error")
|
|
}
|
|
}
|
|
|
|
func TestLoad_BadTOML(t *testing.T) {
|
|
if _, err := Load(writeConfig(t, "addr = [unterminated")); err == nil {
|
|
t.Fatal("expected error")
|
|
}
|
|
}
|
|
|
|
func TestLoad_RelativeDomainsCDBAnchoredToConfigDir(t *testing.T) {
|
|
path := writeConfig(t, "domains_cdb = \"db/rspamd_domains_db\"\nauth_token = \"t\"\n"+redisStatic)
|
|
cfg, err := Load(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := filepath.Join(filepath.Dir(path), "db/rspamd_domains_db")
|
|
if cfg.DomainsCDB != want {
|
|
t.Errorf("domains_cdb = %q, want %q", cfg.DomainsCDB, want)
|
|
}
|
|
}
|
|
|
|
func TestLoad_EnvOverrides(t *testing.T) {
|
|
t.Setenv("MIGADU_COUNTERS_AUTH_TOKEN", "env-token")
|
|
t.Setenv("MIGADU_COUNTERS_REDIS_PASSWORD", "env-redis")
|
|
t.Setenv("MIGADU_COUNTERS_NOTIFY_PASSWORD", "env-notify")
|
|
cfg := mustLoad(t, "domains_cdb = \"/tmp/x\"\nauth_token = \"file-token\"\n"+redisStatic+"password = \"file-redis\"\n[notify]\npassword = \"file-notify\"\n")
|
|
if cfg.AuthToken != "env-token" || cfg.Redis.Password != "env-redis" || cfg.Notify.Password != "env-notify" {
|
|
t.Errorf("env overrides not applied: %q %q %q", cfg.AuthToken, cfg.Redis.Password, cfg.Notify.Password)
|
|
}
|
|
}
|
|
|
|
func TestLoad_ExemptNormalized(t *testing.T) {
|
|
cfg := mustLoad(t, top+"exempt = [\" Support@Migadu.com \", \"\", \"x@y.z\"]\n"+redisStatic)
|
|
got := cfg.ExemptSet()
|
|
if len(got) != 2 || !got["support@migadu.com"] || !got["x@y.z"] {
|
|
t.Errorf("ExemptSet() = %v, want only the configured entries, normalised", got)
|
|
}
|
|
// An explicitly empty list disables the exemption entirely.
|
|
if got := mustLoad(t, top+"exempt = []\n"+redisStatic).ExemptSet(); len(got) != 0 {
|
|
t.Errorf("exempt = [] gave %v, want none", got)
|
|
}
|
|
}
|
|
|
|
func TestLoad_AllowedCIDRs(t *testing.T) {
|
|
cfg := mustLoad(t, top+"allowed_cidrs = [\"10.0.0.0/8\", \"192.168.1.5\", \"::1\"]\n"+redisStatic)
|
|
p := cfg.AllowedPrefixes()
|
|
if len(p) != 3 || p[0].Bits() != 8 || p[1].Bits() != 32 || p[2].Bits() != 128 {
|
|
t.Errorf("AllowedPrefixes() = %v", p)
|
|
}
|
|
}
|
|
|
|
func TestLoad_SentinelMode(t *testing.T) {
|
|
cfg := mustLoad(t, top+"[redis]\nmaster_name = \"mymaster\"\nsentinels = [\"10.0.0.1:26379\", \"10.0.0.2:26379\"]\n")
|
|
if !cfg.Redis.SentinelMode() || len(cfg.Redis.ReadAddrs) != 0 || cfg.Redis.WriteAddr != "" {
|
|
t.Errorf("sentinel mode config = %+v", cfg.Redis)
|
|
}
|
|
}
|
|
|
|
func TestValidate_Errors(t *testing.T) {
|
|
tests := []struct {
|
|
name, toml, wantErr string
|
|
}{
|
|
{"redis db must be 0", top + redisStatic + "db = 1\n", "redis.db"},
|
|
{"domains_cdb required", "auth_token = \"t\"\n" + redisStatic, "domains_cdb"},
|
|
{"auth_token required", "domains_cdb = \"/tmp/x\"\n" + redisStatic, "auth_token is required"},
|
|
{"auth_token blank", "domains_cdb = \"/tmp/x\"\nauth_token = \" \"\n" + redisStatic, "auth_token is required"},
|
|
{"typo auth-token", "domains_cdb = \"/tmp/x\"\nauth-token = \"s3cret\"\n" + redisStatic, "unknown config key auth-token"},
|
|
{"typo allowed_cidr", top + "allowed_cidr = [\"10.0.0.0/8\"]\n" + redisStatic, "unknown config key allowed_cidr"},
|
|
{"typo in table", top + redisStatic + "pool = 3\n", "unknown config key redis.pool"},
|
|
{"queue_size", top + "queue_size = 0\n" + redisStatic, "queue_size"},
|
|
{"workers", top + "workers = 0\n" + redisStatic, "workers"},
|
|
{"max_cached_domains", top + "max_cached_domains = -1\n" + redisStatic, "max_cached_domains"},
|
|
{"sentinel needs master_name", top + "[redis]\nsentinels = [\"10.0.0.1:26379\"]\n", "master_name"},
|
|
{"sentinel bad address", top + "[redis]\nmaster_name = \"m\"\nsentinels = [\"10.0.0.1\"]\n", "sentinels"},
|
|
{"static needs write_addr", top, "write_addr"},
|
|
{"static bad read_addr", top + redisStatic + "read_addrs = [\"nohost\"]\n", "read_addrs"},
|
|
{"bad cidr mask", top + "allowed_cidrs = [\"10.0.0.0/33\"]\n" + redisStatic, "allowed_cidrs"},
|
|
{"bad cidr text", top + "allowed_cidrs = [\"nonsense\"]\n" + redisStatic, "allowed_cidrs"},
|
|
{"notify scheme", minimal + "[notify]\norg_url = \"ftp://x/limits\"\n", "org_url"},
|
|
{"notify no scheme", minimal + "[notify]\naddress_url = \"not a url\"\n", "address_url"},
|
|
{"notify no host", minimal + "[notify]\ndomain_url = \"https:///path\"\n", "domain_url"},
|
|
{"logging format", minimal + "[logging]\nformat = \"xml\"\n", "logging.format"},
|
|
{"logging level", minimal + "[logging]\nlevel = \"loud\"\n", "logging.level"},
|
|
{"counting format", minimal + "[logging]\ncounting_format = \"csv\"\n", "counting_format"},
|
|
{"pool size", top + redisStatic + "pool_size = -1\n", "pool_size"},
|
|
{"timeout_ms", top + redisStatic + "timeout_ms = 0\n", "timeout_ms"},
|
|
{"handler_timeout", top + "handler_timeout = 0\n" + redisStatic, "handler_timeout"},
|
|
{"addr", top + "addr = \"no-port\"\n" + redisStatic, "addr"},
|
|
{"admin_addr", top + "admin_addr = \"no-port\"\n" + redisStatic, "admin_addr"},
|
|
{"notify timeout", minimal + "[notify]\ntimeout_seconds = 0\n", "timeout_seconds"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
_, err := Load(writeConfig(t, tt.toml))
|
|
if err == nil {
|
|
t.Fatalf("expected error mentioning %q", tt.wantErr)
|
|
}
|
|
if !strings.Contains(err.Error(), tt.wantErr) {
|
|
t.Errorf("error %q does not mention %q", err, tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLogging_CountingOutput(t *testing.T) {
|
|
tests := []struct {
|
|
toml string
|
|
wantOut string
|
|
wantEnabled bool
|
|
}{
|
|
{"", "stderr", true},
|
|
{"[logging]\noutput = \"/var/log/a.log\"\n", "/var/log/a.log", true}, // follows output
|
|
{"[logging]\noutput = \"/var/log/a.log\"\ncounting_output = \"/var/log/c.log\"\n", "/var/log/c.log", true},
|
|
{"[logging]\ncounting_output = \"off\"\n", "off", false},
|
|
{"[logging]\ncounting_output = \"OFF\"\n", "off", false},
|
|
}
|
|
for _, tt := range tests {
|
|
cfg := mustLoad(t, minimal+tt.toml)
|
|
if cfg.Logging.CountingOutput != tt.wantOut || cfg.Logging.CountingEnabled() != tt.wantEnabled {
|
|
t.Errorf("%q: counting_output=%q enabled=%v, want %q %v", tt.toml,
|
|
cfg.Logging.CountingOutput, cfg.Logging.CountingEnabled(), tt.wantOut, tt.wantEnabled)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWarnings_MissingPlaceholders(t *testing.T) {
|
|
cfg := mustLoad(t, top+redisStatic+`
|
|
[notify]
|
|
org_url = "https://admin.example/api/limits?level=$LEVEL&id=$ID&flow=$FLOW&period=$PERIOD&by=$ADDRESS"
|
|
address_url = "https://admin.example/api/limits?level=$LEVEL&id=$ID&flow=$FLOW&period=$PERIOD&by=$ADDRESS&count=$COUNT"
|
|
`)
|
|
w := cfg.Warnings()
|
|
if len(w) != 1 || !strings.Contains(w[0], "org_url") || !strings.Contains(w[0], "$COUNT") {
|
|
t.Errorf("Warnings() = %v, want exactly one about org_url missing $COUNT", w)
|
|
}
|
|
}
|
|
|
|
// The env var satisfies the mandatory token like the file does; a typo in the
|
|
// key never starts the daemon unauthenticated.
|
|
func TestLoad_TokenFromEnvOnly(t *testing.T) {
|
|
t.Setenv("MIGADU_COUNTERS_AUTH_TOKEN", "env-token")
|
|
cfg := mustLoad(t, "domains_cdb = \"/tmp/x\"\n"+redisStatic)
|
|
if cfg.AuthToken != "env-token" {
|
|
t.Errorf("auth_token = %q", cfg.AuthToken)
|
|
}
|
|
if _, err := Load(writeConfig(t, "domains_cdb = \"/tmp/x\"\nauth-token = \"x\"\n"+redisStatic)); err == nil {
|
|
t.Error("typo key must be fatal even when the env var supplies a token")
|
|
}
|
|
}
|
|
|
|
func TestLoad_QueueAndCache(t *testing.T) {
|
|
cfg := mustLoad(t, top+"queue_size = 50\nworkers = 2\nmax_cached_domains = 0\n"+redisStatic)
|
|
if cfg.QueueSize != 50 || cfg.Workers != 2 || cfg.MaxCachedDomains != 0 {
|
|
t.Errorf("queue/cache = %d %d %d", cfg.QueueSize, cfg.Workers, cfg.MaxCachedDomains)
|
|
}
|
|
}
|
|
|
|
func TestDiscover(t *testing.T) {
|
|
if got, err := Discover("/explicit/path.toml"); err != nil || got != "/explicit/path.toml" {
|
|
t.Errorf("Discover(explicit) = %q, %v", got, err)
|
|
}
|
|
// With no default file present on the test machine, Discover must explain where it looked.
|
|
paths := DefaultPaths
|
|
DefaultPaths = []string{filepath.Join(t.TempDir(), "missing.toml")}
|
|
defer func() { DefaultPaths = paths }()
|
|
if _, err := Discover(""); err == nil || !strings.Contains(err.Error(), "missing.toml") {
|
|
t.Errorf("Discover(\"\") err = %v, want one listing the searched paths", err)
|
|
}
|
|
found := writeConfig(t, minimal)
|
|
DefaultPaths = []string{DefaultPaths[0], found}
|
|
if got, err := Discover(""); err != nil || got != found {
|
|
t.Errorf("Discover(\"\") = %q, %v; want %q", got, err, found)
|
|
}
|
|
}
|