2026-06-27 00:40:30 +08:00

160 lines
7.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package config 验证 room-service 本地 YAML 配置契约。
package config
import (
"testing"
"time"
)
// TestLoad 验证 room-service 的本地 config.yaml 能被正常读取。
func TestLoad(t *testing.T) {
// 读取真实本地配置,确保端口和关键下游地址没有漂移。
cfg, err := Load("../../configs/config.yaml")
if err != nil {
t.Fatalf("Load failed: %v", err)
}
// room-service 业务只走 gRPCHTTP health 仅服务 CLB 探活,端口必须保持 13xxx 约束。
if cfg.GRPCAddr != ":13001" {
t.Fatalf("unexpected grpc addr: %s", cfg.GRPCAddr)
}
if cfg.HealthHTTPAddr != ":13101" {
t.Fatalf("unexpected health http addr: %s", cfg.HealthHTTPAddr)
}
if cfg.AdvertiseAddr != "127.0.0.1:13001" || cfg.NodeRegistryTTL != 30*time.Second || cfg.NodeRegistryHeartbeatInterval != 10*time.Second || cfg.OwnerForwardTimeout != 2*time.Second {
t.Fatalf("unexpected owner routing config: advertise=%s ttl=%s heartbeat=%s timeout=%s", cfg.AdvertiseAddr, cfg.NodeRegistryTTL, cfg.NodeRegistryHeartbeatInterval, cfg.OwnerForwardTimeout)
}
// lease TTL 是故障接管抖动窗口的基础参数。
if cfg.LeaseTTL != 10*time.Second {
t.Fatalf("unexpected lease ttl: %s", cfg.LeaseTTL)
}
if cfg.RoomListCacheRefreshInterval != 5*time.Minute {
t.Fatalf("unexpected room list cache refresh interval: %s", cfg.RoomListCacheRefreshInterval)
}
// SendGift 主链路依赖 wallet-service默认地址必须与本地编排一致。
if cfg.WalletServiceAddr != "127.0.0.1:13004" {
t.Fatalf("unexpected wallet addr: %s", cfg.WalletServiceAddr)
}
if cfg.TencentIM.SDKAppID != 20040101 || cfg.TencentIM.SecretKey == "" || cfg.TencentIM.AdminIdentifier != "administrator" {
t.Fatalf("local room-service must use test Tencent IM account: %+v", cfg.TencentIM)
}
if !cfg.OutboxWorker.Enabled || cfg.OutboxWorker.PublishMode != OutboxPublishModeMQ || cfg.OutboxWorker.PollInterval != 200*time.Millisecond || cfg.OutboxWorker.BatchSize != 50 || cfg.OutboxWorker.Concurrency != 16 || cfg.OutboxWorker.PublishTimeout != 15*time.Second || cfg.OutboxWorker.RetryStrategy != "exponential_backoff" || cfg.OutboxWorker.MaxRetryCount != 10 || cfg.OutboxWorker.InitialBackoff != 5*time.Second || cfg.OutboxWorker.MaxBackoff != 5*time.Minute {
t.Fatalf("unexpected outbox worker config: %+v", cfg.OutboxWorker)
}
// 本地默认仍走 room_outbox MQ 和火箭延迟唤醒;房间 UI IM 已改为 Room Cell 提交后直发,不再启 MQ IM bridge。
if !cfg.RocketMQ.Enabled || !cfg.RocketMQ.RoomOutbox.Enabled || cfg.RocketMQ.RoomOutbox.TencentIMConsumerEnabled || !cfg.RocketMQ.RocketLaunch.Enabled || !cfg.RocketMQ.UserOutbox.Enabled {
t.Fatalf("local config must enable room MQ fanout and disable MQ IM bridge: %+v", cfg.RocketMQ)
}
}
// TestLoadTencentExample 验证线上腾讯云 IM 模板能覆盖本地默认配置。
func TestLoadTencentExample(t *testing.T) {
cfg, err := Load("../../configs/config.tencent.example.yaml")
if err != nil {
t.Fatalf("Load tencent example failed: %v", err)
}
if !cfg.TencentIM.Enabled {
t.Fatalf("tencent example should enable Tencent IM direct publisher")
}
if cfg.TencentIM.SDKAppID == 0 || cfg.TencentIM.SecretKey == "" {
t.Fatalf("sdk_app_id and secret_key templates must be present")
}
if cfg.TencentIM.AdminIdentifier == "" {
t.Fatalf("admin_identifier template must be present")
}
if cfg.TencentIM.Endpoint == "" || cfg.TencentIM.GroupType == "" {
t.Fatalf("endpoint and group_type templates must be present")
}
if cfg.TencentIM.RequestTimeout != 5*time.Second {
t.Fatalf("unexpected request timeout: %s", cfg.TencentIM.RequestTimeout)
}
if cfg.RoomListCacheRefreshInterval != 5*time.Minute {
t.Fatalf("unexpected room list cache refresh interval: %s", cfg.RoomListCacheRefreshInterval)
}
if cfg.AdvertiseAddr == "" || cfg.AdvertiseAddr == "room-service.internal:13001" {
t.Fatalf("tencent example must use per-instance advertise_addr, got %q", cfg.AdvertiseAddr)
}
if !cfg.OutboxWorker.Enabled || cfg.OutboxWorker.PollInterval != 200*time.Millisecond || cfg.OutboxWorker.BatchSize != 50 || cfg.OutboxWorker.Concurrency != 16 || cfg.OutboxWorker.PublishTimeout != 15*time.Second || cfg.OutboxWorker.RetryStrategy != "exponential_backoff" || cfg.OutboxWorker.MaxRetryCount != 10 {
t.Fatalf("tencent example must configure outbox worker: %+v", cfg.OutboxWorker)
}
if cfg.OutboxWorker.PublishMode != OutboxPublishModeMQ || !cfg.RocketMQ.RoomOutbox.Enabled || !cfg.RocketMQ.RocketLaunch.Enabled {
t.Fatalf("tencent example must route room outbox and rocket launch through RocketMQ: mode=%s mq=%+v", cfg.OutboxWorker.PublishMode, cfg.RocketMQ)
}
}
func TestLoadDockerConfigDoesNotStartRoomIMBridgeWhenOutboxUsesMQ(t *testing.T) {
cfg, err := Load("../../configs/config.docker.yaml")
if err != nil {
t.Fatalf("Load docker config failed: %v", err)
}
if cfg.OutboxWorker.PublishMode != OutboxPublishModeMQ {
t.Fatalf("docker config should publish room outbox through MQ, got %q", cfg.OutboxWorker.PublishMode)
}
if cfg.OutboxWorker.PollInterval != 200*time.Millisecond || cfg.OutboxWorker.BatchSize != 50 || cfg.OutboxWorker.Concurrency != 16 || cfg.OutboxWorker.PublishTimeout != 15*time.Second {
t.Fatalf("docker config must run room outbox with 16 workers, short polling, and a bounded claim window: %+v", cfg.OutboxWorker)
}
if !cfg.RocketMQ.RoomOutbox.Enabled {
t.Fatalf("docker config must enable room outbox MQ")
}
if cfg.RocketMQ.RoomOutbox.TencentIMConsumerEnabled {
t.Fatalf("docker mq room outbox must not start Tencent IM bridge consumer")
}
if cfg.TencentIM.SDKAppID != 20040101 || cfg.TencentIM.AdminIdentifier != "administrator" {
t.Fatalf("docker config must use test IM account: im=%+v", cfg.TencentIM)
}
if !cfg.TencentIM.Enabled {
t.Fatalf("direct room IM publishing requires tencent_im.enabled")
}
}
func TestNormalizeRejectsUnknownOutboxRetryStrategy(t *testing.T) {
cfg := Default()
cfg.OutboxWorker.RetryStrategy = "exponential"
if _, err := Normalize(cfg); err == nil {
t.Fatal("Normalize should reject unsupported outbox retry_strategy")
}
}
func TestNormalizeRejectsUnknownOutboxPublishMode(t *testing.T) {
cfg := Default()
cfg.OutboxWorker.PublishMode = "random"
if _, err := Normalize(cfg); err == nil {
t.Fatal("Normalize should reject unsupported outbox publish_mode")
}
}
func TestNormalizeRejectsMQPublishModeWithoutRoomOutboxMQ(t *testing.T) {
cfg := Default()
cfg.OutboxWorker.PublishMode = OutboxPublishModeMQ
if _, err := Normalize(cfg); err == nil {
t.Fatal("Normalize should reject mq publish_mode without rocketmq.room_outbox.enabled")
}
}
func TestNormalizeOutboxWorkerKeepsSafeDefaults(t *testing.T) {
cfg := Default()
cfg.OutboxWorker.PollInterval = 0
cfg.OutboxWorker.BatchSize = 0
cfg.OutboxWorker.Concurrency = 0
cfg.OutboxWorker.PublishTimeout = 0
cfg.OutboxWorker.RetryStrategy = ""
cfg.OutboxWorker.MaxRetryCount = 0
cfg.OutboxWorker.InitialBackoff = 0
cfg.OutboxWorker.MaxBackoff = 0
normalized, err := Normalize(cfg)
if err != nil {
t.Fatalf("Normalize failed: %v", err)
}
if normalized.OutboxWorker.PublishMode != OutboxPublishModeDirect || normalized.OutboxWorker.PollInterval != time.Second || normalized.OutboxWorker.BatchSize != 100 || normalized.OutboxWorker.Concurrency != 1 || normalized.OutboxWorker.PublishTimeout != 3*time.Second || normalized.OutboxWorker.RetryStrategy != "exponential_backoff" || normalized.OutboxWorker.MaxRetryCount != 10 || normalized.OutboxWorker.InitialBackoff != 5*time.Second || normalized.OutboxWorker.MaxBackoff != 5*time.Minute {
t.Fatalf("outbox worker defaults not restored: %+v", normalized.OutboxWorker)
}
}