优化结构
This commit is contained in:
@@ -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;订阅不会自动创建。"},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user