实现七月迭代公共技术基础
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,222 @@
package releasegate_test
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"github.com/google/uuid"
"gorm.io/gorm"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/releasegate"
"github.com/break/junhong_cmp_fiber/internal/testutil"
)
func TestPublicFoundationMigrationsOnEmptyAndCompatibleDatabase(t *testing.T) {
db := testutil.NewPostgresTransaction(t)
schema := prepareMigrationSchema(t, db, 164)
checker := releasegate.NewCheckerWithSchema(db, schema)
pre, err := checker.Run(context.Background(), releasegate.PhasePre)
if err != nil {
t.Fatalf("执行迁移前检查失败:%v", err)
}
if !pre.Passed() {
t.Fatalf("空数据库迁移前检查不应阻断:%+v", pre.Findings)
}
if err := db.Exec("CREATE TABLE compatible_existing_data (id bigint PRIMARY KEY, value text NOT NULL)").Error; err != nil {
t.Fatalf("准备兼容存量表失败:%v", err)
}
if err := db.Exec("INSERT INTO compatible_existing_data (id, value) VALUES (1, 'keep')").Error; err != nil {
t.Fatalf("准备兼容存量数据失败:%v", err)
}
executeMigration(t, db, "000165_create_public_outbox.up.sql")
executeMigration(t, db, "000166_create_system_config.up.sql")
if err := db.Exec("UPDATE schema_migrations SET version = 166, dirty = false").Error; err != nil {
t.Fatalf("更新隔离迁移版本失败:%v", err)
}
post, err := checker.Run(context.Background(), releasegate.PhasePost)
if err != nil {
t.Fatalf("执行迁移后检查失败:%v", err)
}
if !post.Passed() {
t.Fatalf("迁移后检查应通过:%+v", post.Findings)
}
postAgain, err := checker.Run(context.Background(), releasegate.PhasePost)
if err != nil || !postAgain.Passed() {
t.Fatalf("迁移后检查必须可重复执行:%v结果%+v", err, postAgain.Findings)
}
var compatibleCount int64
if err := db.Table("compatible_existing_data").Count(&compatibleCount).Error; err != nil || compatibleCount != 1 {
t.Fatalf("迁移破坏了兼容存量数据:%v数量%d", err, compatibleCount)
}
if err := db.Transaction(func(tx *gorm.DB) error {
return executeMigrationWithError(tx, "000165_create_public_outbox.up.sql")
}); err == nil {
t.Fatal("公共对象已创建时不得通过重复 DDL 静默掩盖定义")
}
}
func TestMigrationGateBlocksAnomaliesWithoutDestructiveWrites(t *testing.T) {
db := testutil.NewPostgresTransaction(t)
schema := prepareMigrationSchema(t, db, 166)
executeMigration(t, db, "000165_create_public_outbox.up.sql")
executeMigration(t, db, "000166_create_system_config.up.sql")
if err := db.Exec(`INSERT INTO tb_outbox_event
(event_id, event_type, payload_version, aggregate_type, aggregate_id, resource_type, resource_id, payload)
VALUES ('blocked-event', 'foundation.blocked', 1, 'example', '1', 'example', '1', '{}'::jsonb)`).Error; err != nil {
t.Fatalf("准备未投递异常失败:%v", err)
}
if err := db.Exec("ALTER TABLE tb_outbox_event DROP CONSTRAINT ck_outbox_event_status").Error; err != nil {
t.Fatalf("准备非法状态定义失败:%v", err)
}
if err := db.Exec(`INSERT INTO tb_outbox_event
(event_id, event_type, payload_version, aggregate_type, aggregate_id, resource_type, resource_id, payload, status)
VALUES ('invalid-status', 'foundation.invalid', 1, 'example', '2', 'example', '2', '{}'::jsonb, 9)`).Error; err != nil {
t.Fatalf("准备非法状态数据失败:%v", err)
}
report, err := releasegate.NewCheckerWithSchema(db, schema).Run(context.Background(), releasegate.PhasePost)
if err != nil {
t.Fatalf("执行异常门禁失败:%v", err)
}
if report.Passed() || !hasFinding(report, "OUTBOX_UNDELIVERED") || !hasFinding(report, "OUTBOX_INVALID_STATUS") || !hasFinding(report, "FOUNDATION_CONSTRAINT_MISSING") {
t.Fatalf("异常数据和定义未被完整阻断:%+v", report.Findings)
}
var count int64
if err := db.Table("tb_outbox_event").Count(&count).Error; err != nil || count != 2 {
t.Fatalf("只读门禁修改了异常数据:%v数量%d", err, count)
}
}
func TestDownMigrationAllowsEmptyStructuresAndRejectsExistingFacts(t *testing.T) {
t.Run("空结构可回滚", func(t *testing.T) {
db := testutil.NewPostgresTransaction(t)
schema := prepareMigrationSchema(t, db, 166)
executeMigration(t, db, "000165_create_public_outbox.up.sql")
executeMigration(t, db, "000166_create_system_config.up.sql")
executeMigration(t, db, "000166_create_system_config.down.sql")
executeMigration(t, db, "000165_create_public_outbox.down.sql")
for _, table := range []string{"tb_outbox_event", "tb_system_config"} {
var exists bool
if err := db.Raw("SELECT to_regclass(?) IS NOT NULL", schema+"."+table).Scan(&exists).Error; err != nil || exists {
t.Fatalf("空结构回滚失败:%s错误%v", table, err)
}
}
})
t.Run("已有事实拒绝删表", func(t *testing.T) {
db := testutil.NewPostgresTransaction(t)
_ = prepareMigrationSchema(t, db, 166)
executeMigration(t, db, "000165_create_public_outbox.up.sql")
executeMigration(t, db, "000166_create_system_config.up.sql")
if err := db.Exec(`INSERT INTO tb_system_config
(config_key, config_value, value_type, module, description)
VALUES ('foundation.test.fact', 'true', 'bool', 'foundation', '回滚保护测试')`).Error; err != nil {
t.Fatalf("准备配置事实失败:%v", err)
}
if err := db.Transaction(func(tx *gorm.DB) error {
return executeMigrationWithError(tx, "000166_create_system_config.down.sql")
}); err == nil || !strings.Contains(err.Error(), "禁止删表回滚") {
t.Fatalf("已有事实时 down 迁移必须明确拒绝:%v", err)
}
var count int64
if err := db.Table("tb_system_config").Count(&count).Error; err != nil || count != 1 {
t.Fatalf("拒绝回滚后配置事实丢失:%v数量%d", err, count)
}
})
}
func prepareMigrationSchema(t *testing.T, db *gorm.DB, version uint) string {
t.Helper()
schema := "foundation_test_" + strings.ReplaceAll(uuid.NewString(), "-", "")
if err := db.Exec(fmt.Sprintf(`CREATE SCHEMA %s`, schema)).Error; err != nil {
t.Fatalf("创建隔离迁移 schema 失败:%v", err)
}
if err := db.Exec(fmt.Sprintf(`SET LOCAL search_path TO %s`, schema)).Error; err != nil {
t.Fatalf("切换隔离迁移 schema 失败:%v", err)
}
if err := db.Exec("CREATE TABLE schema_migrations (version bigint NOT NULL, dirty boolean NOT NULL)").Error; err != nil {
t.Fatalf("创建隔离迁移版本表失败:%v", err)
}
if err := db.Exec("INSERT INTO schema_migrations (version, dirty) VALUES (?, false)", version).Error; err != nil {
t.Fatalf("写入隔离迁移版本失败:%v", err)
}
return schema
}
func executeMigration(t *testing.T, db *gorm.DB, name string) {
t.Helper()
if err := executeMigrationWithError(db, name); err != nil {
t.Fatalf("执行迁移 %s 失败:%v", name, err)
}
}
func executeMigrationWithError(db *gorm.DB, name string) error {
path := filepath.Join("..", "..", "..", "migrations", name)
content, err := os.ReadFile(path)
if err != nil {
return err
}
for _, statement := range splitSQLStatements(string(content)) {
if err := db.Exec(statement).Error; err != nil {
return err
}
}
return nil
}
func splitSQLStatements(content string) []string {
statements := make([]string, 0)
start := 0
inSingleQuote := false
inDoubleQuote := false
inDollarBlock := false
for index := 0; index < len(content); index++ {
if index+1 < len(content) && content[index:index+2] == "$$" && !inSingleQuote && !inDoubleQuote {
inDollarBlock = !inDollarBlock
index++
continue
}
if inDollarBlock {
continue
}
switch content[index] {
case '\'':
if !inDoubleQuote {
inSingleQuote = !inSingleQuote
}
case '"':
if !inSingleQuote {
inDoubleQuote = !inDoubleQuote
}
case ';':
if !inSingleQuote && !inDoubleQuote {
statement := strings.TrimSpace(content[start : index+1])
if statement != "" {
statements = append(statements, statement)
}
start = index + 1
}
}
}
if tail := strings.TrimSpace(content[start:]); tail != "" {
statements = append(statements, tail)
}
return statements
}
func hasFinding(report releasegate.Report, code string) bool {
for _, finding := range report.Findings {
if finding.Code == code {
return true
}
}
return false
}