重构:拆出 internal/config 与 internal/store 子包

把工程根目录中和"配置加载"、"数据存储"两个领域相关的全部 Go 文件迁到
internal/ 下的子包,按功能分组的第一阶段成果。

internal/config/
- 从 config.go 抽出 Config / MQTTConfig / WebConfig / DatabaseConfig 等
  类型并大写导出,函数改名 Default/Load/Write/Validate/BuildTLS 等。
- 测试随被测代码迁移成 package config 的内部测试。
- 根目录留 config.go 作桥接:用 type alias 让旧的小写名(config /
  mqttConfig / databaseConfig 等)继续可用,避免修改 30+ 处调用方。

internal/store/
- 把 db.go、store_query.go、db_write_queue.go 与 13 个 *_store.go 一并
  迁入;26 个 *Record 类型与 store 同包以避免循环依赖。
- store -> Store;50+ 标识符从小写未导出改为大写导出(包括 record、
  ListOptions、错误变量、bot/llm/runtime 常量、helpers 等)。
- 新增 DB() / Driver() 访问器供 ai 子系统使用,避免直接访问私有字段。
- bot_pki_store.go 独立出来,把 PKI 解密所需的 store 方法集中归类。
- helpers.go 提供 hashPassword / uint32FromRecord / printJSON 等以前在
  其他根目录文件中的辅助;test_helpers_test.go 提供 verifyPassword
  与 publicMapTileSourceDTO 让测试可以本地运行而不依赖 main 包。

根目录新增:
- store_bridge.go:完整 type-alias / 函数包装层,把 internal/store 的
  导出名映射回旧的小写名,让 admin_*_routes.go、web.go、bot_service.go
  等仍未迁出的文件继续编译。后续步骤把它们迁到各自领域包后可逐步删除。
- test_helpers_test.go:根目录测试沿用 openTestStore 的入口。

go build ./... 与 go test ./... 全部通过;测试数量与重构前一致。

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-18 13:56:36 +08:00
co-authored by Claude
parent 6f6b06e37d
commit eff4972668
32 changed files with 1759 additions and 1462 deletions
+549
View File
@@ -0,0 +1,549 @@
package config
import (
cryptotls "crypto/tls"
"fmt"
"os"
"path/filepath"
"runtime"
"gopkg.in/yaml.v3"
)
const FileName = "config.yaml"
const (
DriverSQLite = "sqlite"
DriverMySQL = "mysql"
)
type Config struct {
MQTT MQTTConfig `yaml:"mqtt"`
Meshtastic MeshtasticConfig `yaml:"meshtastic"`
Database DatabaseConfig `yaml:"database"`
Web WebConfig `yaml:"web"`
AI AIConfig `yaml:"ai"`
DataDir string `yaml:"data_dir"`
Key []byte `yaml:"-"`
}
type MQTTConfig struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
TLS TLSConfig `yaml:"tls"`
}
type TLSConfig struct {
Enabled bool `yaml:"enabled"`
CertFile string `yaml:"cert_file"`
KeyFile string `yaml:"key_file"`
}
type MeshtasticConfig struct {
PSK string `yaml:"psk"`
}
type DatabaseConfig struct {
Driver string `yaml:"driver"`
SQLite SQLiteConfig `yaml:"sqlite"`
MySQL MySQLConfig `yaml:"mysql"`
}
type SQLiteConfig struct {
Path string `yaml:"path"`
}
type MySQLConfig struct {
DSN string `yaml:"dsn"`
}
type WebConfig struct {
Enabled bool `yaml:"enabled"`
PortEnabled bool `yaml:"port_enabled"`
SocketEnabled bool `yaml:"socket_enabled"`
Host string `yaml:"host"`
Port int `yaml:"port"`
SocketPath string `yaml:"socket_path"`
StaticDir string `yaml:"static_dir"`
MapTileCacheDir string `yaml:"map_tile_cache_dir"`
Admin WebAdminConfig `yaml:"admin"`
}
type WebAdminConfig struct {
Username string `yaml:"username"`
Password string `yaml:"password"`
SessionSecret string `yaml:"session_secret"`
SessionSecure bool `yaml:"session_secure"`
}
type AIConfig struct {
Enabled bool `yaml:"enabled"`
}
type rawConfig struct {
MQTT *rawMQTTConfig `yaml:"mqtt"`
Meshtastic *rawMeshtasticConfig `yaml:"meshtastic"`
Database *rawDatabaseConfig `yaml:"database"`
Web *rawWebConfig `yaml:"web"`
AI *rawAIConfig `yaml:"ai"`
DataDir *string `yaml:"data_dir"`
}
type rawAIConfig struct {
Enabled *bool `yaml:"enabled"`
}
type rawMQTTConfig struct {
Host *string `yaml:"host"`
Port *int `yaml:"port"`
TLS *rawTLSConfig `yaml:"tls"`
}
type rawTLSConfig struct {
Enabled *bool `yaml:"enabled"`
CertFile *string `yaml:"cert_file"`
KeyFile *string `yaml:"key_file"`
}
type rawMeshtasticConfig struct {
PSK *string `yaml:"psk"`
}
type rawDatabaseConfig struct {
Driver *string `yaml:"driver"`
SQLite *rawSQLiteConfig `yaml:"sqlite"`
MySQL *rawMySQLConfig `yaml:"mysql"`
}
type rawSQLiteConfig struct {
Path *string `yaml:"path"`
}
type rawMySQLConfig struct {
DSN *string `yaml:"dsn"`
}
type rawWebConfig struct {
Enabled *bool `yaml:"enabled"`
PortEnabled *bool `yaml:"port_enabled"`
SocketEnabled *bool `yaml:"socket_enabled"`
Host *string `yaml:"host"`
Port *int `yaml:"port"`
SocketPath *string `yaml:"socket_path"`
StaticDir *string `yaml:"static_dir"`
MapTileCacheDir *string `yaml:"map_tile_cache_dir"`
Admin *rawWebAdminConfig `yaml:"admin"`
}
type rawWebAdminConfig struct {
Username *string `yaml:"username"`
Password *string `yaml:"password"`
SessionSecret *string `yaml:"session_secret"`
SessionSecure *bool `yaml:"session_secure"`
}
// Default 返回内置默认配置。
func Default() *Config {
return &Config{
MQTT: MQTTConfig{
Host: "0.0.0.0",
Port: 1883,
TLS: TLSConfig{
Enabled: false,
CertFile: "",
KeyFile: "",
},
},
Meshtastic: MeshtasticConfig{
PSK: "AQ==",
},
Database: DatabaseConfig{
Driver: DriverSQLite,
SQLite: SQLiteConfig{Path: defaultSQLitePath()},
MySQL: MySQLConfig{DSN: ""},
},
Web: WebConfig{
Enabled: true,
PortEnabled: true,
SocketEnabled: defaultWebSocketPath() != "",
Host: "0.0.0.0",
Port: 8080,
SocketPath: defaultWebSocketPath(),
StaticDir: "./dist",
MapTileCacheDir: defaultMapTileCacheDir(),
Admin: WebAdminConfig{
Username: "admin",
Password: "admin",
SessionSecret: "",
SessionSecure: false,
},
},
AI: AIConfig{
Enabled: false,
},
DataDir: defaultDataDir(),
}
}
// DefaultDir 根据操作系统返回配置目录。
func DefaultDir() string {
return defaultConfigDirForGOOS(runtime.GOOS)
}
func defaultConfigDirForGOOS(goos string) string {
if useRelativeDefaultPath(goos) {
return filepath.Join(".", "win", "etc", "mesh_mqtt_go")
}
return filepath.Join(string(filepath.Separator), "etc", "mesh_mqtt_go")
}
func useRelativeDefaultPath(goos string) bool {
return goos == "windows" || goos == "darwin"
}
// DefaultPath 返回默认配置文件路径。
func DefaultPath() string {
return filepath.Join(DefaultDir(), FileName)
}
func defaultSQLitePath() string {
return defaultSQLitePathForGOOS(runtime.GOOS)
}
func defaultWebSocketPath() string {
return defaultWebSocketPathForGOOS(runtime.GOOS)
}
func defaultMapTileCacheDir() string {
return defaultMapTileCacheDirForGOOS(runtime.GOOS)
}
func defaultMapTileCacheDirForGOOS(goos string) string {
if useRelativeDefaultPath(goos) {
return filepath.Join(".", "win", "srv", "mesh_mqtt_go")
}
return filepath.Join(string(filepath.Separator), "srv", "mesh_mqtt_go")
}
func defaultWebSocketPathForGOOS(goos string) string {
if goos == "windows" {
return ""
}
if useRelativeDefaultPath(goos) {
return filepath.Join(".", "win", "opt", "mesh_mqtt_go", "web.sock")
}
return filepath.Join(string(filepath.Separator), "opt", "mesh_mqtt_go", "web.sock")
}
func ClearWebSocketPathOnUnsupportedGOOS(cfg *Config, goos string) bool {
if goos != "windows" {
return false
}
changed := false
if cfg.Web.SocketPath != "" {
cfg.Web.SocketPath = ""
changed = true
}
if cfg.Web.SocketEnabled {
cfg.Web.SocketEnabled = false
changed = true
}
return changed
}
func defaultSQLitePathForGOOS(goos string) string {
if useRelativeDefaultPath(goos) {
return filepath.Join(".", "win", "etc", "mesh_mqtt_go", "mesh_mqtt_go.db")
}
return filepath.Join(string(filepath.Separator), "srv", "mesh_mqtt_go", "mesh_mqtt_go.db")
}
func defaultDataDir() string {
return defaultDataDirForGOOS(runtime.GOOS)
}
func defaultDataDirForGOOS(goos string) string {
if useRelativeDefaultPath(goos) {
return filepath.Join(".", "win", "var", "lib", "mesh_mqtt_go")
}
return filepath.Join(string(filepath.Separator), "var", "lib", "mesh_mqtt_go")
}
// Load 加载配置文件;文件不存在时生成,字段缺失时自动补全并写回。
func Load(path string) (*Config, error) {
if path == "" {
path = DefaultPath()
}
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return nil, fmt.Errorf("create config directory %s: %w", filepath.Dir(path), err)
}
if _, err := os.Stat(path); err != nil {
if !os.IsNotExist(err) {
return nil, fmt.Errorf("stat config file %s: %w", path, err)
}
cfg := Default()
if err := Write(path, cfg); err != nil {
return nil, err
}
return cfg, nil
}
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read config file %s: %w", path, err)
}
var raw rawConfig
if err := yaml.Unmarshal(data, &raw); err != nil {
return nil, fmt.Errorf("parse config file %s: %w", path, err)
}
cfg, changed := normalize(raw)
if ClearWebSocketPathOnUnsupportedGOOS(cfg, runtime.GOOS) {
changed = true
}
if err := Validate(cfg); err != nil {
return nil, err
}
if changed {
if err := Write(path, cfg); err != nil {
return nil, err
}
}
return cfg, nil
}
// normalize 将原始配置合并到默认配置,并标记是否补齐了缺失项。
func normalize(raw rawConfig) (*Config, bool) {
cfg := Default()
changed := false
if raw.MQTT == nil {
changed = true
} else {
if raw.MQTT.Host == nil {
changed = true
} else {
cfg.MQTT.Host = *raw.MQTT.Host
}
if raw.MQTT.Port == nil {
changed = true
} else {
cfg.MQTT.Port = *raw.MQTT.Port
}
if raw.MQTT.TLS == nil {
changed = true
} else {
if raw.MQTT.TLS.Enabled == nil {
changed = true
} else {
cfg.MQTT.TLS.Enabled = *raw.MQTT.TLS.Enabled
}
if raw.MQTT.TLS.CertFile == nil {
changed = true
} else {
cfg.MQTT.TLS.CertFile = *raw.MQTT.TLS.CertFile
}
if raw.MQTT.TLS.KeyFile == nil {
changed = true
} else {
cfg.MQTT.TLS.KeyFile = *raw.MQTT.TLS.KeyFile
}
}
}
if raw.Meshtastic == nil {
changed = true
} else if raw.Meshtastic.PSK == nil {
changed = true
} else {
cfg.Meshtastic.PSK = *raw.Meshtastic.PSK
}
if raw.Database == nil {
changed = true
} else {
if raw.Database.Driver == nil {
changed = true
} else {
cfg.Database.Driver = *raw.Database.Driver
}
if raw.Database.SQLite == nil {
changed = true
} else if raw.Database.SQLite.Path == nil {
changed = true
} else {
cfg.Database.SQLite.Path = *raw.Database.SQLite.Path
}
if raw.Database.MySQL == nil {
changed = true
} else if raw.Database.MySQL.DSN == nil {
changed = true
} else {
cfg.Database.MySQL.DSN = *raw.Database.MySQL.DSN
}
}
if raw.Web == nil {
changed = true
} else {
if raw.Web.Enabled == nil {
changed = true
} else {
cfg.Web.Enabled = *raw.Web.Enabled
}
if raw.Web.PortEnabled == nil {
changed = true
} else {
cfg.Web.PortEnabled = *raw.Web.PortEnabled
}
if raw.Web.SocketEnabled == nil {
changed = true
} else {
cfg.Web.SocketEnabled = *raw.Web.SocketEnabled
}
if raw.Web.Host == nil {
changed = true
} else {
cfg.Web.Host = *raw.Web.Host
}
if raw.Web.Port == nil {
changed = true
} else {
cfg.Web.Port = *raw.Web.Port
}
if raw.Web.SocketPath == nil {
changed = true
} else {
cfg.Web.SocketPath = *raw.Web.SocketPath
}
if raw.Web.StaticDir == nil {
changed = true
} else {
cfg.Web.StaticDir = *raw.Web.StaticDir
}
if raw.Web.MapTileCacheDir == nil {
changed = true
} else {
cfg.Web.MapTileCacheDir = *raw.Web.MapTileCacheDir
}
if raw.Web.Admin == nil {
changed = true
} else {
if raw.Web.Admin.Username == nil {
changed = true
} else {
cfg.Web.Admin.Username = *raw.Web.Admin.Username
}
if raw.Web.Admin.Password == nil {
changed = true
} else {
cfg.Web.Admin.Password = *raw.Web.Admin.Password
}
if raw.Web.Admin.SessionSecret == nil {
changed = true
} else {
cfg.Web.Admin.SessionSecret = *raw.Web.Admin.SessionSecret
}
if raw.Web.Admin.SessionSecure == nil {
changed = true
} else {
cfg.Web.Admin.SessionSecure = *raw.Web.Admin.SessionSecure
}
}
}
if raw.AI == nil {
changed = true
} else {
if raw.AI.Enabled == nil {
changed = true
} else {
cfg.AI.Enabled = *raw.AI.Enabled
}
}
if raw.DataDir == nil {
changed = true
} else {
cfg.DataDir = *raw.DataDir
}
return cfg, changed
}
func Validate(cfg *Config) error {
if cfg.MQTT.Port <= 0 || cfg.MQTT.Port > 65535 {
return fmt.Errorf("invalid mqtt port %d: must be 1-65535", cfg.MQTT.Port)
}
switch cfg.Database.Driver {
case DriverSQLite:
if cfg.Database.SQLite.Path == "" {
return fmt.Errorf("database.sqlite.path is required when database.driver is sqlite")
}
case DriverMySQL:
if cfg.Database.MySQL.DSN == "" {
return fmt.Errorf("database.mysql.dsn is required when database.driver is mysql")
}
default:
return fmt.Errorf("invalid database.driver %q: must be sqlite or mysql", cfg.Database.Driver)
}
if cfg.Web.Enabled {
if !cfg.Web.PortEnabled && !cfg.Web.SocketEnabled {
return fmt.Errorf("web.port_enabled and web.socket_enabled cannot both be false when web is enabled")
}
if cfg.Web.PortEnabled && (cfg.Web.Port <= 0 || cfg.Web.Port > 65535) {
return fmt.Errorf("invalid web port %d: must be 1-65535", cfg.Web.Port)
}
if cfg.Web.SocketEnabled && cfg.Web.SocketPath == "" {
return fmt.Errorf("web.socket_path is required when web.socket_enabled is true")
}
if cfg.Web.StaticDir == "" {
return fmt.Errorf("web.static_dir is required when web is enabled")
}
if cfg.Web.MapTileCacheDir == "" {
return fmt.Errorf("web.map_tile_cache_dir is required when web is enabled")
}
if cfg.Web.Admin.Username == "" {
return fmt.Errorf("web.admin.username is required when web is enabled")
}
if cfg.Web.Admin.Password == "" {
return fmt.Errorf("web.admin.password is required when web is enabled")
}
}
return nil
}
func Write(path string, cfg *Config) error {
data, err := yaml.Marshal(cfg)
if err != nil {
return fmt.Errorf("encode config file %s: %w", path, err)
}
if err := os.WriteFile(path, data, 0644); err != nil {
return fmt.Errorf("write config file %s: %w", path, err)
}
return nil
}
// BuildTLS 根据配置构造 mochi listener 使用的 TLS 设置。
func BuildTLS(cfg TLSConfig) (*cryptotls.Config, error) {
if !cfg.Enabled {
return nil, nil
}
if cfg.CertFile == "" {
return nil, fmt.Errorf("mqtt tls cert_file is required when tls is enabled")
}
if cfg.KeyFile == "" {
return nil, fmt.Errorf("mqtt tls key_file is required when tls is enabled")
}
cert, err := cryptotls.LoadX509KeyPair(cfg.CertFile, cfg.KeyFile)
if err != nil {
return nil, fmt.Errorf("load mqtt tls certificate: %w", err)
}
return &cryptotls.Config{
MinVersion: cryptotls.VersionTLS12,
Certificates: []cryptotls.Certificate{cert},
}, nil
}
+358
View File
@@ -0,0 +1,358 @@
package config
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestLoadConfigCreatesDefaultFile(t *testing.T) {
path := filepath.Join(t.TempDir(), "mesh_mqtt_go", FileName)
cfg, err := Load(path)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if cfg.MQTT.Host != "0.0.0.0" {
t.Fatalf("host = %q, want 0.0.0.0", cfg.MQTT.Host)
}
if cfg.MQTT.Port != 1883 {
t.Fatalf("port = %d, want 1883", cfg.MQTT.Port)
}
if cfg.MQTT.TLS.Enabled {
t.Fatalf("tls enabled = true, want false")
}
if cfg.Meshtastic.PSK != "AQ==" {
t.Fatalf("psk = %q, want AQ==", cfg.Meshtastic.PSK)
}
if cfg.Database.Driver != "sqlite" {
t.Fatalf("database driver = %q, want sqlite", cfg.Database.Driver)
}
if cfg.Database.SQLite.Path == "" {
t.Fatalf("sqlite path is empty")
}
if !cfg.Web.Enabled {
t.Fatalf("web enabled = false, want true")
}
if !cfg.Web.PortEnabled {
t.Fatalf("web port enabled = false, want true")
}
wantSocketEnabled := defaultWebSocketPath() != ""
if cfg.Web.SocketEnabled != wantSocketEnabled {
t.Fatalf("web socket enabled = %t, want %t", cfg.Web.SocketEnabled, wantSocketEnabled)
}
if cfg.Web.Port != 8080 {
t.Fatalf("web port = %d, want 8080", cfg.Web.Port)
}
if cfg.Web.SocketPath != defaultWebSocketPath() {
t.Fatalf("web socket path = %q, want %q", cfg.Web.SocketPath, defaultWebSocketPath())
}
if cfg.Web.StaticDir != "./dist" {
t.Fatalf("web static dir = %q, want ./dist", cfg.Web.StaticDir)
}
if cfg.Web.MapTileCacheDir != defaultMapTileCacheDir() {
t.Fatalf("web map tile cache dir = %q, want %q", cfg.Web.MapTileCacheDir, defaultMapTileCacheDir())
}
if _, err := os.Stat(path); err != nil {
t.Fatalf("default config was not written: %v", err)
}
}
func TestLoadConfigFillsMissingFields(t *testing.T) {
path := filepath.Join(t.TempDir(), "mesh_mqtt_go", FileName)
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte("mqtt:\n port: 1884\n"), 0644); err != nil {
t.Fatal(err)
}
cfg, err := Load(path)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if cfg.MQTT.Port != 1884 {
t.Fatalf("port = %d, want 1884", cfg.MQTT.Port)
}
if cfg.MQTT.Host != "0.0.0.0" {
t.Fatalf("host = %q, want 0.0.0.0", cfg.MQTT.Host)
}
if cfg.Meshtastic.PSK != "AQ==" {
t.Fatalf("psk = %q, want AQ==", cfg.Meshtastic.PSK)
}
if cfg.Database.Driver != "sqlite" {
t.Fatalf("database driver = %q, want sqlite", cfg.Database.Driver)
}
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
text := string(data)
for _, want := range []string{"host:", "tls:", "enabled:", "cert_file:", "key_file:", "meshtastic:", "psk:", "database:", "driver:", "sqlite:", "mysql:", "dsn:", "web:", "port_enabled:", "socket_enabled:", "port:", "socket_path:", "static_dir:", "map_tile_cache_dir:"} {
if !strings.Contains(text, want) {
t.Fatalf("completed config missing %q in:\n%s", want, text)
}
}
}
func TestLoadConfigPreservesExplicitFalse(t *testing.T) {
path := filepath.Join(t.TempDir(), "mesh_mqtt_go", FileName)
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
t.Fatal(err)
}
content := "mqtt:\n host: 127.0.0.1\n port: 1885\n tls:\n enabled: false\n cert_file: cert.pem\n key_file: key.pem\nmeshtastic:\n psk: AQ==\ndatabase:\n driver: sqlite\n sqlite:\n path: test.db\n mysql:\n dsn: \"\"\n"
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
t.Fatal(err)
}
cfg, err := Load(path)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if cfg.MQTT.TLS.Enabled {
t.Fatalf("tls enabled = true, want explicit false")
}
if cfg.MQTT.TLS.CertFile != "cert.pem" || cfg.MQTT.TLS.KeyFile != "key.pem" {
t.Fatalf("tls paths = %q/%q, want cert.pem/key.pem", cfg.MQTT.TLS.CertFile, cfg.MQTT.TLS.KeyFile)
}
}
func TestLoadConfigPreservesExplicitWebFalse(t *testing.T) {
path := filepath.Join(t.TempDir(), "mesh_mqtt_go", FileName)
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
t.Fatal(err)
}
content := "web:\n enabled: false\n port_enabled: false\n socket_enabled: false\n host: 127.0.0.1\n port: 8081\n static_dir: ./public\n"
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
t.Fatal(err)
}
cfg, err := Load(path)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if cfg.Web.Enabled {
t.Fatalf("web enabled = true, want explicit false")
}
if cfg.Web.PortEnabled || cfg.Web.SocketEnabled {
t.Fatalf("web listener enabled = %t/%t, want explicit false/false", cfg.Web.PortEnabled, cfg.Web.SocketEnabled)
}
if cfg.Web.Host != "127.0.0.1" || cfg.Web.Port != 8081 || cfg.Web.StaticDir != "./public" {
t.Fatalf("web config = %#v", cfg.Web)
}
}
func TestLoadConfigMalformedYAMLDoesNotOverwrite(t *testing.T) {
path := filepath.Join(t.TempDir(), "mesh_mqtt_go", FileName)
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
t.Fatal(err)
}
content := "mqtt:\n port: [\n"
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
t.Fatal(err)
}
_, err := Load(path)
if err == nil {
t.Fatalf("Load() error = nil, want parse error")
}
data, readErr := os.ReadFile(path)
if readErr != nil {
t.Fatal(readErr)
}
if string(data) != content {
t.Fatalf("malformed config was overwritten: %q", string(data))
}
}
func TestDefaultConfigDirForGOOS(t *testing.T) {
wantRelative := filepath.Join(".", "win", "etc", "mesh_mqtt_go")
for _, goos := range []string{"windows", "darwin"} {
path := defaultConfigDirForGOOS(goos)
if path != wantRelative {
t.Fatalf("%s config dir = %q, want %q", goos, path, wantRelative)
}
}
linuxPath := defaultConfigDirForGOOS("linux")
wantLinux := filepath.Join(string(filepath.Separator), "etc", "mesh_mqtt_go")
if linuxPath != wantLinux {
t.Fatalf("linux config dir = %q, want %q", linuxPath, wantLinux)
}
}
func TestDefaultMapTileCacheDirForGOOS(t *testing.T) {
wantRelative := filepath.Join(".", "win", "srv", "mesh_mqtt_go")
for _, goos := range []string{"windows", "darwin"} {
path := defaultMapTileCacheDirForGOOS(goos)
if path != wantRelative {
t.Fatalf("%s map tile cache dir = %q, want %q", goos, path, wantRelative)
}
}
linuxPath := defaultMapTileCacheDirForGOOS("linux")
wantLinux := filepath.Join(string(filepath.Separator), "srv", "mesh_mqtt_go")
if linuxPath != wantLinux {
t.Fatalf("linux map tile cache dir = %q, want %q", linuxPath, wantLinux)
}
}
func TestDefaultWebSocketPathForGOOS(t *testing.T) {
if windowsPath := defaultWebSocketPathForGOOS("windows"); windowsPath != "" {
t.Fatalf("windows web socket path = %q, want empty", windowsPath)
}
darwinPath := defaultWebSocketPathForGOOS("darwin")
wantDarwin := filepath.Join(".", "win", "opt", "mesh_mqtt_go", "web.sock")
if darwinPath != wantDarwin {
t.Fatalf("darwin web socket path = %q, want %q", darwinPath, wantDarwin)
}
linuxPath := defaultWebSocketPathForGOOS("linux")
want := filepath.Join(string(filepath.Separator), "opt", "mesh_mqtt_go", "web.sock")
if linuxPath != want {
t.Fatalf("linux web socket path = %q, want %q", linuxPath, want)
}
}
func TestClearWebSocketPathOnUnsupportedGOOS(t *testing.T) {
cfg := Default()
cfg.Web.SocketPath = filepath.Join(".", "win", "opt", "mesh_mqtt_go", "web.sock")
if !ClearWebSocketPathOnUnsupportedGOOS(cfg, "windows") {
t.Fatalf("ClearWebSocketPathOnUnsupportedGOOS() = false, want true")
}
if cfg.Web.SocketPath != "" {
t.Fatalf("windows web socket path = %q, want empty", cfg.Web.SocketPath)
}
if cfg.Web.SocketEnabled {
t.Fatalf("windows web socket enabled = true, want false")
}
cfg.Web.SocketPath = "/opt/mesh_mqtt_go/web.sock"
if ClearWebSocketPathOnUnsupportedGOOS(cfg, "linux") {
t.Fatalf("linux ClearWebSocketPathOnUnsupportedGOOS() = true, want false")
}
if cfg.Web.SocketPath == "" {
t.Fatalf("linux web socket path was cleared")
}
}
func TestDefaultSQLitePathForGOOS(t *testing.T) {
wantRelative := filepath.Join(".", "win", "etc", "mesh_mqtt_go", "mesh_mqtt_go.db")
for _, goos := range []string{"windows", "darwin"} {
path := defaultSQLitePathForGOOS(goos)
if path != wantRelative {
t.Fatalf("%s sqlite path = %q, want %q", goos, path, wantRelative)
}
}
linuxPath := defaultSQLitePathForGOOS("linux")
want := filepath.Join(string(filepath.Separator), "srv", "mesh_mqtt_go", "mesh_mqtt_go.db")
if linuxPath != want {
t.Fatalf("linux sqlite path = %q, want %q", linuxPath, want)
}
}
func TestValidateConfigDatabase(t *testing.T) {
cfg := Default()
cfg.Database.Driver = "postgres"
if err := Validate(cfg); err == nil || !strings.Contains(err.Error(), "database.driver") {
t.Fatalf("invalid driver error = %v, want database.driver error", err)
}
cfg = Default()
cfg.Database.SQLite.Path = ""
if err := Validate(cfg); err == nil || !strings.Contains(err.Error(), "database.sqlite.path") {
t.Fatalf("missing sqlite path error = %v, want database.sqlite.path error", err)
}
cfg = Default()
cfg.Database.Driver = "mysql"
cfg.Database.MySQL.DSN = ""
if err := Validate(cfg); err == nil || !strings.Contains(err.Error(), "database.mysql.dsn") {
t.Fatalf("missing mysql dsn error = %v, want database.mysql.dsn error", err)
}
}
func TestValidateConfigWeb(t *testing.T) {
cfg := Default()
cfg.Web.Port = 0
if err := Validate(cfg); err == nil || !strings.Contains(err.Error(), "web port") {
t.Fatalf("invalid web port error = %v, want web port error", err)
}
cfg = Default()
cfg.Web.PortEnabled = false
cfg.Web.Port = 0
if err := Validate(cfg); err != nil {
t.Fatalf("disabled web port with invalid port error = %v, want nil", err)
}
cfg = Default()
cfg.Web.SocketEnabled = false
cfg.Web.SocketPath = ""
if err := Validate(cfg); err != nil {
t.Fatalf("disabled web socket with empty path error = %v, want nil", err)
}
cfg = Default()
cfg.Web.PortEnabled = false
cfg.Web.SocketEnabled = true
cfg.Web.SocketPath = ""
if err := Validate(cfg); err == nil || !strings.Contains(err.Error(), "web.socket_path") {
t.Fatalf("missing web socket path error = %v, want web.socket_path error", err)
}
cfg = Default()
cfg.Web.PortEnabled = false
cfg.Web.SocketEnabled = false
if err := Validate(cfg); err == nil || !strings.Contains(err.Error(), "web.port_enabled") {
t.Fatalf("disabled web listeners error = %v, want web.port_enabled error", err)
}
cfg = Default()
cfg.Web.StaticDir = ""
if err := Validate(cfg); err == nil || !strings.Contains(err.Error(), "web.static_dir") {
t.Fatalf("missing web static dir error = %v, want web.static_dir error", err)
}
cfg = Default()
cfg.Web.MapTileCacheDir = ""
if err := Validate(cfg); err == nil || !strings.Contains(err.Error(), "web.map_tile_cache_dir") {
t.Fatalf("missing map tile cache dir error = %v, want web.map_tile_cache_dir error", err)
}
cfg = Default()
cfg.Web.Enabled = false
cfg.Web.PortEnabled = false
cfg.Web.SocketEnabled = false
cfg.Web.Port = 0
cfg.Web.StaticDir = ""
if err := Validate(cfg); err != nil {
t.Fatalf("disabled web validate error = %v, want nil", err)
}
}
func TestBuildTLSConfigDisabled(t *testing.T) {
cfg, err := BuildTLS(TLSConfig{})
if err != nil {
t.Fatalf("BuildTLS() error = %v", err)
}
if cfg != nil {
t.Fatalf("BuildTLS() = %#v, want nil", cfg)
}
}
func TestBuildTLSConfigRequiresCertAndKey(t *testing.T) {
_, err := BuildTLS(TLSConfig{Enabled: true})
if err == nil || !strings.Contains(err.Error(), "cert_file") {
t.Fatalf("missing cert error = %v, want cert_file error", err)
}
_, err = BuildTLS(TLSConfig{Enabled: true, CertFile: "cert.pem"})
if err == nil || !strings.Contains(err.Error(), "key_file") {
t.Fatalf("missing key error = %v, want key_file error", err)
}
}