package routes import ( "context" stderrors "errors" "net/http/httptest" "strings" "sync" "testing" "time" "github.com/bytedance/sonic" "github.com/gofiber/fiber/v2" "go.uber.org/zap" "gorm.io/gorm" systemConfigApp "github.com/break/junhong_cmp_fiber/internal/application/systemconfig" "github.com/break/junhong_cmp_fiber/internal/handler/admin" systemConfigInfra "github.com/break/junhong_cmp_fiber/internal/infrastructure/systemconfig" internalMiddleware "github.com/break/junhong_cmp_fiber/internal/middleware" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/internal/model/dto" systemConfigQuery "github.com/break/junhong_cmp_fiber/internal/query/systemconfig" "github.com/break/junhong_cmp_fiber/internal/testutil" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/middleware" ) type configAuditWriter struct { fail bool } func (w *configAuditWriter) WriteConfigChange(_ context.Context, tx *gorm.DB, audit systemConfigApp.ChangeAudit) error { if w.fail { return stderrors.New("测试审计写入失败") } before, _ := sonic.Marshal(audit.BeforeData) after, _ := sonic.Marshal(audit.AfterData) return tx.Exec(`INSERT INTO test_system_config_audit (config_key, operator_id, request_id, before_data, after_data) VALUES (?, ?, ?, ?::jsonb, ?::jsonb)`, audit.ConfigKey, audit.OperatorID, audit.RequestID, string(before), string(after)).Error } type recordingAlerts struct { mu sync.Mutex codes []string } func (a *recordingAlerts) Warn(_ context.Context, code, _, _, _ string) { a.mu.Lock() defer a.mu.Unlock() a.codes = append(a.codes, code) } type failingCache struct{} func (failingCache) Get(context.Context, string) (string, error) { return "", stderrors.New("缓存不可用") } func (failingCache) Set(context.Context, string, string, time.Duration) error { return stderrors.New("缓存不可用") } func (failingCache) Delete(context.Context, string) error { return stderrors.New("缓存不可用") } func TestSystemConfigFiberListAndUpdateUseRealPostgresAndRedis(t *testing.T) { db := testutil.NewPostgresTransaction(t) redisClient := testutil.NewRedisClient(t) testutil.CreateTemporarySystemConfigTable(t, db) createTemporarySystemConfigAuditTable(t, db) registry := newSystemConfigTestRegistry(t) cache := systemConfigInfra.NewRedisCache(redisClient) alerts := &recordingAlerts{} reader := systemConfigInfra.NewReader(db, registry, cache, alerts) handler := admin.NewSystemConfigHandler( systemConfigQuery.NewListQuery(reader), systemConfigApp.NewUpdateService(db, registry, cache, &configAuditWriter{}, alerts, nil), ) if err := db.Create(&model.SystemConfig{ ConfigKey: "legacy.unknown.value", ConfigValue: "legacy", ValueType: constants.SystemConfigTypeString, Module: "legacy", Description: "未注册遗留配置", IsSensitive: false, }).Error; err != nil { t.Fatalf("准备未注册配置失败:%v", err) } if err := redisClient.Set(context.Background(), constants.RedisSystemConfigKey("foundation.demo.int"), "7", constants.SystemConfigCacheTTL).Err(); err != nil { t.Fatalf("准备 Redis 配置缓存失败:%v", err) } t.Cleanup(func() { for _, definition := range registry.List() { _ = redisClient.Del(context.Background(), constants.RedisSystemConfigKey(definition.Key)).Err() } }) app := newSystemConfigTestApp(handler, constants.UserTypeSuperAdmin) listResponse, err := app.Test(httptest.NewRequest("GET", "/api/admin/system-configs?module=foundation&page=1&page_size=20", nil)) if err != nil { t.Fatalf("查询系统配置接口失败:%v", err) } if listResponse.StatusCode != fiber.StatusOK { t.Fatalf("查询系统配置状态码错误:%d", listResponse.StatusCode) } var listBody struct { Code int `json:"code"` Data dto.SystemConfigListResponse `json:"data"` } if err := sonic.ConfigDefault.NewDecoder(listResponse.Body).Decode(&listBody); err != nil { t.Fatalf("解析系统配置列表响应失败:%v", err) } _ = listResponse.Body.Close() if listBody.Code != 0 || listBody.Data.Total != 4 || len(listBody.Data.List) != 4 { t.Fatalf("四种注册类型未完整返回:%+v", listBody) } for _, item := range listBody.Data.List { if item.Sensitive && item.Value != "[已配置]" { t.Fatalf("敏感配置未脱敏:%+v", item) } } updateRequest := httptest.NewRequest("PUT", "/api/admin/system-configs/foundation.demo.int", strings.NewReader(`{"value":"8"}`)) updateRequest.Header.Set("Content-Type", "application/json") updateResponse, err := app.Test(updateRequest) if err != nil { t.Fatalf("更新系统配置接口失败:%v", err) } if updateResponse.StatusCode != fiber.StatusOK { t.Fatalf("更新系统配置状态码错误:%d", updateResponse.StatusCode) } _ = updateResponse.Body.Close() var stored model.SystemConfig if err := db.Where("config_key = ?", "foundation.demo.int").First(&stored).Error; err != nil || stored.ConfigValue != "8" { t.Fatalf("系统配置数据库事实错误:%v,记录:%+v", err, stored) } var auditCount int64 if err := db.Table("test_system_config_audit").Where("config_key = ? AND operator_id = ?", stored.ConfigKey, 7).Count(&auditCount).Error; err != nil || auditCount != 1 { t.Fatalf("系统配置审计事实错误:%v,数量:%d", err, auditCount) } if exists, err := redisClient.Exists(context.Background(), constants.RedisSystemConfigKey(stored.ConfigKey)).Result(); err != nil || exists != 0 { t.Fatalf("更新后缓存未失效:%v,存在:%d", err, exists) } } func TestSystemConfigPermissionsValidationRollbackAndCacheFailure(t *testing.T) { db := testutil.NewPostgresTransaction(t) testutil.CreateTemporarySystemConfigTable(t, db) createTemporarySystemConfigAuditTable(t, db) registry := newSystemConfigTestRegistry(t) alerts := &recordingAlerts{} reader := systemConfigInfra.NewReader(db, registry, failingCache{}, alerts) service := systemConfigApp.NewUpdateService(db, registry, failingCache{}, &configAuditWriter{}, alerts, nil) handler := admin.NewSystemConfigHandler(systemConfigQuery.NewListQuery(reader), service) forbiddenApp := newSystemConfigTestApp(handler, constants.UserTypePlatform) response, err := forbiddenApp.Test(httptest.NewRequest("GET", "/api/admin/system-configs", nil)) if err != nil { t.Fatalf("执行无权限查询失败:%v", err) } if response.StatusCode != fiber.StatusForbidden { t.Fatalf("权限不足必须返回 403,得到:%d", response.StatusCode) } _ = response.Body.Close() ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{UserID: 7, UserType: constants.UserTypeSuperAdmin}) invalidCases := []struct { key string value string }{ {key: "foundation.demo.int", value: "not-int"}, {key: "foundation.demo.bool", value: "yes"}, {key: "foundation.demo.json", value: "{"}, {key: "foundation.unknown.value", value: "x"}, {key: "foundation.demo.readonly", value: "x"}, } for _, item := range invalidCases { if _, err := service.Execute(ctx, item.key, dto.UpdateSystemConfigRequest{Value: item.value}); err == nil { t.Fatalf("非法配置更新应失败:%s=%s", item.key, item.value) } } failClosed := systemConfigApp.NewUpdateService(db, registry, failingCache{}, &configAuditWriter{fail: true}, alerts, nil) if _, err := failClosed.Execute(ctx, "foundation.demo.string", dto.UpdateSystemConfigRequest{Value: "must-rollback"}); err == nil { t.Fatal("审计写入失败时配置事务必须回滚") } var count int64 if err := db.Model(&model.SystemConfig{}).Where("config_key = ?", "foundation.demo.string").Count(&count).Error; err != nil || count != 0 { t.Fatalf("审计失败后配置事实未回滚:%v,数量:%d", err, count) } if _, err := service.Execute(ctx, "foundation.demo.string", dto.UpdateSystemConfigRequest{Value: "committed"}); err != nil { t.Fatalf("缓存失效失败不应回滚数据库事实:%v", err) } if len(alerts.codes) == 0 { t.Fatal("缓存故障必须产生可观察告警") } } func newSystemConfigTestRegistry(t *testing.T) *systemConfigInfra.Registry { t.Helper() registry := systemConfigInfra.NewRegistry() minimum, maximum := int64(1), int64(10) definitions := []systemConfigInfra.Definition{ {Key: "foundation.demo.string", Module: "foundation", ValueType: constants.SystemConfigTypeString, DefaultValue: "default", Description: "字符串示例", Control: "input"}, {Key: "foundation.demo.int", Module: "foundation", ValueType: constants.SystemConfigTypeInt, DefaultValue: "5", Description: "整数示例", Control: "number", Min: &minimum, Max: &maximum}, {Key: "foundation.demo.bool", Module: "foundation", ValueType: constants.SystemConfigTypeBool, DefaultValue: "true", Description: "布尔示例", Control: "switch"}, {Key: "foundation.demo.json", Module: "foundation", ValueType: constants.SystemConfigTypeJSON, DefaultValue: `{"enabled":true}`, Description: "JSON 示例", Control: "structured", Sensitive: true}, {Key: "foundation.demo.readonly", Module: "foundation-readonly", ValueType: constants.SystemConfigTypeString, DefaultValue: "fixed", Description: "只读示例", Control: "readonly", Readonly: true}, } for _, definition := range definitions { if err := registry.Register(definition); err != nil { t.Fatalf("注册测试配置失败:%v", err) } } return registry } func newSystemConfigTestApp(handler *admin.SystemConfigHandler, userType int) *fiber.App { app := fiber.New(fiber.Config{JSONEncoder: sonic.Marshal, JSONDecoder: sonic.Unmarshal, ErrorHandler: internalMiddleware.ErrorHandler(zap.NewNop())}) api := app.Group("/api/admin", func(c *fiber.Ctx) error { ctx := middleware.SetUserContext(c.UserContext(), &middleware.UserContextInfo{UserID: 7, UserType: userType}) ctx = context.WithValue(ctx, constants.ContextKeyRequestID, "request-system-config") c.SetUserContext(ctx) return c.Next() }) registerSystemConfigRoutes(api, handler, nil, "/api/admin") return app } func createTemporarySystemConfigAuditTable(t *testing.T, db *gorm.DB) { t.Helper() if err := db.Exec(`CREATE TEMP TABLE test_system_config_audit ( id bigserial PRIMARY KEY, config_key varchar(150) NOT NULL, operator_id bigint NOT NULL, request_id varchar(100) NOT NULL, before_data jsonb NOT NULL, after_data jsonb NOT NULL ) ON COMMIT DROP`).Error; err != nil { t.Fatalf("创建系统配置审计测试表失败:%v", err) } }