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