Files
Ryan 71ef622b4f
Test and deploy MQTT backend / test (push) Successful in 2m8s
Test and deploy MQTT backend / deploy (push) Successful in 18s
Deploy MQTT test backend with private viewer and Gitea CI/CD
2026-10-08 14:48:53 +03:30

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