All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m20s
237 lines
10 KiB
Go
237 lines
10 KiB
Go
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)
|
||
}
|
||
}
|