Files
junhong_cmp_fiber/internal/routes/system_config_integration_test.go
break 17782d5f8e
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m20s
实现七月迭代公共技术基础
2026-07-23 17:52:48 +09:00

237 lines
10 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 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)
}
}