Files
gochat/backend/internal/config/config_test.go
T
rogee 762de6aa3b feat: migrate worker job dispatch from DB polling to Redis Streams
- worker.go: add Redis fields, XADD on Enqueue, XREADGROUP BLOCK consumer
  loop, sweep compensation for delayed/failed jobs, DB-polling fallback
  when rdb is nil, XGroupCreateMkStream for idempotent consumer groups
- config.go: extend WorkerConfig with stream_prefix, consumer_group,
  block_timeout_s, sweep_interval_s + defaults, env bindings, reloadable
- validator.go: validate new worker Redis fields
- bootstrap.go: fix concurrency bug (NewWorkerPoolWithOptions with Redis
  + cfg.Worker.Concurrency), reorder rdb init before worker pool
- config.dev.yaml/config.prod.yaml: add worker Redis params
- worker_test.go: 6 new miniredis tests (end-to-end, fallback, sweep,
  multi-consumer competition, group creation, Redis failure recovery)
- config_test.go/reloader_test.go: adapt fixtures for new fields
2026-07-10 12:56:32 +08:00

239 lines
7.7 KiB
Go

package config
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestValidate_ValidConfig(t *testing.T) {
cfg := &Config{
Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "debug"},
Database: DatabaseConfig{
Host: "localhost",
Port: 5432,
User: "gochat",
Password: "secret",
Name: "gochat_db",
},
Redis: RedisConfig{
URL: "redis://localhost:6379",
},
JWT: JWTConfig{
Secret: "test-secret-key-min-32-chars!!",
ExpiryHours: 24,
RefreshExpiryHours: 168,
},
Log: LogConfig{Level: "info", Format: "json"},
Captain: CaptainConfig{Enabled: false},
Worker: WorkerConfig{Concurrency: 4, BlockTimeoutS: 5, SweepIntervalS: 30},
OAuth: OAuthConfig{},
RateLimit: RateLimitConfig{Enabled: true, RequestsPerMinute: 100, WindowSeconds: 60},
Search: SearchConfig{Engine: "meilisearch", Host: "http://localhost:7700", IndexPrefix: "gochat_", TimeoutSeconds: 5},
}
err := Validate(cfg)
assert.NoError(t, err)
}
func TestValidate_InvalidPort(t *testing.T) {
cfg := &Config{
Server: ServerConfig{Host: "localhost", Port: 0, Mode: "debug"},
Database: DatabaseConfig{Host: "localhost", Name: "db", User: "user"},
Redis: RedisConfig{URL: "redis://localhost:6379"},
JWT: JWTConfig{Secret: "test-secret-key-min-32-chars!!"},
}
err := Validate(cfg)
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid server port")
}
func TestValidate_InvalidMode(t *testing.T) {
cfg := &Config{
Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "invalid"},
Database: DatabaseConfig{Host: "localhost", Name: "db", User: "user"},
Redis: RedisConfig{URL: "redis://localhost:6379"},
JWT: JWTConfig{Secret: "test-secret-key-min-32-chars!!"},
}
err := Validate(cfg)
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid server mode")
}
func TestValidate_MissingDBHost(t *testing.T) {
cfg := &Config{
Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "debug"},
Database: DatabaseConfig{Name: "db", User: "user"},
Redis: RedisConfig{URL: "redis://localhost:6379"},
JWT: JWTConfig{Secret: "test-secret-key-min-32-chars!!"},
}
err := Validate(cfg)
assert.Error(t, err)
assert.Contains(t, err.Error(), "database host is required")
}
func TestValidate_MissingDBName(t *testing.T) {
cfg := &Config{
Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "debug"},
Database: DatabaseConfig{Host: "localhost", User: "user"},
Redis: RedisConfig{URL: "redis://localhost:6379"},
JWT: JWTConfig{Secret: "test-secret-key-min-32-chars!!"},
}
err := Validate(cfg)
assert.Error(t, err)
assert.Contains(t, err.Error(), "database name is required")
}
func TestValidate_MissingRedisURL(t *testing.T) {
cfg := &Config{
Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "debug"},
Database: DatabaseConfig{Host: "localhost", Name: "db", User: "user"},
JWT: JWTConfig{Secret: "test-secret-key-min-32-chars!!"},
}
err := Validate(cfg)
assert.Error(t, err)
assert.Contains(t, err.Error(), "redis url is required")
}
func TestValidate_InvalidRedisURL(t *testing.T) {
cfg := &Config{
Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "debug"},
Database: DatabaseConfig{Host: "localhost", Name: "db", User: "user"},
Redis: RedisConfig{URL: "not-a-valid-url://::"},
JWT: JWTConfig{Secret: "test-secret-key-min-32-chars!!"},
}
err := Validate(cfg)
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid redis URL")
}
func TestValidate_JWTSecretInProduction(t *testing.T) {
cfg := &Config{
Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "release"},
Database: DatabaseConfig{Host: "localhost", Name: "db", User: "user"},
Redis: RedisConfig{URL: "redis://localhost:6379"},
JWT: JWTConfig{Secret: "change-me-in-production"},
}
err := Validate(cfg)
assert.Error(t, err)
assert.Contains(t, err.Error(), "JWT secret must be changed in production")
}
func TestValidate_InvalidLogLevel(t *testing.T) {
cfg := &Config{
Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "debug"},
Database: DatabaseConfig{Host: "localhost", Name: "db", User: "user"},
Redis: RedisConfig{URL: "redis://localhost:6379"},
JWT: JWTConfig{Secret: "test-secret-key-min-32-chars!!"},
Log: LogConfig{Level: "invalid"},
}
err := Validate(cfg)
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid log level")
}
func TestValidate_InvalidWorkerConcurrency(t *testing.T) {
cfg := &Config{
Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "debug"},
Database: DatabaseConfig{Host: "localhost", Name: "db", User: "user"},
Redis: RedisConfig{URL: "redis://localhost:6379"},
JWT: JWTConfig{Secret: "test-secret-key-min-32-chars!!"},
Log: LogConfig{Level: "info"},
Worker: WorkerConfig{Concurrency: 0},
}
err := Validate(cfg)
assert.Error(t, err)
assert.Contains(t, err.Error(), "worker concurrency")
}
func TestValidate_SearchMeilisearchRequiresValidHost(t *testing.T) {
cfg := &Config{
Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "debug"},
Database: DatabaseConfig{Host: "localhost", Name: "db", User: "user"},
Redis: RedisConfig{URL: "redis://localhost:6379"},
JWT: JWTConfig{Secret: "test-secret-key-min-32-chars!!"},
Log: LogConfig{Level: "info"},
Worker: WorkerConfig{Concurrency: 1, BlockTimeoutS: 5, SweepIntervalS: 30},
RateLimit: RateLimitConfig{RequestsPerMinute: 100, WindowSeconds: 60},
Search: SearchConfig{Engine: "meilisearch", Host: "not a url", TimeoutSeconds: 5},
}
err := Validate(cfg)
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid search.host")
}
func TestValidate_SearchDBFallbackAllowed(t *testing.T) {
cfg := &Config{
Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "debug"},
Database: DatabaseConfig{Host: "localhost", Name: "db", User: "user"},
Redis: RedisConfig{URL: "redis://localhost:6379"},
JWT: JWTConfig{Secret: "test-secret-key-min-32-chars!!"},
Log: LogConfig{Level: "info"},
Worker: WorkerConfig{Concurrency: 1, BlockTimeoutS: 5, SweepIntervalS: 30},
RateLimit: RateLimitConfig{RequestsPerMinute: 100, WindowSeconds: 60},
Search: SearchConfig{Engine: "db"},
}
err := Validate(cfg)
assert.NoError(t, err)
}
func TestValidate_SearchDBFallbackRejectedInRelease(t *testing.T) {
cfg := &Config{
Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "release"},
Database: DatabaseConfig{Host: "localhost", Name: "db", User: "user"},
Redis: RedisConfig{URL: "redis://localhost:6379"},
JWT: JWTConfig{Secret: "test-secret-key-min-32-chars!!"},
Log: LogConfig{Level: "info"},
Worker: WorkerConfig{Concurrency: 1, BlockTimeoutS: 5, SweepIntervalS: 30},
RateLimit: RateLimitConfig{RequestsPerMinute: 100, WindowSeconds: 60},
Search: SearchConfig{Engine: "db"},
}
err := Validate(cfg)
assert.Error(t, err)
assert.Contains(t, err.Error(), "release mode requires meilisearch")
}
func TestDatabaseConfig_DSN(t *testing.T) {
cfg := DatabaseConfig{
Host: "localhost",
Port: 5432,
User: "gochat",
Password: "secret",
DBName: "gochat_db",
SSLMode: "disable",
}
dsn := cfg.DSN()
assert.Contains(t, dsn, "host=localhost")
assert.Contains(t, dsn, "port=5432")
assert.Contains(t, dsn, "user=gochat")
assert.Contains(t, dsn, "dbname=gochat_db")
assert.Contains(t, dsn, "sslmode=disable")
}
func TestJWTConfig_ExpiryDuration(t *testing.T) {
cfg := JWTConfig{ExpiryHours: 24}
dur := cfg.ExpiryDuration()
assert.Equal(t, 24*time.Hour, dur)
}
func TestServerConfig_Address(t *testing.T) {
cfg := ServerConfig{Host: "0.0.0.0", Port: 3000}
addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
assert.Equal(t, "0.0.0.0:3000", addr)
}