First commit Go counter service

This commit is contained in:
Dimitrije Dimitrijevic
2026-10-15 02:12:18 +02:00
commit c43ff17627
59 changed files with 11205 additions and 0 deletions
+288
View File
@@ -0,0 +1,288 @@
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)
}
}