实现七月迭代公共技术基础
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m20s

This commit is contained in:
2026-07-23 17:52:48 +09:00
parent f7c42252c0
commit 17782d5f8e
76 changed files with 5246 additions and 158 deletions

View File

@@ -0,0 +1,236 @@
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)
}
}