182 lines
6.1 KiB
Go
182 lines
6.1 KiB
Go
package config
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func cleanEnv(t *testing.T) {
|
|
t.Helper()
|
|
for _, v := range os.Environ() {
|
|
key, _, _ := strings.Cut(v, "=")
|
|
if strings.HasPrefix(key, Prefix) {
|
|
t.Setenv(key, "")
|
|
os.Unsetenv(key)
|
|
}
|
|
}
|
|
}
|
|
func configFile(t *testing.T, text string) string {
|
|
t.Helper()
|
|
p := filepath.Join(t.TempDir(), "config.toml")
|
|
if err := os.WriteFile(p, []byte(text), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return p
|
|
}
|
|
func TestPrecedence(t *testing.T) {
|
|
cleanEnv(t)
|
|
path := configFile(t, "[topics]\ndevice_id='file-device'\n[logging]\nlevel='debug'\n[mqtt]\nclean_start=false\n")
|
|
c, err := Load(path, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if c.Topics.DeviceID != "file-device" || c.Logging.Level != "debug" || c.MQTT.CleanStart {
|
|
t.Fatalf("TOML/default precedence failed")
|
|
}
|
|
t.Setenv(Prefix+"TOPICS__DEVICE_ID", "env-device")
|
|
t.Setenv(Prefix+"MQTT__SESSION_EXPIRY", "10m")
|
|
t.Setenv(Prefix+"MQTT__QOS", "0")
|
|
t.Setenv(Prefix+"MQTT__CLEAN_START", "true")
|
|
c, err = Load(path, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if c.Topics.DeviceID != "env-device" || c.MQTT.SessionExpiry != 10*time.Minute || c.MQTT.QoS != 0 || !c.MQTT.CleanStart {
|
|
t.Fatal("environment conversion failed")
|
|
}
|
|
c, err = Load(path, map[string]string{"topics.device_id": "cli-device"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if c.Topics.DeviceID != "cli-device" || c.Logging.Level != "debug" {
|
|
t.Fatal("explicit overrides failed")
|
|
}
|
|
}
|
|
func TestValidationAndSafeErrors(t *testing.T) {
|
|
cleanEnv(t)
|
|
cases := []struct{ name, body, key string }{
|
|
{"unknown", "[mqtt]\npasswrod='SUPER_SECRET'", "mqtt.passwrod"},
|
|
{"empty table", "[unexpected]", "unexpected"},
|
|
{"numeric duration", "[mqtt]\nkeep_alive=30", "mqtt.keep_alive"},
|
|
{"unitless duration", "[mqtt]\nkeep_alive='0'", "mqtt.keep_alive"},
|
|
{"overflow duration", "[mqtt]\nsession_expiry='999999999999999999999h'", "mqtt.session_expiry"},
|
|
{"fraction session", "[mqtt]\nsession_expiry='1.5s'", "mqtt.session_expiry"},
|
|
{"session max", "[mqtt]\nsession_expiry='4294967296s'", "mqtt.session_expiry"},
|
|
{"keepalive max", "[mqtt]\nkeep_alive='65536s'", "mqtt.keep_alive"},
|
|
{"negative", "[deadlines]\nconnect='-1s'", "deadlines.connect"},
|
|
{"qos", "[mqtt]\nqos=2", "mqtt.qos"},
|
|
{"fraction integer", "[mqtt]\nqos=0.5", "mqtt.qos"},
|
|
{"type mismatch", "[mqtt]\npassword=123", "mqtt.password"},
|
|
{"plain MQTT", "[mqtt]\nurl='tcp://localhost:1883'", "mqtt.url"},
|
|
{"userinfo", "[mqtt]\nurl='tls://user:SUPER_SECRET@localhost:8883'", "mqtt.url"},
|
|
{"port", "[mqtt]\nurl='tls://localhost:0'", "mqtt.url"},
|
|
{"wildcard", "[topics]\ndevice_id='sim/+'", "topics.device_id"},
|
|
{"production topic", "[topics]\nnamespace='device'", "topics.namespace"},
|
|
{"cert pair", "[tls]\nclient_key='SUPER_SECRET'", "tls.client_cert"},
|
|
{"secret source conflict", "[mqtt]\npassword='SUPER_SECRET'\npassword_file='secrets/password'", "mqtt.password_file"},
|
|
{"missing CA", "[tls]\nca_file='/missing/SUPER_SECRET'", "tls.ca_file"},
|
|
{"queue", "[limits]\ninbound_queue=0", "limits.inbound_queue"},
|
|
{"preview", "[limits]\npreview_bytes=20000", "limits.preview_bytes"},
|
|
{"retention", "[limits]\ndedup_retention='10s'", "limits.dedup_retention"},
|
|
{"command cap", "[deadlines]\ncommand='61s'", "deadlines.command"},
|
|
{"backoff", "[mqtt]\nreconnect_min='31s'", "mqtt.reconnect_max"},
|
|
{"public web", "[web]\naddress='0.0.0.0:8080'", "web.address"},
|
|
{"log level", "[logging]\nlevel='SUPER_SECRET'", "logging.level"},
|
|
{"syntax", "[mqtt]\npassword='SUPER_SECRET", "invalid TOML"},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, err := Load(configFile(t, tc.body), nil)
|
|
if err == nil || !strings.Contains(err.Error(), tc.key) {
|
|
t.Fatalf("expected %s error, got %v", tc.key, err)
|
|
}
|
|
if strings.Contains(err.Error(), "SUPER_SECRET") {
|
|
t.Fatal("secret leaked")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
func TestEmptyEnvAndUnknown(t *testing.T) {
|
|
cleanEnv(t)
|
|
path := configFile(t, "[mqtt]\npassword='file-secret'\n")
|
|
t.Setenv(Prefix+"MQTT__PASSWORD", "")
|
|
c, err := Load(path, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if c.MQTT.Password != "" {
|
|
t.Fatal("empty env must clear password")
|
|
}
|
|
t.Setenv(Prefix+"MQTT__CLEAN_START", "1")
|
|
if _, err = Load(path, nil); err == nil {
|
|
t.Fatal("numeric boolean accepted")
|
|
}
|
|
os.Unsetenv(Prefix + "MQTT__CLEAN_START")
|
|
t.Setenv(Prefix+"UNKNOWN", "secret")
|
|
if _, err = Load(path, nil); err == nil {
|
|
t.Fatal("unknown env accepted")
|
|
}
|
|
}
|
|
func TestFilesAndBounds(t *testing.T) {
|
|
cleanEnv(t)
|
|
if _, err := Load("/missing/config", nil); err == nil {
|
|
t.Fatal("missing config accepted")
|
|
}
|
|
c := Defaults()
|
|
c.MQTT.SessionExpiry = 4294967295 * time.Second
|
|
c.MQTT.KeepAlive = 65535 * time.Second
|
|
if err := c.Validate(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
secret := configFile(t, "test-password")
|
|
c.MQTT.PasswordFile = secret
|
|
if err := c.Validate(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
c.MQTT.PasswordFile = t.TempDir()
|
|
if err := c.Validate(); err == nil {
|
|
t.Fatal("directory accepted as secret")
|
|
}
|
|
}
|
|
|
|
func TestPlaintextRequiresOptIn(t *testing.T) {
|
|
cfg := Defaults()
|
|
cfg.MQTT.URL = "mqtt://localhost:1884"
|
|
if cfg.Validate() == nil {
|
|
t.Fatal("plaintext accepted without opt-in")
|
|
}
|
|
cfg.MQTT.AllowPlaintext = true
|
|
if err := cfg.Validate(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg.MQTT.URL = "mqtt://user:secret@localhost:1884"
|
|
if cfg.Validate() == nil {
|
|
t.Fatal("embedded credentials accepted")
|
|
}
|
|
cfg.MQTT.URL = "tls://localhost:8884"
|
|
cfg.MQTT.AllowPlaintext = false
|
|
if err := cfg.Validate(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestRoleDefaultsRemainBelowConfiguration(t *testing.T) {
|
|
defaults := Defaults()
|
|
defaults.MQTT.ClientID = "role-default"
|
|
file := filepath.Join(t.TempDir(), "role.toml")
|
|
if err := os.WriteFile(file, []byte("[mqtt]\nclient_id='file-client'\n"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg, err := LoadWithDefaults(file, nil, defaults)
|
|
if err != nil || cfg.MQTT.ClientID != "file-client" {
|
|
t.Fatal("role defaults overrode TOML")
|
|
}
|
|
cfg, err = LoadWithDefaults(file, map[string]string{"mqtt.client_id": "flag-client"}, defaults)
|
|
if err != nil || cfg.MQTT.ClientID != "flag-client" {
|
|
t.Fatal("explicit flags lost precedence")
|
|
}
|
|
}
|