From c3a384e764af72e0c21bf1be1604e7b467928e87 Mon Sep 17 00:00:00 2001 From: Bo-Yi Wu Date: Wed, 8 Apr 2026 22:36:17 +0800 Subject: [PATCH 1/3] refactor(config): replace manual viper mapping with Unmarshal - Replace 84 manual viper.GetX() calls with viper.Unmarshal() using yaml struct tags - Remove 116-line defaultConf YAML blob, consolidate all defaults into setDefaults() - Use viper.New() instead of global singleton to prevent test pollution - Extract shared test assertions into assertCommonConfig helper - Remove tests that referenced deleted defaultConf variable - Fix Huawei test that relied on placeholder default values Co-Authored-By: Claude Opus 4.6 (1M context) --- config/config.go | 436 ++++++++++---------------------- config/config_test.go | 265 +++++++------------ go.mod | 2 +- notify/notification_hms_test.go | 1 + 4 files changed, 224 insertions(+), 480 deletions(-) diff --git a/config/config.go b/config/config.go index 5409b110..e9bc4ebc 100644 --- a/config/config.go +++ b/config/config.go @@ -11,126 +11,10 @@ import ( "strconv" "strings" + "github.com/go-viper/mapstructure/v2" "github.com/spf13/viper" ) -var defaultConf = []byte(` -core: - enabled: true # enable httpd server - address: "" # ip address to bind (default: any) - shutdown_timeout: 30 # default is 30 second - port: "8088" # ignore this port number if auto_tls is enabled (listen 443). - worker_num: 0 # default worker number is runtime.NumCPU() - queue_num: 0 # default queue number is 8192 - max_notification: 100 - # set true if you need get error message from fail push notification in API response. - # It only works when the queue engine is local. - sync: false - # set webhook url if you need get error message asynchronously from fail push notification in API response. - feedback_hook_url: "" - feedback_timeout: 10 # default is 10 second - feedback_header: - mode: "release" - ssl: false - cert_path: "cert.pem" - key_path: "key.pem" - cert_base64: "" - key_base64: "" - http_proxy: "" - pid: - enabled: false - path: "gorush.pid" - override: true - auto_tls: - enabled: false # Automatically install TLS certificates from Let's Encrypt. - folder: ".cache" # folder for storing TLS certificates - host: "" # which domains the Let's Encrypt will attempt - -grpc: - enabled: false # enable gRPC server - port: 9000 - -api: - push_uri: "/api/push" - stat_go_uri: "/api/stat/go" - stat_app_uri: "/api/stat/app" - config_uri: "/api/config" - sys_stat_uri: "/sys/stats" - metric_uri: "/metrics" - health_uri: "/healthz" - -android: - enabled: true - key_path: "" # path to fcm key file - credential: "" # fcm credential data - max_retry: 0 # resend fail notification, default value zero is disabled - -huawei: - enabled: false - appsecret: "YOUR_APP_SECRET" - appid: "YOUR_APP_ID" - max_retry: 0 # resend fail notification, default value zero is disabled - -queue: - engine: "local" # support "local", "nsq", "nats" and "redis" default value is "local" - nsq: - addr: 127.0.0.1:4150 - topic: gorush - channel: gorush - nats: - addr: 127.0.0.1:4222 - subj: gorush - queue: gorush - redis: - addr: 127.0.0.1:6379 - group: gorush - consumer: gorush - stream_name: gorush - with_tls: false - username: "" - password: "" - db: 0 - -ios: - enabled: false - key_path: "" - key_base64: "" # load iOS key from base64 input - key_type: "pem" # could be pem, p12 or p8 type - password: "" # certificate password, default as empty string. - production: false - max_concurrent_pushes: 100 # just for push ios notification - max_retry: 0 # resend fail notification, default value zero is disabled - key_id: "" # KeyID from developer account (Certificates, Identifiers & Profiles -> Keys) - team_id: "" # TeamID from developer account (View Account -> Membership) - -log: - format: "string" # string or json - access_log: "stdout" # stdout: output to console, or define log path like "log/access_log" - access_level: "debug" - error_log: "stderr" # stderr: output to console, or define log path like "log/error_log" - error_level: "error" - hide_token: true - hide_messages: false - -stat: - engine: "memory" # support memory, redis, boltdb, buntdb or leveldb - redis: - cluster: false - addr: "localhost:6379" # if cluster is true, you may set this to "localhost:6379,localhost:6380,localhost:6381" - username: "" - password: "" - db: 0 - boltdb: - path: "bolt.db" - bucket: "gorush" - buntdb: - path: "bunt.db" - leveldb: - path: "level.db" - badgerdb: - path: "badger.db" -`) - // ConfYaml is config structure. type ConfYaml struct { Core SectionCore `yaml:"core"` @@ -315,76 +199,120 @@ type SectionGRPC struct { Port string `yaml:"port"` } -func setDefaults() { - // Core defaults - viper.SetDefault("core.enabled", true) - viper.SetDefault("core.address", "") - viper.SetDefault("core.shutdown_timeout", 30) - viper.SetDefault("core.port", "8088") - viper.SetDefault("core.worker_num", 0) - viper.SetDefault("core.queue_num", 0) - viper.SetDefault("core.max_notification", 100) - viper.SetDefault("core.sync", false) - viper.SetDefault("core.feedback_timeout", 10) - viper.SetDefault("core.mode", "release") - viper.SetDefault("core.ssl", false) - viper.SetDefault("core.cert_path", "cert.pem") - viper.SetDefault("core.key_path", "key.pem") - viper.SetDefault("core.pid.enabled", false) - viper.SetDefault("core.pid.path", "gorush.pid") - viper.SetDefault("core.pid.override", true) - viper.SetDefault("core.auto_tls.enabled", false) - viper.SetDefault("core.auto_tls.folder", ".cache") - - // iOS defaults - viper.SetDefault("ios.enabled", false) - viper.SetDefault("ios.key_type", "pem") - viper.SetDefault("ios.production", false) - viper.SetDefault("ios.max_concurrent_pushes", uint(100)) - viper.SetDefault("ios.max_retry", 0) - - // Android defaults - viper.SetDefault("android.enabled", true) - viper.SetDefault("android.max_retry", 0) - - // API defaults - viper.SetDefault("api.push_uri", "/api/push") - viper.SetDefault("api.stat_go_uri", "/api/stat/go") - viper.SetDefault("api.stat_app_uri", "/api/stat/app") - viper.SetDefault("api.config_uri", "/api/config") - viper.SetDefault("api.sys_stat_uri", "/sys/stats") - viper.SetDefault("api.metric_uri", "/metrics") - viper.SetDefault("api.health_uri", "/healthz") - - // gRPC defaults - viper.SetDefault("grpc.enabled", false) - viper.SetDefault("grpc.port", "9000") - - // Queue defaults - viper.SetDefault("queue.engine", "local") - - // Stat defaults - viper.SetDefault("stat.engine", "memory") - - // Log defaults - viper.SetDefault("log.format", "string") - viper.SetDefault("log.access_log", "stdout") - viper.SetDefault("log.access_level", "debug") - viper.SetDefault("log.error_log", "stderr") - viper.SetDefault("log.error_level", "error") - viper.SetDefault("log.hide_token", true) - viper.SetDefault("log.hide_messages", false) +func setDefaults(v *viper.Viper) { + // Core + v.SetDefault("core.enabled", true) + v.SetDefault("core.address", "") + v.SetDefault("core.shutdown_timeout", 30) + v.SetDefault("core.port", "8088") + v.SetDefault("core.worker_num", 0) + v.SetDefault("core.queue_num", 0) + v.SetDefault("core.max_notification", 100) + v.SetDefault("core.sync", false) + v.SetDefault("core.feedback_hook_url", "") + v.SetDefault("core.feedback_timeout", 10) + v.SetDefault("core.mode", "release") + v.SetDefault("core.ssl", false) + v.SetDefault("core.cert_path", "cert.pem") + v.SetDefault("core.key_path", "key.pem") + v.SetDefault("core.cert_base64", "") + v.SetDefault("core.key_base64", "") + v.SetDefault("core.http_proxy", "") + v.SetDefault("core.pid.enabled", false) + v.SetDefault("core.pid.path", "gorush.pid") + v.SetDefault("core.pid.override", true) + v.SetDefault("core.auto_tls.enabled", false) + v.SetDefault("core.auto_tls.folder", ".cache") + v.SetDefault("core.auto_tls.host", "") + + // API + v.SetDefault("api.push_uri", "/api/push") + v.SetDefault("api.stat_go_uri", "/api/stat/go") + v.SetDefault("api.stat_app_uri", "/api/stat/app") + v.SetDefault("api.config_uri", "/api/config") + v.SetDefault("api.sys_stat_uri", "/sys/stats") + v.SetDefault("api.metric_uri", "/metrics") + v.SetDefault("api.health_uri", "/healthz") + + // Android + v.SetDefault("android.enabled", true) + v.SetDefault("android.max_retry", 0) + + // Android + v.SetDefault("android.key_path", "") + v.SetDefault("android.credential", "") + + // Huawei + v.SetDefault("huawei.enabled", false) + v.SetDefault("huawei.appsecret", "") + v.SetDefault("huawei.appid", "") + v.SetDefault("huawei.max_retry", 0) + + // iOS + v.SetDefault("ios.enabled", false) + v.SetDefault("ios.key_path", "") + v.SetDefault("ios.key_base64", "") + v.SetDefault("ios.key_type", "pem") + v.SetDefault("ios.password", "") + v.SetDefault("ios.production", false) + v.SetDefault("ios.max_concurrent_pushes", uint(100)) + v.SetDefault("ios.max_retry", 0) + v.SetDefault("ios.key_id", "") + v.SetDefault("ios.team_id", "") + + // gRPC + v.SetDefault("grpc.enabled", false) + v.SetDefault("grpc.port", "9000") + + // Queue + v.SetDefault("queue.engine", "local") + v.SetDefault("queue.nsq.addr", "127.0.0.1:4150") + v.SetDefault("queue.nsq.topic", "gorush") + v.SetDefault("queue.nsq.channel", "gorush") + v.SetDefault("queue.nats.addr", "127.0.0.1:4222") + v.SetDefault("queue.nats.subj", "gorush") + v.SetDefault("queue.nats.queue", "gorush") + v.SetDefault("queue.redis.addr", "127.0.0.1:6379") + v.SetDefault("queue.redis.group", "gorush") + v.SetDefault("queue.redis.consumer", "gorush") + v.SetDefault("queue.redis.stream_name", "gorush") + v.SetDefault("queue.redis.with_tls", false) + v.SetDefault("queue.redis.username", "") + v.SetDefault("queue.redis.password", "") + v.SetDefault("queue.redis.db", 0) + + // Stat + v.SetDefault("stat.engine", "memory") + v.SetDefault("stat.redis.cluster", false) + v.SetDefault("stat.redis.addr", "localhost:6379") + v.SetDefault("stat.redis.username", "") + v.SetDefault("stat.redis.password", "") + v.SetDefault("stat.redis.db", 0) + v.SetDefault("stat.boltdb.path", "bolt.db") + v.SetDefault("stat.boltdb.bucket", "gorush") + v.SetDefault("stat.buntdb.path", "bunt.db") + v.SetDefault("stat.leveldb.path", "level.db") + v.SetDefault("stat.badgerdb.path", "badger.db") + + // Log + v.SetDefault("log.format", "string") + v.SetDefault("log.access_log", "stdout") + v.SetDefault("log.access_level", "debug") + v.SetDefault("log.error_log", "stderr") + v.SetDefault("log.error_level", "error") + v.SetDefault("log.hide_token", true) + v.SetDefault("log.hide_messages", false) } // LoadConf load config from file and read in environment variables that match func LoadConf(confPath ...string) (*ConfYaml, error) { - // load default values - setDefaults() + v := viper.New() + setDefaults(v) - viper.SetConfigType("yaml") - viper.AutomaticEnv() // read in environment variables that match - viper.SetEnvPrefix("gorush") // will be uppercased automatically - viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + v.SetConfigType("yaml") + v.AutomaticEnv() // read in environment variables that match + v.SetEnvPrefix("gorush") // will be uppercased automatically + v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) // If config path is provided, load from file if len(confPath) > 0 && confPath[0] != "" { @@ -393,146 +321,48 @@ func LoadConf(confPath ...string) (*ConfYaml, error) { return nil, fmt.Errorf("failed to read config file %s: %w", confPath[0], err) } - if err := viper.ReadConfig(bytes.NewBuffer(content)); err != nil { + if err := v.ReadConfig(bytes.NewBuffer(content)); err != nil { return nil, fmt.Errorf("failed to parse config file %s: %w", confPath[0], err) } - // Early return after successfully loading config from file - return loadConfigFromViper() + return loadConfigFromViper(v) } // Search config in home directory with name ".gorush" (without extension). - viper.AddConfigPath("/etc/gorush/") - viper.AddConfigPath("$HOME/.gorush") - viper.AddConfigPath(".") - viper.SetConfigName("config") + v.AddConfigPath("/etc/gorush/") + v.AddConfigPath("$HOME/.gorush") + v.AddConfigPath(".") + v.SetConfigName("config") // If a config file is found, read it in. - if err := viper.ReadInConfig(); err == nil { - log.Println("Using config file:", viper.ConfigFileUsed()) - return loadConfigFromViper() + if err := v.ReadInConfig(); err == nil { + log.Println("Using config file:", v.ConfigFileUsed()) } - // Try to load default config as fallback - if err := viper.ReadConfig(bytes.NewBuffer(defaultConf)); err != nil { - return nil, fmt.Errorf("failed to load default config and no config file found: %w", err) - } - - return loadConfigFromViper() + return loadConfigFromViper(v) } -// loadConfigFromViper extracts config loading logic to avoid code duplication -func loadConfigFromViper() (*ConfYaml, error) { +// loadConfigFromViper unmarshals viper config into the ConfYaml struct +func loadConfigFromViper(v *viper.Viper) (*ConfYaml, error) { conf := &ConfYaml{} - // Core - conf.Core.Address = viper.GetString("core.address") - conf.Core.Port = viper.GetString("core.port") - conf.Core.ShutdownTimeout = int64(viper.GetInt("core.shutdown_timeout")) - conf.Core.Enabled = viper.GetBool("core.enabled") - conf.Core.WorkerNum = int64(viper.GetInt("core.worker_num")) - conf.Core.QueueNum = int64(viper.GetInt("core.queue_num")) - conf.Core.Mode = viper.GetString("core.mode") - conf.Core.Sync = viper.GetBool("core.sync") - conf.Core.FeedbackURL = viper.GetString("core.feedback_hook_url") - conf.Core.FeedbackTimeout = int64(viper.GetInt("core.feedback_timeout")) - conf.Core.FeedbackHeader = viper.GetStringSlice("core.feedback_header") - conf.Core.SSL = viper.GetBool("core.ssl") - conf.Core.CertPath = viper.GetString("core.cert_path") - conf.Core.KeyPath = viper.GetString("core.key_path") - conf.Core.CertBase64 = viper.GetString("core.cert_base64") - conf.Core.KeyBase64 = viper.GetString("core.key_base64") - conf.Core.MaxNotification = int64(viper.GetInt("core.max_notification")) - conf.Core.HTTPProxy = viper.GetString("core.http_proxy") - conf.Core.PID.Enabled = viper.GetBool("core.pid.enabled") - conf.Core.PID.Path = viper.GetString("core.pid.path") - conf.Core.PID.Override = viper.GetBool("core.pid.override") - conf.Core.AutoTLS.Enabled = viper.GetBool("core.auto_tls.enabled") - conf.Core.AutoTLS.Folder = viper.GetString("core.auto_tls.folder") - conf.Core.AutoTLS.Host = viper.GetString("core.auto_tls.host") - - // Api - conf.API.PushURI = viper.GetString("api.push_uri") - conf.API.StatGoURI = viper.GetString("api.stat_go_uri") - conf.API.StatAppURI = viper.GetString("api.stat_app_uri") - conf.API.ConfigURI = viper.GetString("api.config_uri") - conf.API.SysStatURI = viper.GetString("api.sys_stat_uri") - conf.API.MetricURI = viper.GetString("api.metric_uri") - conf.API.HealthURI = viper.GetString("api.health_uri") - - // Android - conf.Android.Enabled = viper.GetBool("android.enabled") - conf.Android.KeyPath = viper.GetString("android.key_path") - conf.Android.Credential = viper.GetString("android.credential") - conf.Android.MaxRetry = viper.GetInt("android.max_retry") + err := v.Unmarshal(conf, func(dc *mapstructure.DecoderConfig) { + dc.TagName = "yaml" + }) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal config: %w", err) + } - // Huawei - conf.Huawei.Enabled = viper.GetBool("huawei.enabled") - conf.Huawei.AppSecret = viper.GetString("huawei.appsecret") - conf.Huawei.AppID = viper.GetString("huawei.appid") - conf.Huawei.MaxRetry = viper.GetInt("huawei.max_retry") + // GetStringSlice handles space-delimited env vars (e.g., "key1:val1 key2:val2") + // which Unmarshal does not split automatically + conf.Core.FeedbackHeader = v.GetStringSlice("core.feedback_header") - // iOS - conf.Ios.Enabled = viper.GetBool("ios.enabled") - conf.Ios.KeyPath = viper.GetString("ios.key_path") - conf.Ios.KeyBase64 = viper.GetString("ios.key_base64") - conf.Ios.KeyType = viper.GetString("ios.key_type") - conf.Ios.Password = viper.GetString("ios.password") - conf.Ios.Production = viper.GetBool("ios.production") - conf.Ios.MaxConcurrentPushes = viper.GetUint("ios.max_concurrent_pushes") - conf.Ios.MaxRetry = viper.GetInt("ios.max_retry") - conf.Ios.KeyID = viper.GetString("ios.key_id") - conf.Ios.TeamID = viper.GetString("ios.team_id") - - // log - conf.Log.Format = viper.GetString("log.format") - conf.Log.AccessLog = viper.GetString("log.access_log") - conf.Log.AccessLevel = viper.GetString("log.access_level") - conf.Log.ErrorLog = viper.GetString("log.error_log") - conf.Log.ErrorLevel = viper.GetString("log.error_level") - conf.Log.HideToken = viper.GetBool("log.hide_token") - conf.Log.HideMessages = viper.GetBool("log.hide_messages") - - // Queue Engine - conf.Queue.Engine = viper.GetString("queue.engine") - conf.Queue.NSQ.Addr = viper.GetString("queue.nsq.addr") - conf.Queue.NSQ.Topic = viper.GetString("queue.nsq.topic") - conf.Queue.NSQ.Channel = viper.GetString("queue.nsq.channel") - conf.Queue.NATS.Addr = viper.GetString("queue.nats.addr") - conf.Queue.NATS.Subj = viper.GetString("queue.nats.subj") - conf.Queue.NATS.Queue = viper.GetString("queue.nats.queue") - conf.Queue.Redis.Addr = viper.GetString("queue.redis.addr") - conf.Queue.Redis.StreamName = viper.GetString("queue.redis.stream_name") - conf.Queue.Redis.Group = viper.GetString("queue.redis.group") - conf.Queue.Redis.Consumer = viper.GetString("queue.redis.consumer") - conf.Queue.Redis.WithTLS = viper.GetBool("queue.redis.with_tls") - conf.Queue.Redis.Username = viper.GetString("queue.redis.username") - conf.Queue.Redis.Password = viper.GetString("queue.redis.password") - conf.Queue.Redis.DB = viper.GetInt("queue.redis.db") - - // Stat Engine - conf.Stat.Engine = viper.GetString("stat.engine") - conf.Stat.Redis.Cluster = viper.GetBool("stat.redis.cluster") - conf.Stat.Redis.Addr = viper.GetString("stat.redis.addr") - conf.Stat.Redis.Username = viper.GetString("stat.redis.username") - conf.Stat.Redis.Password = viper.GetString("stat.redis.password") - conf.Stat.Redis.DB = viper.GetInt("stat.redis.db") - conf.Stat.BoltDB.Path = viper.GetString("stat.boltdb.path") - conf.Stat.BoltDB.Bucket = viper.GetString("stat.boltdb.bucket") - conf.Stat.BuntDB.Path = viper.GetString("stat.buntdb.path") - conf.Stat.LevelDB.Path = viper.GetString("stat.leveldb.path") - conf.Stat.BadgerDB.Path = viper.GetString("stat.badgerdb.path") - - // gRPC Server - conf.GRPC.Enabled = viper.GetBool("grpc.enabled") - conf.GRPC.Port = viper.GetString("grpc.port") - - if conf.Core.WorkerNum == int64(0) { + if conf.Core.WorkerNum == 0 { conf.Core.WorkerNum = int64(runtime.NumCPU()) } - if conf.Core.QueueNum == int64(0) { - conf.Core.QueueNum = int64(8192) + if conf.Core.QueueNum == 0 { + conf.Core.QueueNum = 8192 } return conf, nil diff --git a/config/config_test.go b/config/config_test.go index cb5bd562..15e485e6 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -43,25 +43,6 @@ func TestInvalidYAMLFile(t *testing.T) { assert.Contains(t, err.Error(), tmpFile) } -// Test improved error messages for default config loading -func TestDefaultConfigLoadFailure(t *testing.T) { - // Backup original defaultConf - originalDefaultConf := defaultConf - defer func() { - defaultConf = originalDefaultConf - }() - - // Set invalid default config - defaultConf = []byte(`invalid: yaml: [unclosed`) - - conf, err := LoadConf() - - assert.Nil(t, conf) - require.Error(t, err) - // Check that the error message describes the default config loading failure - assert.Contains(t, err.Error(), "failed to load default config and no config file found") -} - func TestEmptyConfig(t *testing.T) { conf, err := LoadConf("testdata/empty.yml") if err != nil { @@ -89,62 +70,98 @@ func (suite *ConfigTestSuite) SetupTest() { } } -func (suite *ConfigTestSuite) TestValidateConfDefault() { +// assertCommonConfig verifies config values shared by both default and file-loaded configs. +func (suite *ConfigTestSuite) assertCommonConfig(conf *ConfYaml) { // Core + suite.Equal("8088", conf.Core.Port) + suite.Equal(int64(30), conf.Core.ShutdownTimeout) + suite.True(conf.Core.Enabled) + suite.Equal(int64(runtime.NumCPU()), conf.Core.WorkerNum) + suite.Equal(int64(8192), conf.Core.QueueNum) + suite.Equal("release", conf.Core.Mode) + suite.False(conf.Core.Sync) + suite.Empty(conf.Core.FeedbackURL) + suite.Equal(int64(10), conf.Core.FeedbackTimeout) + suite.False(conf.Core.SSL) + suite.Equal("cert.pem", conf.Core.CertPath) + suite.Equal("key.pem", conf.Core.KeyPath) + suite.Empty(conf.Core.CertBase64) + suite.Empty(conf.Core.KeyBase64) + suite.Equal(int64(100), conf.Core.MaxNotification) + suite.Empty(conf.Core.HTTPProxy) + + // PID + suite.False(conf.Core.PID.Enabled) + suite.Equal("gorush.pid", conf.Core.PID.Path) + suite.True(conf.Core.PID.Override) + suite.False(conf.Core.AutoTLS.Enabled) + suite.Equal(".cache", conf.Core.AutoTLS.Folder) + suite.Empty(conf.Core.AutoTLS.Host) + + // API + suite.Equal("/api/push", conf.API.PushURI) + suite.Equal("/api/stat/go", conf.API.StatGoURI) + suite.Equal("/api/stat/app", conf.API.StatAppURI) + suite.Equal("/api/config", conf.API.ConfigURI) + suite.Equal("/sys/stats", conf.API.SysStatURI) + suite.Equal("/metrics", conf.API.MetricURI) + suite.Equal("/healthz", conf.API.HealthURI) + + // iOS (common fields) + suite.False(conf.Ios.Enabled) + suite.Empty(conf.Ios.KeyBase64) + suite.Equal("pem", conf.Ios.KeyType) + suite.Empty(conf.Ios.Password) + suite.False(conf.Ios.Production) + suite.Equal(uint(100), conf.Ios.MaxConcurrentPushes) + suite.Equal(0, conf.Ios.MaxRetry) + suite.Empty(conf.Ios.KeyID) + suite.Empty(conf.Ios.TeamID) + + // Log + suite.Equal("string", conf.Log.Format) + suite.Equal("stdout", conf.Log.AccessLog) + suite.Equal("debug", conf.Log.AccessLevel) + suite.Equal("stderr", conf.Log.ErrorLog) + suite.Equal("error", conf.Log.ErrorLevel) + suite.True(conf.Log.HideToken) + + // Stat + suite.Equal("memory", conf.Stat.Engine) + suite.False(conf.Stat.Redis.Cluster) + suite.Equal("localhost:6379", conf.Stat.Redis.Addr) + suite.Empty(conf.Stat.Redis.Username) + suite.Empty(conf.Stat.Redis.Password) + suite.Equal(0, conf.Stat.Redis.DB) + suite.Equal("bolt.db", conf.Stat.BoltDB.Path) + suite.Equal("gorush", conf.Stat.BoltDB.Bucket) + suite.Equal("bunt.db", conf.Stat.BuntDB.Path) + suite.Equal("level.db", conf.Stat.LevelDB.Path) + suite.Equal("badger.db", conf.Stat.BadgerDB.Path) + + // gRPC + suite.False(conf.GRPC.Enabled) + suite.Equal("9000", conf.GRPC.Port) +} + +func (suite *ConfigTestSuite) TestValidateConfDefault() { + suite.assertCommonConfig(suite.ConfGorushDefault) + + // Default-specific assertions suite.Empty(suite.ConfGorushDefault.Core.Address) - suite.Equal("8088", suite.ConfGorushDefault.Core.Port) - suite.Equal(int64(30), suite.ConfGorushDefault.Core.ShutdownTimeout) - suite.True(suite.ConfGorushDefault.Core.Enabled) - suite.Equal(int64(runtime.NumCPU()), suite.ConfGorushDefault.Core.WorkerNum) - suite.Equal(int64(8192), suite.ConfGorushDefault.Core.QueueNum) - suite.Equal("release", suite.ConfGorushDefault.Core.Mode) - suite.False(suite.ConfGorushDefault.Core.Sync) - suite.Empty(suite.ConfGorushDefault.Core.FeedbackURL) suite.Empty(suite.ConfGorushDefault.Core.FeedbackHeader) - suite.Equal(int64(10), suite.ConfGorushDefault.Core.FeedbackTimeout) - suite.False(suite.ConfGorushDefault.Core.SSL) - suite.Equal("cert.pem", suite.ConfGorushDefault.Core.CertPath) - suite.Equal("key.pem", suite.ConfGorushDefault.Core.KeyPath) - suite.Empty(suite.ConfGorushDefault.Core.KeyBase64) - suite.Empty(suite.ConfGorushDefault.Core.CertBase64) - suite.Equal(int64(100), suite.ConfGorushDefault.Core.MaxNotification) - suite.Empty(suite.ConfGorushDefault.Core.HTTPProxy) - // Pid - suite.False(suite.ConfGorushDefault.Core.PID.Enabled) - suite.Equal("gorush.pid", suite.ConfGorushDefault.Core.PID.Path) - suite.True(suite.ConfGorushDefault.Core.PID.Override) - suite.False(suite.ConfGorushDefault.Core.AutoTLS.Enabled) - suite.Equal(".cache", suite.ConfGorushDefault.Core.AutoTLS.Folder) - suite.Empty(suite.ConfGorushDefault.Core.AutoTLS.Host) - - // Api - suite.Equal("/api/push", suite.ConfGorushDefault.API.PushURI) - suite.Equal("/api/stat/go", suite.ConfGorushDefault.API.StatGoURI) - suite.Equal("/api/stat/app", suite.ConfGorushDefault.API.StatAppURI) - suite.Equal("/api/config", suite.ConfGorushDefault.API.ConfigURI) - suite.Equal("/sys/stats", suite.ConfGorushDefault.API.SysStatURI) - suite.Equal("/metrics", suite.ConfGorushDefault.API.MetricURI) - suite.Equal("/healthz", suite.ConfGorushDefault.API.HealthURI) - - // Android + suite.False(suite.ConfGorushDefault.Log.HideMessages) + + // Android defaults suite.True(suite.ConfGorushDefault.Android.Enabled) suite.Empty(suite.ConfGorushDefault.Android.KeyPath) suite.Empty(suite.ConfGorushDefault.Android.Credential) suite.Equal(0, suite.ConfGorushDefault.Android.MaxRetry) - // iOS - suite.False(suite.ConfGorushDefault.Ios.Enabled) + // iOS defaults suite.Empty(suite.ConfGorushDefault.Ios.KeyPath) - suite.Empty(suite.ConfGorushDefault.Ios.KeyBase64) - suite.Equal("pem", suite.ConfGorushDefault.Ios.KeyType) - suite.Empty(suite.ConfGorushDefault.Ios.Password) - suite.False(suite.ConfGorushDefault.Ios.Production) - suite.Equal(uint(100), suite.ConfGorushDefault.Ios.MaxConcurrentPushes) - suite.Equal(0, suite.ConfGorushDefault.Ios.MaxRetry) - suite.Empty(suite.ConfGorushDefault.Ios.KeyID) - suite.Empty(suite.ConfGorushDefault.Ios.TeamID) - - // queue + + // Queue defaults suite.Equal("local", suite.ConfGorushDefault.Queue.Engine) suite.Equal("127.0.0.1:4150", suite.ConfGorushDefault.Queue.NSQ.Addr) suite.Equal("gorush", suite.ConfGorushDefault.Queue.NSQ.Topic) @@ -162,118 +179,26 @@ func (suite *ConfigTestSuite) TestValidateConfDefault() { suite.Empty(suite.ConfGorushDefault.Queue.Redis.Password) suite.False(suite.ConfGorushDefault.Queue.Redis.WithTLS) suite.Equal(0, suite.ConfGorushDefault.Queue.Redis.DB) - - // log - suite.Equal("string", suite.ConfGorushDefault.Log.Format) - suite.Equal("stdout", suite.ConfGorushDefault.Log.AccessLog) - suite.Equal("debug", suite.ConfGorushDefault.Log.AccessLevel) - suite.Equal("stderr", suite.ConfGorushDefault.Log.ErrorLog) - suite.Equal("error", suite.ConfGorushDefault.Log.ErrorLevel) - suite.True(suite.ConfGorushDefault.Log.HideToken) - suite.False(suite.ConfGorushDefault.Log.HideMessages) - - suite.Equal("memory", suite.ConfGorushDefault.Stat.Engine) - suite.False(suite.ConfGorushDefault.Stat.Redis.Cluster) - suite.Equal("localhost:6379", suite.ConfGorushDefault.Stat.Redis.Addr) - suite.Empty(suite.ConfGorushDefault.Stat.Redis.Username) - suite.Empty(suite.ConfGorushDefault.Stat.Redis.Password) - suite.Equal(0, suite.ConfGorushDefault.Stat.Redis.DB) - - suite.Equal("bolt.db", suite.ConfGorushDefault.Stat.BoltDB.Path) - suite.Equal("gorush", suite.ConfGorushDefault.Stat.BoltDB.Bucket) - - suite.Equal("bunt.db", suite.ConfGorushDefault.Stat.BuntDB.Path) - suite.Equal("level.db", suite.ConfGorushDefault.Stat.LevelDB.Path) - suite.Equal("badger.db", suite.ConfGorushDefault.Stat.BadgerDB.Path) - - // gRPC - suite.False(suite.ConfGorushDefault.GRPC.Enabled) - suite.Equal("9000", suite.ConfGorushDefault.GRPC.Port) } func (suite *ConfigTestSuite) TestValidateConf() { - // Core - suite.Equal("8088", suite.ConfGorush.Core.Port) - suite.Equal(int64(30), suite.ConfGorush.Core.ShutdownTimeout) - suite.True(suite.ConfGorush.Core.Enabled) - suite.Equal(int64(runtime.NumCPU()), suite.ConfGorush.Core.WorkerNum) - suite.Equal(int64(8192), suite.ConfGorush.Core.QueueNum) - suite.Equal("release", suite.ConfGorush.Core.Mode) - suite.False(suite.ConfGorush.Core.Sync) - suite.Empty(suite.ConfGorush.Core.FeedbackURL) - suite.Equal(int64(10), suite.ConfGorush.Core.FeedbackTimeout) + suite.assertCommonConfig(suite.ConfGorush) + + // File-specific assertions suite.Len(suite.ConfGorush.Core.FeedbackHeader, 1) suite.Equal( "x-gorush-token:4e989115e09680f44a645519fed6a976", suite.ConfGorush.Core.FeedbackHeader[0], ) - suite.False(suite.ConfGorush.Core.SSL) - suite.Equal("cert.pem", suite.ConfGorush.Core.CertPath) - suite.Equal("key.pem", suite.ConfGorush.Core.KeyPath) - suite.Empty(suite.ConfGorush.Core.CertBase64) - suite.Empty(suite.ConfGorush.Core.KeyBase64) - suite.Equal(int64(100), suite.ConfGorush.Core.MaxNotification) - suite.Empty(suite.ConfGorush.Core.HTTPProxy) - // Pid - suite.False(suite.ConfGorush.Core.PID.Enabled) - suite.Equal("gorush.pid", suite.ConfGorush.Core.PID.Path) - suite.True(suite.ConfGorush.Core.PID.Override) - suite.False(suite.ConfGorush.Core.AutoTLS.Enabled) - suite.Equal(".cache", suite.ConfGorush.Core.AutoTLS.Folder) - suite.Empty(suite.ConfGorush.Core.AutoTLS.Host) - - // Api - suite.Equal("/api/push", suite.ConfGorush.API.PushURI) - suite.Equal("/api/stat/go", suite.ConfGorush.API.StatGoURI) - suite.Equal("/api/stat/app", suite.ConfGorush.API.StatAppURI) - suite.Equal("/api/config", suite.ConfGorush.API.ConfigURI) - suite.Equal("/sys/stats", suite.ConfGorush.API.SysStatURI) - suite.Equal("/metrics", suite.ConfGorush.API.MetricURI) - suite.Equal("/healthz", suite.ConfGorush.API.HealthURI) - - // Android + + // Android from file suite.True(suite.ConfGorush.Android.Enabled) suite.Equal("key.json", suite.ConfGorush.Android.KeyPath) suite.Equal("CREDENTIAL_JSON_DATA", suite.ConfGorush.Android.Credential) suite.Equal(0, suite.ConfGorush.Android.MaxRetry) - // iOS - suite.False(suite.ConfGorush.Ios.Enabled) + // iOS from file suite.Equal("key.pem", suite.ConfGorush.Ios.KeyPath) - suite.Empty(suite.ConfGorush.Ios.KeyBase64) - suite.Equal("pem", suite.ConfGorush.Ios.KeyType) - suite.Empty(suite.ConfGorush.Ios.Password) - suite.False(suite.ConfGorush.Ios.Production) - suite.Equal(uint(100), suite.ConfGorush.Ios.MaxConcurrentPushes) - suite.Equal(0, suite.ConfGorush.Ios.MaxRetry) - suite.Empty(suite.ConfGorush.Ios.KeyID) - suite.Empty(suite.ConfGorush.Ios.TeamID) - - // log - suite.Equal("string", suite.ConfGorush.Log.Format) - suite.Equal("stdout", suite.ConfGorush.Log.AccessLog) - suite.Equal("debug", suite.ConfGorush.Log.AccessLevel) - suite.Equal("stderr", suite.ConfGorush.Log.ErrorLog) - suite.Equal("error", suite.ConfGorush.Log.ErrorLevel) - suite.True(suite.ConfGorush.Log.HideToken) - - suite.Equal("memory", suite.ConfGorush.Stat.Engine) - suite.False(suite.ConfGorush.Stat.Redis.Cluster) - suite.Equal("localhost:6379", suite.ConfGorush.Stat.Redis.Addr) - suite.Empty(suite.ConfGorush.Stat.Redis.Username) - suite.Empty(suite.ConfGorush.Stat.Redis.Password) - suite.Equal(0, suite.ConfGorush.Stat.Redis.DB) - - suite.Equal("bolt.db", suite.ConfGorush.Stat.BoltDB.Path) - suite.Equal("gorush", suite.ConfGorush.Stat.BoltDB.Bucket) - - suite.Equal("bunt.db", suite.ConfGorush.Stat.BuntDB.Path) - suite.Equal("level.db", suite.ConfGorush.Stat.LevelDB.Path) - suite.Equal("badger.db", suite.ConfGorush.Stat.BadgerDB.Path) - - // gRPC - suite.False(suite.ConfGorush.GRPC.Enabled) - suite.Equal("9000", suite.ConfGorush.GRPC.Port) } func TestConfigTestSuite(t *testing.T) { @@ -338,18 +263,6 @@ func TestRedisDBConfigurationFromEnv(t *testing.T) { assert.Equal(t, 9, conf.Stat.Redis.DB) } -func TestLoadWrongDefaultYAMLConfig(t *testing.T) { - // Backup original defaultConf - originalDefaultConf := defaultConf - defer func() { - defaultConf = originalDefaultConf - }() - - defaultConf = []byte(`a`) - _, err := LoadConf() - assert.Error(t, err) -} - func TestValidatePort(t *testing.T) { tests := []struct { name string diff --git a/go.mod b/go.mod index 0cc86396..0ee68297 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/dgraph-io/badger/v4 v4.9.1 github.com/gin-contrib/logger v1.2.6 github.com/gin-gonic/gin v1.12.0 + github.com/go-viper/mapstructure/v2 v2.5.0 github.com/golang-queue/nats v0.2.0 github.com/golang-queue/nsq v0.3.0 github.com/golang-queue/queue v0.5.0 @@ -86,7 +87,6 @@ require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.30.1 // indirect - github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.19.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect diff --git a/notify/notification_hms_test.go b/notify/notification_hms_test.go index 351cae39..06aa2942 100644 --- a/notify/notification_hms_test.go +++ b/notify/notification_hms_test.go @@ -28,6 +28,7 @@ func TestMissingHuaweiAppID(t *testing.T) { cfg.Android.Enabled = false cfg.Huawei.Enabled = true + cfg.Huawei.AppSecret = "test-secret" cfg.Huawei.AppID = "" err := CheckPushConf(cfg) From cd29bcaa530cedd6828a14fc7732a9841ae5c899 Mon Sep 17 00:00:00 2001 From: Bo-Yi Wu Date: Wed, 8 Apr 2026 22:45:12 +0800 Subject: [PATCH 2/3] style(config): merge duplicate Android default blocks Co-Authored-By: Claude Opus 4.6 (1M context) --- config/config.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/config/config.go b/config/config.go index e9bc4ebc..19e26b10 100644 --- a/config/config.go +++ b/config/config.go @@ -237,8 +237,6 @@ func setDefaults(v *viper.Viper) { // Android v.SetDefault("android.enabled", true) v.SetDefault("android.max_retry", 0) - - // Android v.SetDefault("android.key_path", "") v.SetDefault("android.credential", "") From 0924e1928741fbd0cfbf5ad99f81ee55757a834d Mon Sep 17 00:00:00 2001 From: Bo-Yi Wu Date: Wed, 8 Apr 2026 22:52:04 +0800 Subject: [PATCH 3/3] fix(config): fail fast on config parse errors and fix misleading comment - Fix misleading comment about config file search behavior - Return error on config parse/permission failures instead of silently falling back to defaults; only ignore ConfigFileNotFoundError Co-Authored-By: Claude Opus 4.6 (1M context) --- config/config.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/config/config.go b/config/config.go index 19e26b10..e86e0a80 100644 --- a/config/config.go +++ b/config/config.go @@ -326,14 +326,19 @@ func LoadConf(confPath ...string) (*ConfYaml, error) { return loadConfigFromViper(v) } - // Search config in home directory with name ".gorush" (without extension). + // Search for config.{yaml,yml,json,...} in standard directories. v.AddConfigPath("/etc/gorush/") v.AddConfigPath("$HOME/.gorush") v.AddConfigPath(".") v.SetConfigName("config") // If a config file is found, read it in. - if err := v.ReadInConfig(); err == nil { + if err := v.ReadInConfig(); err != nil { + // Only ignore "config file not found" — fail on parse/permission errors + if _, ok := err.(viper.ConfigFileNotFoundError); !ok { + return nil, fmt.Errorf("failed to read config: %w", err) + } + } else { log.Println("Using config file:", v.ConfigFileUsed()) }