Scert/backend/internal/database/resource_project_mysql_test.go
xuans 7284958819 完善域名服务器项目归属与数据库关联约束
保存资源项目、所有者和操作人,接口从会话记录归属,普通更新保留创建信息。
新增三库第十一版迁移、同项目绑定约束和 SQLite 触发器保护,补充迁移回滚重试、仓储、接口及备份恢复测试。
本次整理验证:后端构建、静态检查、关联改造针对性测试和 CLI 包测试通过。
范围说明:项目成员权限及其他资源归属尚未完成;本次未重新执行外部数据库实库、独立程序及生产部署验收。
2026-09-28 10:45:00 +08:00

93 lines
4.3 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 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)
}
}