Files
junhong_cmp_fiber/internal/application/systemconfig/update_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

142 lines
5.4 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 systemconfig_test
import (
"context"
"fmt"
"os"
"strings"
"sync"
"testing"
"github.com/bytedance/sonic"
"github.com/google/uuid"
"gorm.io/driver/postgres"
"gorm.io/gorm"
systemconfigapp "github.com/break/junhong_cmp_fiber/internal/application/systemconfig"
systemconfiginfra "github.com/break/junhong_cmp_fiber/internal/infrastructure/systemconfig"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
"github.com/break/junhong_cmp_fiber/pkg/config"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/middleware"
)
type transactionAuditWriter struct{}
func (transactionAuditWriter) WriteConfigChange(_ context.Context, tx *gorm.DB, audit systemconfigapp.ChangeAudit) error {
before, _ := sonic.Marshal(audit.BeforeData)
after, _ := sonic.Marshal(audit.AfterData)
return tx.Exec(`INSERT INTO test_system_config_audit
(config_key, operator_id, before_data, after_data) VALUES (?, ?, ?::jsonb, ?::jsonb)`,
audit.ConfigKey, audit.OperatorID, string(before), string(after)).Error
}
func TestConcurrentFirstSystemConfigUpdatesAreSerializedByPostgres(t *testing.T) {
if os.Getenv("JUNHONG_DATABASE_HOST") == "" {
t.Skip("未加载 .env.local跳过依赖真实 PostgreSQL 的并发集成测试")
}
cfg, err := config.Load()
if err != nil {
t.Fatalf("加载测试配置失败:%v", err)
}
schema := "test_system_config_" + strings.ReplaceAll(uuid.NewString(), "-", "")
adminDB := openSchemaDatabase(t, &cfg.Database, "public")
if err := adminDB.Exec("CREATE SCHEMA " + schema).Error; err != nil {
t.Fatalf("创建隔离 schema 失败:%v", err)
}
t.Cleanup(func() {
_ = adminDB.Exec("DROP SCHEMA " + schema + " CASCADE").Error
closeDatabase(adminDB)
})
firstDB := openSchemaDatabase(t, &cfg.Database, schema)
secondDB := openSchemaDatabase(t, &cfg.Database, schema)
t.Cleanup(func() {
closeDatabase(firstDB)
closeDatabase(secondDB)
})
createConcurrentConfigTables(t, firstDB)
registry := systemconfiginfra.NewRegistry()
definition := systemconfiginfra.Definition{
Key: "foundation.concurrent.value", Module: "foundation", ValueType: constants.SystemConfigTypeString,
DefaultValue: "default", Description: "并发配置测试值",
}
if err := registry.Register(definition); err != nil {
t.Fatalf("注册并发测试配置失败:%v", err)
}
ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{UserID: 7, UserType: constants.UserTypeSuperAdmin})
services := []*systemconfigapp.UpdateService{
systemconfigapp.NewUpdateService(firstDB, registry, nil, transactionAuditWriter{}, nil, nil),
systemconfigapp.NewUpdateService(secondDB, registry, nil, transactionAuditWriter{}, nil, nil),
}
values := []string{"first", "second"}
start := make(chan struct{})
errorsFound := make(chan error, len(services))
var waitGroup sync.WaitGroup
for index := range services {
waitGroup.Add(1)
go func(index int) {
defer waitGroup.Done()
<-start
_, executeErr := services[index].Execute(ctx, definition.Key, dto.UpdateSystemConfigRequest{Value: values[index]})
errorsFound <- executeErr
}(index)
}
close(start)
waitGroup.Wait()
close(errorsFound)
for executeErr := range errorsFound {
if executeErr != nil {
t.Fatalf("并发更新不应产生唯一键冲突:%v", executeErr)
}
}
var stored model.SystemConfig
if err := firstDB.Where("config_key = ?", definition.Key).First(&stored).Error; err != nil {
t.Fatalf("读取并发更新结果失败:%v", err)
}
if stored.ConfigValue != values[0] && stored.ConfigValue != values[1] {
t.Fatalf("并发更新保存了非法结果:%q", stored.ConfigValue)
}
var auditCount int64
if err := firstDB.Table("test_system_config_audit").Where("config_key = ?", definition.Key).Count(&auditCount).Error; err != nil || auditCount != 2 {
t.Fatalf("并发更新审计事实不完整:错误=%v数量=%d", err, auditCount)
}
}
func openSchemaDatabase(t *testing.T, cfg *config.DatabaseConfig, schema string) *gorm.DB {
t.Helper()
dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s search_path=%s TimeZone=Asia/Shanghai",
cfg.Host, cfg.Port, cfg.User, cfg.Password, cfg.DBName, cfg.SSLMode, schema)
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{SkipDefaultTransaction: true})
if err != nil {
t.Fatalf("连接隔离 PostgreSQL 失败:%v", err)
}
return db
}
func closeDatabase(db *gorm.DB) {
if sqlDB, err := db.DB(); err == nil {
_ = sqlDB.Close()
}
}
func createConcurrentConfigTables(t *testing.T, db *gorm.DB) {
t.Helper()
if err := db.Exec(`CREATE TABLE tb_system_config (
id bigserial PRIMARY KEY, config_key varchar(150) NOT NULL UNIQUE, config_value text NOT NULL,
value_type varchar(20) NOT NULL, module varchar(100) NOT NULL, description varchar(500) NOT NULL,
is_readonly boolean NOT NULL DEFAULT false, is_sensitive boolean NOT NULL DEFAULT false,
creator bigint NOT NULL DEFAULT 0, updater bigint NOT NULL DEFAULT 0,
created_at timestamptz NOT NULL DEFAULT NOW(), updated_at timestamptz NOT NULL DEFAULT NOW()
)`).Error; err != nil {
t.Fatalf("创建隔离配置表失败:%v", err)
}
if err := db.Exec(`CREATE TABLE test_system_config_audit (
id bigserial PRIMARY KEY, config_key varchar(150) NOT NULL, operator_id bigint NOT NULL,
before_data jsonb NOT NULL, after_data jsonb NOT NULL
)`).Error; err != nil {
t.Fatalf("创建隔离审计表失败:%v", err)
}
}