保存资源项目、所有者和操作人,接口从会话记录归属,普通更新保留创建信息。 新增三库第十一版迁移、同项目绑定约束和 SQLite 触发器保护,补充迁移回滚重试、仓储、接口及备份恢复测试。 本次整理验证:后端构建、静态检查、关联改造针对性测试和 CLI 包测试通过。 范围说明:项目成员权限及其他资源归属尚未完成;本次未重新执行外部数据库实库、独立程序及生产部署验收。
93 lines
4.3 KiB
Go
93 lines
4.3 KiB
Go
package database
|
||
|
||
import (
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
func TestMySQLRealResourceProjectMigrationRetry(t *testing.T) {
|
||
fixture := startRealMySQL(t)
|
||
db := fixture.database(t)
|
||
ctx := t.Context()
|
||
if _, err := db.DB.ExecContext(ctx, migrationTableSQL("mysql")); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
for version := 1; version <= 10; version++ {
|
||
if err := applyMigration(ctx, db.DB, "mysql", version); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
for _, statement := range []string{
|
||
`ALTER TABLE domains CONVERT TO CHARACTER SET latin1 COLLATE latin1_bin`,
|
||
`ALTER TABLE servers CONVERT TO CHARACTER SET latin1 COLLATE latin1_bin`,
|
||
`ALTER TABLE domain_servers CONVERT TO CHARACTER SET latin1 COLLATE latin1_bin`,
|
||
`INSERT INTO domains (id, name, certificate_type, sans_json, status) VALUES ('domain', 'example.test', 'single', '[]', 'pending')`,
|
||
`INSERT INTO servers (id, name, address, port, username, private_key_ref, host_key_fingerprint, certificate_path, private_key_path, certificate_chain_path, web_server, reload_command_json, validation_command_json, ssh_key_error) VALUES ('server', 'server', '192.0.2.10', 22, 'deploy', 'secret://test', 'test-only', '/test/cert.pem', '/test/key.pem', '/test/chain.pem', 'nginx', '[]', '[]', '')`,
|
||
`INSERT INTO domain_servers (domain_id, server_id) VALUES ('domain', 'server')`,
|
||
`INSERT INTO domain_servers (domain_id, server_id) VALUES ('missing', 'server')`,
|
||
} {
|
||
if _, err := db.DB.ExecContext(ctx, statement); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
if err := Migrate(ctx, db.DB, "mysql"); err == nil {
|
||
t.Fatal("历史悬空关联未阻断迁移")
|
||
}
|
||
var count int
|
||
if err := db.DB.QueryRowContext(ctx, "SELECT COUNT(*) FROM domain_servers").Scan(&count); err != nil || count != 2 {
|
||
t.Fatalf("迁移失败擅自删除历史关联:%d,%v", count, err)
|
||
}
|
||
if _, err := db.DB.ExecContext(ctx, "DELETE FROM domain_servers WHERE domain_id = 'missing'"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
var schema string
|
||
if err := db.DB.QueryRowContext(ctx, "SELECT DATABASE()").Scan(&schema); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
schema = "`" + strings.ReplaceAll(schema, "`", "``") + "`"
|
||
if _, err := fixture.admin.ExecContext(ctx, `CREATE TRIGGER `+schema+`.fail_resource_version BEFORE INSERT ON `+schema+`.schema_migrations
|
||
FOR EACH ROW SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'test-only'`); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := Migrate(ctx, db.DB, "mysql"); err == nil {
|
||
t.Fatal("版本登记故障未阻断迁移")
|
||
}
|
||
version, err := currentMigrationVersion(ctx, db.DB)
|
||
if err != nil || version != 10 {
|
||
t.Fatalf("部分执行的迁移错误登记版本:%d,%v", version, err)
|
||
}
|
||
if _, err := db.DB.ExecContext(ctx, `UPDATE domains SET owner_id = 'after-partial', created_by = 'retained', updated_by = 'retained' WHERE id = 'domain'`); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := fixture.admin.ExecContext(ctx, "DROP TRIGGER "+schema+".fail_resource_version"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
for round := 0; round < 2; round++ {
|
||
if err := Migrate(ctx, db.DB, "mysql"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
var project, owner, creator, updater string
|
||
if err := db.DB.QueryRowContext(ctx, "SELECT project_id, owner_id, created_by, updated_by FROM domains WHERE id = 'domain'").Scan(&project, &owner, &creator, &updater); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if project != "default" || owner != "after-partial" || creator != "retained" || updater != "retained" {
|
||
t.Fatal("重试改写已保存的资源归属")
|
||
}
|
||
if err := db.DB.QueryRowContext(ctx, `SELECT COUNT(*) FROM information_schema.referential_constraints WHERE constraint_schema = DATABASE() AND constraint_name IN ('fk_domains_project', 'fk_servers_project', 'fk_binding_domain_project', 'fk_binding_server_project')`).Scan(&count); err != nil || count != 4 {
|
||
t.Fatalf("项目关联外键未完整建立:%d,%v", count, err)
|
||
}
|
||
for _, statement := range []string{
|
||
`DELETE FROM projects WHERE id = 'default'`,
|
||
`UPDATE domains SET project_id = 'DEFAULT' WHERE id = 'domain'`,
|
||
`UPDATE domain_servers SET server_id = 'missing'`,
|
||
} {
|
||
if _, err := db.DB.ExecContext(ctx, statement); err == nil {
|
||
t.Errorf("约束未生效:%s", statement)
|
||
}
|
||
}
|
||
if err := db.DB.QueryRowContext(ctx, "SELECT COUNT(*) FROM domain_servers WHERE domain_id = 'domain' AND server_id = 'server' AND project_id = 'default'").Scan(&count); err != nil || count != 1 {
|
||
t.Fatalf("历史有效绑定丢失:%d,%v", count, err)
|
||
}
|
||
}
|