优化结构

This commit is contained in:
Yvan
2026-08-30 01:08:10 +08:00
parent 509df03d61
commit e0c35e4e9b
23 changed files with 1513 additions and 38 deletions
+1 -1
View File
@@ -11,7 +11,7 @@
- `config`:Viper 配置模型、加载、快照和热更新
- `data`:共享数据库生命周期;仓储按 `data/system`、`data/integration`、`data/task`、`data/payment` 隔离
- `initialize`:数据库首次初始化和系统种子数据编排
- `integration`:Redis、邮件、对象存储、支付、WebSocket、EMQX 和 RabbitMQ 适配器
- `integration`:Redis、邮件、对象存储、支付、WebSocket、EMQX、Kafka 和 RabbitMQ 适配器
- `routecatalog`:统一声明 HTTP 路由的公开性、操作审计、请求体策略和 API 元数据
- `server`:Gin server 组合与生命周期;横切 HTTP 代码按子包维护:
`server/handler`、`server/middleware`、`server/router`、`server/staticfiles`;
+71 -4
View File
@@ -79,6 +79,25 @@ type IntegrationConnectionTester interface {
TestIntegration(context.Context, *IntegrationConfig) error
}
type ConnectionTestError struct {
Provider string
Err error
}
func (e *ConnectionTestError) Error() string {
if e == nil || strings.TrimSpace(e.Provider) == "" {
return "连接测试失败"
}
return strings.TrimSpace(e.Provider) + " 连接测试失败"
}
func (e *ConnectionTestError) Unwrap() error {
if e == nil {
return nil
}
return e.Err
}
type IntegrationConfigUsecase struct {
repo IntegrationConfigRepo
tester IntegrationConnectionTester
@@ -165,7 +184,14 @@ func (uc *IntegrationConfigUsecase) Test(ctx context.Context, config *Integratio
if uc.tester == nil {
return errors.New("集成连接测试器未初始化")
}
return uc.tester.TestIntegration(ctx, config)
if err := uc.tester.TestIntegration(ctx, config); err != nil {
provider := config.Provider
if definition, ok := IntegrationDefinition(config.Kind, config.Provider); ok && strings.TrimSpace(definition.Name) != "" {
provider = definition.Name
}
return &ConnectionTestError{Provider: provider, Err: err}
}
return nil
}
func (uc *IntegrationConfigUsecase) Delete(ctx context.Context, kind, provider string) error {
@@ -481,11 +507,52 @@ func IsIntegrationSecretKey(key string) bool {
}
func integrationInt64(values map[string]any, key string, fallback int64) int64 {
value := integrationText(values, key)
if value == "" {
value, exists := values[key]
if !exists || value == nil {
return fallback
}
parsed, err := strconv.ParseInt(value, 10, 64)
switch typed := value.(type) {
case int:
return int64(typed)
case int8:
return int64(typed)
case int16:
return int64(typed)
case int32:
return int64(typed)
case int64:
return typed
case uint:
return parseIntegrationInt64(strconv.FormatUint(uint64(typed), 10), fallback)
case uint8:
return int64(typed)
case uint16:
return int64(typed)
case uint32:
return int64(typed)
case uint64:
return parseIntegrationInt64(strconv.FormatUint(typed, 10), fallback)
case float32:
return parseIntegrationInt64(strconv.FormatFloat(float64(typed), 'f', -1, 32), fallback)
case float64:
return parseIntegrationInt64(strconv.FormatFloat(typed, 'f', -1, 64), fallback)
case json.Number:
if parsed, err := typed.Int64(); err == nil {
return parsed
}
if parsed, err := typed.Float64(); err == nil {
return parseIntegrationInt64(strconv.FormatFloat(parsed, 'f', -1, 64), fallback)
}
return fallback
case string:
return parseIntegrationInt64(typed, fallback)
default:
return parseIntegrationInt64(fmt.Sprint(typed), fallback)
}
}
func parseIntegrationInt64(value string, fallback int64) int64 {
parsed, err := strconv.ParseInt(strings.TrimSpace(value), 10, 64)
if err != nil {
return fallback
}
@@ -3,6 +3,7 @@ package integration
import (
"context"
"encoding/json"
"errors"
"testing"
)
@@ -27,17 +28,19 @@ func (*integrationConfigRepoTestDouble) DeleteIntegrationConfig(context.Context,
type integrationConnectionTesterDouble struct {
calls int
config *IntegrationConfig
err error
}
func (t *integrationConnectionTesterDouble) TestIntegration(_ context.Context, config *IntegrationConfig) error {
t.calls++
t.config = config
return nil
return t.err
}
func TestCommunicationIntegrationDefinitionsAndValidation(t *testing.T) {
for _, target := range []struct{ kind, provider string }{
{IntegrationKindMQ, "emqx"},
{IntegrationKindMQ, "kafka"},
{IntegrationKindMQ, "rabbitmq"},
{IntegrationKindWebSocket, "melody"},
} {
@@ -51,12 +54,49 @@ func TestCommunicationIntegrationDefinitionsAndValidation(t *testing.T) {
}
}
func TestKafkaDefaultConfigSurvivesJSONRoundTrip(t *testing.T) {
raw, err := json.Marshal(DefaultIntegrationConfig(IntegrationKindMQ, "kafka"))
if err != nil {
t.Fatal(err)
}
values, err := decodeIntegrationObject(raw)
if err != nil {
t.Fatal(err)
}
if got := integrationInt64(values, "max_bytes", 0); got != 10485760 {
t.Fatalf("max_bytes = %d, want 10485760; decoded=%#v", got, values["max_bytes"])
}
if err = ValidateIntegrationConfig(IntegrationKindMQ, "kafka", values); err != nil {
t.Fatalf("JSON-decoded kafka defaults are invalid: %v", err)
}
}
func TestIntegrationInt64RejectsFractionalAndOverflowValues(t *testing.T) {
values := map[string]any{"fractional": 1.5, "overflow": json.Number("9223372036854775808")}
if got := integrationInt64(values, "fractional", -1); got != -1 {
t.Fatalf("fractional value = %d, want fallback", got)
}
if got := integrationInt64(values, "overflow", -1); got != -1 {
t.Fatalf("overflow value = %d, want fallback", got)
}
}
func TestCommunicationIntegrationValidationRejectsInvalidValues(t *testing.T) {
rabbit := DefaultIntegrationConfig(IntegrationKindMQ, "rabbitmq")
rabbit["port"] = 0
if err := ValidateIntegrationConfig(IntegrationKindMQ, "rabbitmq", rabbit); err == nil {
t.Fatal("invalid rabbitmq port was accepted")
}
kafka := DefaultIntegrationConfig(IntegrationKindMQ, "kafka")
kafka["brokers"] = []string{"missing-port"}
if err := ValidateIntegrationConfig(IntegrationKindMQ, "kafka", kafka); err == nil {
t.Fatal("invalid kafka broker was accepted")
}
kafka = DefaultIntegrationConfig(IntegrationKindMQ, "kafka")
kafka["max_bytes"] = 0
if err := ValidateIntegrationConfig(IntegrationKindMQ, "kafka", kafka); err == nil {
t.Fatal("invalid kafka byte limits were accepted")
}
websocket := DefaultIntegrationConfig(IntegrationKindWebSocket, "melody")
websocket["path"] = "ws"
if err := ValidateIntegrationConfig(IntegrationKindWebSocket, "melody", websocket); err == nil {
@@ -97,6 +137,31 @@ func TestIntegrationConfigTestDoesNotPersistCandidate(t *testing.T) {
}
}
func TestIntegrationConfigTestReturnsSafeProviderFailure(t *testing.T) {
underlying := errors.New("dial tcp 127.0.0.1:9092: connection refused")
for _, test := range []struct {
provider string
name string
}{
{provider: "kafka", name: "Kafka"},
{provider: "emqx", name: "EMQX"},
{provider: "rabbitmq", name: "RabbitMQ"},
} {
t.Run(test.provider, func(t *testing.T) {
values := DefaultIntegrationConfig(IntegrationKindMQ, test.provider)
raw, _ := json.Marshal(values)
usecase := NewIntegrationConfigUsecase(&integrationConfigRepoTestDouble{}, &integrationConnectionTesterDouble{err: underlying})
err := usecase.Test(context.Background(), &IntegrationConfig{Kind: IntegrationKindMQ, Provider: test.provider, Values: raw})
if err == nil || err.Error() != test.name+" 连接测试失败" {
t.Fatalf("Test() error = %v", err)
}
if !errors.Is(err, underlying) {
t.Fatalf("Test() error does not wrap the provider failure: %v", err)
}
})
}
}
func TestIntegrationConfigRejectsJSONNull(t *testing.T) {
repo := &integrationConfigRepoTestDouble{}
usecase := NewIntegrationConfigUsecase(repo, &integrationConnectionTesterDouble{})
@@ -117,7 +117,7 @@ var integrationDefinitions = map[string][]IntegrationConfigDefinition{
{Key: "brokers", Label: "Broker 地址", Type: "string-list", Required: true, Placeholder: "127.0.0.1:9092", Description: "每行一个 host:port 地址。"},
{Key: "client_id", Label: "客户端 ID", Type: "text", Required: true, Placeholder: "kra"},
{Key: "group_id", Label: "消费组 ID", Type: "text", Required: true, Placeholder: "kra"},
{Key: "username", Label: "SASL 用户名", Type: "text"},
{Key: "username", Label: "SASL 用户名", Type: "text", Description: "当前使用 SASL/PLAIN;请与密码同时配置。"},
{Key: "password", Label: "SASL 密码", Type: "password", Secret: true},
{Key: "tls", Label: "启用 TLS", Type: "switch"},
{Key: "tls_skip_verify", Label: "跳过 TLS 证书校验", Type: "switch", Description: "仅用于受控测试环境。"},
@@ -127,7 +127,7 @@ var integrationDefinitions = map[string][]IntegrationConfigDefinition{
{Key: "max_wait", Label: "最大拉取等待(秒)", Type: "number", Required: true},
{Key: "connect_timeout", Label: "连接超时(秒)", Type: "number", Required: true},
{Key: "reconnect_interval", Label: "重连间隔(秒)", Type: "number", Required: true},
{Key: "allow_auto_topic_creation", Label: "允许自动创建 Topic", Type: "switch"},
{Key: "allow_auto_topic_creation", Label: "允许自动创建 Topic", Type: "switch", Description: "仅在生产消息时允许创建不存在的 Topic;订阅不会自动创建。"},
},
},
{
@@ -51,3 +51,93 @@ func TestIntegrationConfigSavePublishesUnmaskedRuntimeValues(t *testing.T) {
t.Fatalf("runtime password = %#v", stored["password"])
}
}
func TestCommunicationDefaultsIncludeKafka(t *testing.T) {
db, err := openWithDriver("sqlite", "file:"+t.Name()+"?mode=memory&cache=shared")
if err != nil {
t.Fatal(err)
}
if err = db.AutoMigrate(&ConfigPO{}); err != nil {
t.Fatal(err)
}
if err = ensureCommunicationIntegrationConfigs(db); err != nil {
t.Fatal(err)
}
var row ConfigPO
if err = db.Where("kind = ? AND provider = ?", integrationbiz.IntegrationKindMQ, "kafka").First(&row).Error; err != nil {
t.Fatal(err)
}
if row.Enabled || row.Config == "" {
t.Fatalf("kafka default row = %#v", row)
}
}
func TestKafkaDefaultMigrationDoesNotRecreateOtherProviders(t *testing.T) {
db, err := openWithDriver("sqlite", "file:"+t.Name()+"?mode=memory&cache=shared")
if err != nil {
t.Fatal(err)
}
if err = db.AutoMigrate(&ConfigPO{}); err != nil {
t.Fatal(err)
}
if err = ensureKafkaIntegrationConfig(db); err != nil {
t.Fatal(err)
}
var kafkaRows, otherRows int64
if err = db.Model(&ConfigPO{}).Where("kind = ? AND provider = ?", integrationbiz.IntegrationKindMQ, "kafka").Count(&kafkaRows).Error; err != nil {
t.Fatal(err)
}
if err = db.Model(&ConfigPO{}).Where("provider <> ?", "kafka").Count(&otherRows).Error; err != nil {
t.Fatal(err)
}
if kafkaRows != 1 || otherRows != 0 {
t.Fatalf("migration rows kafka=%d other=%d", kafkaRows, otherRows)
}
}
func TestCommunicationDefaultUpgradeRepairsRequiredEmptyFields(t *testing.T) {
db, err := openWithDriver("sqlite", "file:"+t.Name()+"?mode=memory&cache=shared")
if err != nil {
t.Fatal(err)
}
if err = db.AutoMigrate(&ConfigPO{}); err != nil {
t.Fatal(err)
}
legacy := `{"enabled":false,"broker":"","client_id":"","username":"","password":"","keep_alive":30,"clean_session":true,"connect_timeout":10}`
if err = db.Create(&ConfigPO{Kind: integrationbiz.IntegrationKindMQ, Provider: "emqx", Config: legacy}).Error; err != nil {
t.Fatal(err)
}
if err = upgradeCommunicationIntegrationDefaults(db); err != nil {
t.Fatal(err)
}
var row ConfigPO
if err = db.Where("kind = ? AND provider = ?", integrationbiz.IntegrationKindMQ, "emqx").First(&row).Error; err != nil {
t.Fatal(err)
}
values := map[string]any{}
if err = json.Unmarshal([]byte(row.Config), &values); err != nil {
t.Fatal(err)
}
if values["broker"] != "tcp://127.0.0.1:1883" || values["client_id"] != "kra" || values["reconnect_interval"] != float64(5) {
t.Fatalf("upgraded emqx values = %#v", values)
}
if values["username"] != "" || values["password"] != "" {
t.Fatalf("optional credentials were overwritten: %#v", values)
}
if _, exists := values["enabled"]; exists {
t.Fatalf("legacy enabled field remained: %#v", values)
}
if err = db.Delete(&row).Error; err != nil {
t.Fatal(err)
}
if err = upgradeCommunicationIntegrationDefaults(db); err != nil {
t.Fatal(err)
}
var count int64
if err = db.Model(&ConfigPO{}).Where("kind = ? AND provider = ?", integrationbiz.IntegrationKindMQ, "emqx").Count(&count).Error; err != nil {
t.Fatal(err)
}
if count != 0 {
t.Fatalf("upgrade recreated deleted emqx row: %d", count)
}
}
+81 -14
View File
@@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"reflect"
"strings"
integrationbiz "kra/internal/biz/integration"
"kra/pkg/database/migration"
@@ -18,7 +19,8 @@ func Migrations() []migration.Step {
}},
{ID: "202608210001_communication_integration_defaults", Migrate: ensureCommunicationIntegrationConfigs},
{ID: "202608200004_payment_defaults", Migrate: ensurePaymentIntegrationConfigs},
{ID: "202608290004_kafka_integration_default", Migrate: ensureCommunicationIntegrationConfigs},
{ID: "202608290004_kafka_integration_default", Migrate: ensureKafkaIntegrationConfig},
{ID: "202608290005_communication_integration_default_upgrade", Migrate: upgradeCommunicationIntegrationDefaults},
}
}
@@ -30,25 +32,90 @@ func ensureCommunicationIntegrationConfigs(db *gorm.DB) error {
{integrationbiz.IntegrationKindWebSocket, "melody"},
}
for _, item := range defaults {
var row ConfigPO
err := db.Where("kind = ? AND provider = ?", item.kind, item.provider).First(&row).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
values, marshalErr := json.Marshal(integrationbiz.DefaultIntegrationConfig(item.kind, item.provider))
if marshalErr != nil {
return marshalErr
}
if err = db.Create(&ConfigPO{Kind: item.kind, Provider: item.provider, Enabled: false, Config: string(values)}).Error; err != nil {
return err
}
continue
}
if err != nil {
if err := ensureIntegrationConfig(db, item.kind, item.provider); err != nil {
return err
}
}
return nil
}
func ensureKafkaIntegrationConfig(db *gorm.DB) error {
return ensureIntegrationConfig(db, integrationbiz.IntegrationKindMQ, "kafka")
}
func ensureIntegrationConfig(db *gorm.DB, kind, provider string) error {
var row ConfigPO
err := db.Where("kind = ? AND provider = ?", kind, provider).First(&row).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
values, marshalErr := json.Marshal(integrationbiz.DefaultIntegrationConfig(kind, provider))
if marshalErr != nil {
return marshalErr
}
return db.Create(&ConfigPO{Kind: kind, Provider: provider, Enabled: false, Config: string(values)}).Error
}
return err
}
func upgradeCommunicationIntegrationDefaults(db *gorm.DB) error {
for _, kind := range []string{integrationbiz.IntegrationKindMQ, integrationbiz.IntegrationKindWebSocket} {
for _, definition := range integrationbiz.IntegrationDefinitions(kind) {
var row ConfigPO
err := db.Where("kind = ? AND provider = ?", kind, definition.Provider).First(&row).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
continue
}
if err != nil {
return err
}
values := integrationObject(json.RawMessage(row.Config))
changed := false
for key, value := range definition.Defaults {
if _, exists := values[key]; !exists {
values[key] = value
changed = true
}
}
for _, field := range definition.Fields {
if !field.Required || !emptyIntegrationConfigValue(values[field.Key]) || emptyIntegrationConfigValue(definition.Defaults[field.Key]) {
continue
}
values[field.Key] = definition.Defaults[field.Key]
changed = true
}
if _, exists := values["enabled"]; exists {
delete(values, "enabled")
changed = true
}
if !changed {
continue
}
encoded, marshalErr := json.Marshal(values)
if marshalErr != nil {
return marshalErr
}
if err := db.Model(&row).Update("config", string(encoded)).Error; err != nil {
return err
}
}
}
return nil
}
func emptyIntegrationConfigValue(value any) bool {
switch typed := value.(type) {
case nil:
return true
case string:
return strings.TrimSpace(typed) == ""
case []any:
return len(typed) == 0
case []string:
return len(typed) == 0
default:
return false
}
}
func ensurePaymentIntegrationConfigs(db *gorm.DB) error {
for _, definition := range integrationbiz.IntegrationDefinitions(integrationbiz.IntegrationKindPayment) {
provider := definition.Provider
+4 -4
View File
@@ -34,15 +34,15 @@ func TestMigrateAllRunsModuleSchemasWithoutBootstrapSeed(t *testing.T) {
if err = db.Table(migration.TableName).Count(&versions).Error; err != nil {
t.Fatal(err)
}
if versions != 11 {
t.Fatalf("migration versions = %d, want 11", versions)
if versions != 13 {
t.Fatalf("migration versions = %d, want 13", versions)
}
var communicationRows []dataintegration.ConfigPO
if err = db.Where("kind IN ?", []string{"mq", "websocket"}).Order("kind, provider").Find(&communicationRows).Error; err != nil {
t.Fatal(err)
}
if len(communicationRows) != 3 {
t.Fatalf("communication integration rows = %d, want 3", len(communicationRows))
if len(communicationRows) != 4 {
t.Fatalf("communication integration rows = %d, want 4", len(communicationRows))
}
var paymentRows int64
if err = db.Model(&dataintegration.ConfigPO{}).Where("kind = ?", "payment").Count(&paymentRows).Error; err != nil {
+1 -1
View File
@@ -74,7 +74,7 @@ func (t *ConnectivityTester) restoreMaskedSecrets(kind, provider string, values
}
for key := range masked {
value, _ := values[key].(string)
if !config.IsMaskedSecret(strings.TrimSpace(value)) {
if strings.TrimSpace(value) != config.MaskedSecret {
continue
}
prior, _ := currentValues[key].(string)
@@ -0,0 +1,94 @@
package integration
import (
"context"
"encoding/json"
"fmt"
"net"
"os"
"strconv"
"strings"
"testing"
"time"
integrationbiz "kra/internal/biz/integration"
)
type kafkaConnectivityRepo struct{}
func (*kafkaConnectivityRepo) ListIntegrationConfigs(context.Context, string) ([]*integrationbiz.IntegrationConfig, error) {
return nil, nil
}
func (*kafkaConnectivityRepo) FindIntegrationConfig(context.Context, string, string) (*integrationbiz.IntegrationConfig, error) {
return nil, nil
}
func (*kafkaConnectivityRepo) SaveIntegrationConfig(context.Context, *integrationbiz.IntegrationConfig) error {
return nil
}
func (*kafkaConnectivityRepo) DeleteIntegrationConfig(context.Context, string, string) error {
return nil
}
func TestKafkaConnectivityIntegration(t *testing.T) {
rawBrokers := strings.TrimSpace(os.Getenv("KRA_KAFKA_TEST_BROKERS"))
if rawBrokers == "" {
t.Skip("KRA_KAFKA_TEST_BROKERS is not configured")
}
brokers := make([]string, 0)
for _, broker := range strings.Split(rawBrokers, ",") {
if broker = strings.TrimSpace(broker); broker != "" {
brokers = append(brokers, broker)
}
}
values := integrationbiz.DefaultIntegrationConfig(integrationbiz.IntegrationKindMQ, "kafka")
values["brokers"] = brokers
values["client_id"] = fmt.Sprintf("kra-connectivity-%d", time.Now().UnixNano())
values["group_id"] = fmt.Sprintf("kra-connectivity-%d", time.Now().UnixNano())
testMQConnectivityIntegration(t, "kafka", values)
}
func TestEMQXConnectivityIntegration(t *testing.T) {
broker := strings.TrimSpace(os.Getenv("KRA_EMQX_TEST_BROKER"))
if broker == "" {
t.Skip("KRA_EMQX_TEST_BROKER is not configured")
}
values := integrationbiz.DefaultIntegrationConfig(integrationbiz.IntegrationKindMQ, "emqx")
values["broker"] = broker
values["client_id"] = fmt.Sprintf("kra-connectivity-%d", time.Now().UnixNano())
testMQConnectivityIntegration(t, "emqx", values)
}
func TestRabbitMQConnectivityIntegration(t *testing.T) {
address := strings.TrimSpace(os.Getenv("KRA_RABBITMQ_TEST_ADDR"))
if address == "" {
t.Skip("KRA_RABBITMQ_TEST_ADDR is not configured")
}
host, portText, err := net.SplitHostPort(address)
if err != nil {
t.Fatal(err)
}
port, err := strconv.Atoi(portText)
if err != nil {
t.Fatal(err)
}
values := integrationbiz.DefaultIntegrationConfig(integrationbiz.IntegrationKindMQ, "rabbitmq")
values["host"] = host
values["port"] = port
testMQConnectivityIntegration(t, "rabbitmq", values)
}
func testMQConnectivityIntegration(t *testing.T, provider string, values map[string]any) {
t.Helper()
raw, err := json.Marshal(values)
if err != nil {
t.Fatal(err)
}
usecase := integrationbiz.NewIntegrationConfigUsecase(&kafkaConnectivityRepo{}, NewConnectivityTester(nil))
if err = usecase.Test(context.Background(), &integrationbiz.IntegrationConfig{
Kind: integrationbiz.IntegrationKindMQ,
Provider: provider,
Values: raw,
}); err != nil {
t.Fatalf("test JSON-decoded %s defaults: %v", provider, err)
}
}
+11
View File
@@ -46,6 +46,17 @@ func TestConnectivityTesterRestoreMaskedSecrets(t *testing.T) {
}
})
t.Run("keeps an empty optional secret", func(t *testing.T) {
values := map[string]any{"password": ""}
if err := tester.restoreMaskedSecrets("mq", "kafka", values); err != nil {
t.Fatalf("restoreMaskedSecrets() error = %v", err)
}
if got := values["password"]; got != "" {
t.Fatalf("password = %q, want empty value", got)
}
})
t.Run("does not treat non-secret fields as masked secrets", func(t *testing.T) {
values := map[string]any{
"username": "******",
+10 -1
View File
@@ -279,7 +279,13 @@ func configStrings(values map[string]any, key string) []string {
}
stringsValue, ok := values[key].([]string)
if ok {
return append([]string(nil), stringsValue...)
result := make([]string, 0, len(stringsValue))
for _, item := range stringsValue {
if value := strings.TrimSpace(item); value != "" {
result = append(result, value)
}
}
return result
}
return nil
}
@@ -327,6 +333,9 @@ func (r *Reloadable) retryOnce() {
client := r.clientLocked(provider)
if client != nil && client.Connected() {
if r.pending[provider] {
if retryAt := r.nextRetry[provider]; !retryAt.IsZero() && now.Before(retryAt) {
continue
}
if err := r.reconcileProviderLocked(provider); err != nil {
r.scheduleRetryLocked(provider, config.Values)
}
+67
View File
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"log/slog"
"strings"
"testing"
"time"
@@ -209,3 +210,69 @@ func TestReloadableLegacySubscribeDoesNotLeaveOfflineDeclaration(t *testing.T) {
t.Fatalf("offline legacy declaration remained: %#v", r.subscriptions[ProviderEMQX])
}
}
func TestReloadableExposesKafkaNamedClient(t *testing.T) {
r := &Reloadable{}
client := r.Client(" KAFKA ")
if client == nil {
t.Fatal("kafka named client is nil")
}
if client.Connected() {
t.Fatal("unconfigured kafka named client reported connected")
}
}
func TestConfigStringsDecodesJSONArrays(t *testing.T) {
values := map[string]any{"brokers": []any{" kafka-1:9092 ", "", "kafka-2:9092"}}
brokers := configStrings(values, "brokers")
if len(brokers) != 2 || brokers[0] != "kafka-1:9092" || brokers[1] != "kafka-2:9092" {
t.Fatalf("brokers = %#v", brokers)
}
}
func TestConfigStringsNormalizesTypedSlices(t *testing.T) {
values := map[string]any{"brokers": []string{" kafka-1:9092 ", "", "kafka-2:9092"}}
brokers := configStrings(values, "brokers")
if len(brokers) != 2 || brokers[0] != "kafka-1:9092" || brokers[1] != "kafka-2:9092" {
t.Fatalf("brokers = %#v", brokers)
}
}
func TestReloadableRejectsUnsupportedKafkaQoS(t *testing.T) {
r := &Reloadable{}
err := r.Register(platformmq.SubscriptionSet{
Owner: "orders",
Provider: ProviderKafka,
Topics: []platformmq.TopicSubscription{{Topic: "orders.created", QoS: platformmq.ExactlyOnce, Handler: func(context.Context, platformmq.Message) {}}},
})
if err == nil || !strings.Contains(err.Error(), "supports qos 0 or 1") {
t.Fatalf("Register() error = %v", err)
}
}
func TestRetryOnceHonorsPendingSubscriptionDeadline(t *testing.T) {
client := &fakeClient{}
r := &Reloadable{
clients: map[string]platformmq.Client{ProviderKafka: client},
configs: map[string]runtimeconfig.Config{
ProviderKafka: {Kind: "mq", Provider: ProviderKafka, Enabled: true},
},
subscriptions: map[string]map[string]map[string]subscription{
ProviderKafka: {
"orders.created": {"orders": {qos: platformmq.AtLeastOnce, handler: func(context.Context, platformmq.Message) {}}},
},
},
bindings: map[string]map[string]byte{ProviderKafka: {}},
pending: map[string]bool{ProviderKafka: true},
nextRetry: map[string]time.Time{ProviderKafka: time.Now().Add(time.Minute)},
}
r.retryOnce()
if len(client.subscribed) != 0 {
t.Fatalf("subscription retried before deadline: %v", client.subscribed)
}
r.nextRetry[ProviderKafka] = time.Now().Add(-time.Second)
r.retryOnce()
if len(client.subscribed) != 1 || client.subscribed[0] != "orders.created" {
t.Fatalf("subscription was not retried after deadline: %v", client.subscribed)
}
}
+2 -2
View File
@@ -25,8 +25,8 @@ func TestCatalogContainsBuiltInModulesInDependencyOrder(t *testing.T) {
t.Fatalf("definition[%d] = %q, want %q", index, got, name)
}
}
if got := catalog.MigrationSteps(); len(got) != 11 {
t.Fatalf("module migrations = %d, want 11", len(got))
if got := catalog.MigrationSteps(); len(got) != 13 {
t.Fatalf("module migrations = %d, want 13", len(got))
}
var menus, apis int
for _, definition := range catalog.Definitions {
@@ -4,7 +4,7 @@ import "testing"
func TestDefinitionOwnsCommunicationSurface(t *testing.T) {
definition := Definition()
if definition.Name != "integration" || len(definition.Migrations) != 3 || len(definition.Surface.Menus) != 1 || len(definition.Surface.APIs) != 5 {
if definition.Name != "integration" || len(definition.Migrations) != 5 || len(definition.Surface.Menus) != 1 || len(definition.Surface.APIs) != 5 {
t.Fatalf("integration surface = %#v", definition.Surface)
}
}
+8
View File
@@ -33,6 +33,14 @@ func TestResponseHelpers(t *testing.T) {
}
}
func TestSanitizeFailureMessageKeepsSafeConnectionSummary(t *testing.T) {
for _, message := range []string{"Kafka 连接测试失败", "EMQX 连接测试失败", "RabbitMQ 连接测试失败"} {
if got := sanitizeFailureMessage(message); got != message {
t.Fatalf("sanitizeFailureMessage(%q) = %q", message, got)
}
}
}
func TestSetCookieUsesRequestedName(t *testing.T) {
gin.SetMode(gin.TestMode)
r := gin.New()