mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-09 16:23:27 +08:00
- 支持 Nacos v1、v2、v3 就绪探测与受限账号连接 - 强化认证、缓存、单飞连接和关闭代际的并发边界 - 修复监听重复事件、导入身份碰撞及部分失败误报成功 - 补齐只读保护、结构化错误与六语言提示
422 lines
13 KiB
Go
422 lines
13 KiB
Go
package app
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"path/filepath"
|
||
"strings"
|
||
"sync/atomic"
|
||
"testing"
|
||
|
||
"GoNavi-Wails/internal/connection"
|
||
"GoNavi-Wails/internal/nacos"
|
||
)
|
||
|
||
type nacosTransferTestClient struct {
|
||
nacos.Client
|
||
getConfig func(context.Context, string, string, string) (*nacos.ConfigDetail, error)
|
||
publish func(context.Context, nacos.PublishRequest) error
|
||
closed atomic.Int32
|
||
}
|
||
|
||
func (client *nacosTransferTestClient) Connect(connection.ConnectionConfig) error {
|
||
return nil
|
||
}
|
||
|
||
func (client *nacosTransferTestClient) Close() error {
|
||
client.closed.Add(1)
|
||
return nil
|
||
}
|
||
|
||
func (client *nacosTransferTestClient) GetConfig(
|
||
ctx context.Context,
|
||
namespaceID, group, dataID string,
|
||
) (*nacos.ConfigDetail, error) {
|
||
if client.getConfig != nil {
|
||
return client.getConfig(ctx, namespaceID, group, dataID)
|
||
}
|
||
return nil, errors.New("Config not found: test fixture")
|
||
}
|
||
|
||
func (client *nacosTransferTestClient) PublishConfig(ctx context.Context, request nacos.PublishRequest) error {
|
||
if client.publish != nil {
|
||
return client.publish(ctx, request)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func TestNacosImportConfigsRejectsEmptyEffectiveSelection(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
items []NacosConfigIdentity
|
||
}{
|
||
{name: "empty", items: nil},
|
||
{
|
||
name: "all invalid",
|
||
items: []NacosConfigIdentity{
|
||
{DataID: " ", Group: "DEFAULT_GROUP"},
|
||
{DataID: "", Group: "DEV_GROUP"},
|
||
},
|
||
},
|
||
}
|
||
|
||
for _, test := range tests {
|
||
t.Run(test.name, func(t *testing.T) {
|
||
installNacosCacheTestHooks(t)
|
||
var published atomic.Int32
|
||
client := &nacosTransferTestClient{
|
||
publish: func(context.Context, nacos.PublishRequest) error {
|
||
published.Add(1)
|
||
return nil
|
||
},
|
||
}
|
||
newNacosClientFunc = func() nacos.Client { return client }
|
||
|
||
transfer := nacos.NewTransferFile("dev", "Development")
|
||
transfer.Configs = []nacos.TransferConfigEntry{{
|
||
DataID: "application.yaml",
|
||
Group: "DEFAULT_GROUP",
|
||
Content: "enabled: true",
|
||
}}
|
||
filename := filepath.Join(t.TempDir(), "selected-import.json")
|
||
if err := nacos.WriteTransferFile(filename, transfer); err != nil {
|
||
t.Fatalf("WriteTransferFile: %v", err)
|
||
}
|
||
|
||
result := (&App{}).NacosImportConfigs(connection.ConnectionConfig{
|
||
Type: "nacos",
|
||
Host: "nacos.example.test",
|
||
Port: 8848,
|
||
Timeout: 1,
|
||
}, NacosImportConfigsOptions{
|
||
NamespaceID: "dev",
|
||
ConflictMode: "overwrite",
|
||
File: filename,
|
||
Scope: "selected",
|
||
Items: test.items,
|
||
})
|
||
if result.Success {
|
||
t.Fatalf("selected import unexpectedly succeeded: %#v", result)
|
||
}
|
||
if got := published.Load(); got != 0 {
|
||
t.Fatalf("selected import published %d config(s), want 0", got)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestNacosImportConfigsMatchesSelectedIdentityWithoutDelimiterCollisions(t *testing.T) {
|
||
installNacosCacheTestHooks(t)
|
||
|
||
var published []nacos.PublishRequest
|
||
client := &nacosTransferTestClient{
|
||
publish: func(_ context.Context, request nacos.PublishRequest) error {
|
||
published = append(published, request)
|
||
return nil
|
||
},
|
||
}
|
||
newNacosClientFunc = func() nacos.Client { return client }
|
||
|
||
transfer := nacos.NewTransferFile("dev", "Development")
|
||
transfer.Configs = []nacos.TransferConfigEntry{
|
||
{Group: "A", DataID: "B@@C", Content: "first"},
|
||
{Group: "A@@B", DataID: "C", Content: "second"},
|
||
}
|
||
filename := filepath.Join(t.TempDir(), "delimiter-selection.json")
|
||
if err := nacos.WriteTransferFile(filename, transfer); err != nil {
|
||
t.Fatalf("WriteTransferFile: %v", err)
|
||
}
|
||
selectedIndex := 1
|
||
|
||
result := (&App{}).NacosImportConfigs(connection.ConnectionConfig{
|
||
Type: "nacos",
|
||
Host: "nacos.example.test",
|
||
Port: 8848,
|
||
}, NacosImportConfigsOptions{
|
||
NamespaceID: "dev",
|
||
ConflictMode: "overwrite",
|
||
File: filename,
|
||
Scope: "selected",
|
||
Items: []NacosConfigIdentity{{
|
||
Index: &selectedIndex,
|
||
Group: "A@@B",
|
||
DataID: "C",
|
||
}},
|
||
})
|
||
if !result.Success {
|
||
t.Fatalf("selected import failed: %#v", result)
|
||
}
|
||
if len(published) != 1 {
|
||
t.Fatalf("published %d configs, want exactly 1", len(published))
|
||
}
|
||
if published[0].Group != "A@@B" || published[0].DataID != "C" || published[0].Content != "second" {
|
||
t.Fatalf("published %#v, want the second preview row", published[0])
|
||
}
|
||
}
|
||
|
||
func TestNacosImportConfigsRejectsSelectedItemsThatDoNotMatchTheFile(t *testing.T) {
|
||
installNacosCacheTestHooks(t)
|
||
|
||
var published atomic.Int32
|
||
client := &nacosTransferTestClient{
|
||
publish: func(context.Context, nacos.PublishRequest) error {
|
||
published.Add(1)
|
||
return nil
|
||
},
|
||
}
|
||
newNacosClientFunc = func() nacos.Client { return client }
|
||
|
||
transfer := nacos.NewTransferFile("dev", "Development")
|
||
transfer.Configs = []nacos.TransferConfigEntry{{
|
||
Group: "DEFAULT_GROUP", DataID: "application.yaml", Content: "enabled: true",
|
||
}}
|
||
filename := filepath.Join(t.TempDir(), "selection-mismatch.json")
|
||
if err := nacos.WriteTransferFile(filename, transfer); err != nil {
|
||
t.Fatalf("WriteTransferFile: %v", err)
|
||
}
|
||
|
||
result := (&App{}).NacosImportConfigs(connection.ConnectionConfig{
|
||
Type: "nacos",
|
||
Host: "nacos.example.test",
|
||
Port: 8848,
|
||
}, NacosImportConfigsOptions{
|
||
NamespaceID: "dev",
|
||
File: filename,
|
||
Scope: "selected",
|
||
Items: []NacosConfigIdentity{{
|
||
Group: "DEFAULT_GROUP", DataID: "missing.yaml",
|
||
}},
|
||
})
|
||
if result.Success {
|
||
t.Fatalf("mismatched selected import unexpectedly succeeded: %#v", result)
|
||
}
|
||
if got := published.Load(); got != 0 {
|
||
t.Fatalf("mismatched selected import published %d config(s)", got)
|
||
}
|
||
}
|
||
|
||
func TestNacosImportConfigsReportsPartialFailures(t *testing.T) {
|
||
installNacosCacheTestHooks(t)
|
||
|
||
client := &nacosTransferTestClient{
|
||
publish: func(_ context.Context, request nacos.PublishRequest) error {
|
||
if request.DataID == "failed.yaml" {
|
||
return errors.New("publish denied")
|
||
}
|
||
return nil
|
||
},
|
||
}
|
||
newNacosClientFunc = func() nacos.Client { return client }
|
||
|
||
transfer := nacos.NewTransferFile("dev", "Development")
|
||
transfer.Configs = []nacos.TransferConfigEntry{
|
||
{Group: "DEFAULT_GROUP", DataID: "ok.yaml", Content: "ok"},
|
||
{Group: "DEFAULT_GROUP", DataID: "failed.yaml", Content: "failed"},
|
||
}
|
||
filename := filepath.Join(t.TempDir(), "partial-failure.json")
|
||
if err := nacos.WriteTransferFile(filename, transfer); err != nil {
|
||
t.Fatalf("WriteTransferFile: %v", err)
|
||
}
|
||
|
||
result := (&App{}).NacosImportConfigs(connection.ConnectionConfig{
|
||
Type: "nacos",
|
||
Host: "nacos.example.test",
|
||
Port: 8848,
|
||
}, NacosImportConfigsOptions{
|
||
NamespaceID: "dev",
|
||
ConflictMode: "overwrite",
|
||
File: filename,
|
||
Scope: "all",
|
||
})
|
||
if result.Success {
|
||
t.Fatalf("partial import unexpectedly reported success: %#v", result)
|
||
}
|
||
counts, ok := result.Data.(map[string]any)
|
||
if !ok {
|
||
t.Fatalf("partial import data = %#v, want count map", result.Data)
|
||
}
|
||
if counts["imported"] != 1 || counts["failed"] != 1 {
|
||
t.Fatalf("partial import counts = %#v, want imported=1 failed=1", counts)
|
||
}
|
||
}
|
||
|
||
func TestBuildNacosImportPreviewFailsWhenExistenceCheckIsForbidden(t *testing.T) {
|
||
messages := []string{
|
||
"Nacos HTTP error 403: permission denied; config not found",
|
||
"Nacos HTTP 错误 403:权限不足;配置不存在",
|
||
"Nacos HTTP 錯誤 403:權限不足;設定不存在",
|
||
"Nacos HTTP エラー 403: 権限がありません; 設定が見つかりません",
|
||
"Nacos-HTTP-Fehler 403: Keine Berechtigung; Konfiguration nicht gefunden",
|
||
"Ошибка HTTP Nacos 403: нет разрешения; Конфигурация не найдена",
|
||
}
|
||
for _, forbiddenMessage := range messages {
|
||
t.Run(forbiddenMessage, func(t *testing.T) {
|
||
client := &nacosTransferTestClient{
|
||
getConfig: func(context.Context, string, string, string) (*nacos.ConfigDetail, error) {
|
||
return nil, errors.New(forbiddenMessage)
|
||
},
|
||
}
|
||
payload := nacos.NewTransferFile("source", "Source")
|
||
payload.Configs = []nacos.TransferConfigEntry{{
|
||
DataID: "application.yaml",
|
||
Group: "DEFAULT_GROUP",
|
||
}}
|
||
|
||
_, err := buildNacosImportPreview(context.Background(), client, "import.json", "target", payload)
|
||
if err == nil {
|
||
t.Fatal("preview unexpectedly treated forbidden existence check as missing config")
|
||
}
|
||
if !strings.Contains(err.Error(), "403") {
|
||
t.Fatalf("preview error = %v, want forbidden existence check error", err)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestBuildNacosImportPreviewFailsOnNonNotFoundErrors(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
err error
|
||
}{
|
||
{name: "timeout", err: context.DeadlineExceeded},
|
||
{name: "unrelated number", err: errors.New("upstream port 4040 is unavailable")},
|
||
}
|
||
for _, test := range tests {
|
||
t.Run(test.name, func(t *testing.T) {
|
||
client := &nacosTransferTestClient{
|
||
getConfig: func(context.Context, string, string, string) (*nacos.ConfigDetail, error) {
|
||
return nil, test.err
|
||
},
|
||
}
|
||
payload := nacos.NewTransferFile("source", "Source")
|
||
payload.Configs = []nacos.TransferConfigEntry{{
|
||
DataID: "application.yaml",
|
||
Group: "DEFAULT_GROUP",
|
||
}}
|
||
|
||
_, err := buildNacosImportPreview(context.Background(), client, "import.json", "target", payload)
|
||
if err == nil {
|
||
t.Fatalf("preview unexpectedly treated %v as missing config", test.err)
|
||
}
|
||
if !errors.Is(err, test.err) && err.Error() != test.err.Error() {
|
||
t.Fatalf("preview error = %v, want %v", err, test.err)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestBuildNacosImportPreviewTreatsExplicitHTTP404AsMissing(t *testing.T) {
|
||
client := &nacosTransferTestClient{
|
||
getConfig: func(context.Context, string, string, string) (*nacos.ConfigDetail, error) {
|
||
return nil, errors.New("Nacos HTTP error 404: config is absent")
|
||
},
|
||
}
|
||
payload := nacos.NewTransferFile("source", "Source")
|
||
payload.Configs = []nacos.TransferConfigEntry{{
|
||
DataID: "application.yaml",
|
||
Group: "DEFAULT_GROUP",
|
||
}}
|
||
|
||
preview, err := buildNacosImportPreview(context.Background(), client, "import.json", "target", payload)
|
||
if err != nil {
|
||
t.Fatalf("preview rejected an explicit HTTP 404: %v", err)
|
||
}
|
||
if preview.ExistsCount != 0 || preview.NewCount != 1 || len(preview.Items) != 1 ||
|
||
preview.Items[0].Exists || preview.Items[0].Index != 0 {
|
||
t.Fatalf("preview = %#v, want one missing config", preview)
|
||
}
|
||
}
|
||
|
||
func TestBuildNacosImportPreviewTreatsLocalizedConfigNotFoundAsMissing(t *testing.T) {
|
||
messages := []string{
|
||
"Config not found: DEFAULT_GROUP / application.yaml",
|
||
"配置不存在:DEFAULT_GROUP / application.yaml",
|
||
"設定不存在:DEFAULT_GROUP / application.yaml",
|
||
"設定が見つかりません: DEFAULT_GROUP / application.yaml",
|
||
"Konfiguration nicht gefunden: DEFAULT_GROUP / application.yaml",
|
||
"Конфигурация не найдена: DEFAULT_GROUP / application.yaml",
|
||
}
|
||
for _, message := range messages {
|
||
t.Run(message, func(t *testing.T) {
|
||
client := &nacosTransferTestClient{
|
||
getConfig: func(context.Context, string, string, string) (*nacos.ConfigDetail, error) {
|
||
return nil, errors.New(message)
|
||
},
|
||
}
|
||
payload := nacos.NewTransferFile("source", "Source")
|
||
payload.Configs = []nacos.TransferConfigEntry{{
|
||
DataID: "application.yaml",
|
||
Group: "DEFAULT_GROUP",
|
||
}}
|
||
|
||
preview, err := buildNacosImportPreview(context.Background(), client, "import.json", "target", payload)
|
||
if err != nil {
|
||
t.Fatalf("preview rejected localized config-not-found error: %v", err)
|
||
}
|
||
if preview.NewCount != 1 || preview.ExistsCount != 0 {
|
||
t.Fatalf("preview = %#v, want one missing config", preview)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestEnsureNacosDataImportAllowedHonorsExplicitProtection(t *testing.T) {
|
||
app := &App{}
|
||
base := connection.ConnectionConfig{Type: "nacos"}
|
||
|
||
if err := app.ensureNacosDataImportAllowed(base); err != nil {
|
||
t.Fatalf("unrestricted import was rejected: %v", err)
|
||
}
|
||
|
||
importRestricted := base
|
||
importRestricted.Protection.RestrictDataImport = true
|
||
if err := app.ensureNacosDataImportAllowed(importRestricted); err == nil {
|
||
t.Fatal("restrictDataImport should reject Nacos config import")
|
||
}
|
||
|
||
dataEditRestricted := base
|
||
dataEditRestricted.Protection.RestrictDataEdit = true
|
||
if err := app.ensureNacosDataImportAllowed(dataEditRestricted); err != nil {
|
||
t.Fatalf("restrictDataEdit should not reject independently protected Nacos config import: %v", err)
|
||
}
|
||
|
||
readOnly := base
|
||
readOnly.ReadOnly = true
|
||
if err := app.ensureNacosDataImportAllowed(readOnly); err == nil {
|
||
t.Fatal("readOnly should reject Nacos config import")
|
||
}
|
||
}
|
||
|
||
func TestEnsureNacosStructureEditAllowedHonorsOwnProtection(t *testing.T) {
|
||
app := &App{}
|
||
base := connection.ConnectionConfig{Type: "nacos"}
|
||
|
||
if err := app.ensureNacosStructureEditAllowed(base); err != nil {
|
||
t.Fatalf("unrestricted structure edit was rejected: %v", err)
|
||
}
|
||
|
||
structureRestricted := base
|
||
structureRestricted.Protection.RestrictStructureEdit = true
|
||
if err := app.ensureNacosStructureEditAllowed(structureRestricted); err == nil {
|
||
t.Fatal("restrictStructureEdit should reject Nacos structure edits")
|
||
}
|
||
|
||
dataEditRestricted := base
|
||
dataEditRestricted.Protection.RestrictDataEdit = true
|
||
if err := app.ensureNacosStructureEditAllowed(dataEditRestricted); err != nil {
|
||
t.Fatalf("restrictDataEdit should not reject independently protected Nacos structure edits: %v", err)
|
||
}
|
||
|
||
importRestricted := base
|
||
importRestricted.Protection.RestrictDataImport = true
|
||
if err := app.ensureNacosStructureEditAllowed(importRestricted); err != nil {
|
||
t.Fatalf("restrictDataImport should not reject independently protected Nacos structure edits: %v", err)
|
||
}
|
||
|
||
readOnly := base
|
||
readOnly.ReadOnly = true
|
||
if err := app.ensureNacosStructureEditAllowed(readOnly); err == nil {
|
||
t.Fatal("readOnly should reject Nacos structure edits")
|
||
}
|
||
}
|