Scert/backend/internal/database/domain_dns_account_migration_test.go

214 lines
9.5 KiB
Go

package database
import (
"database/sql"
"fmt"
"testing"
"git.pcux.cn/xuans/Scert/backend/internal/config"
"git.pcux.cn/xuans/Scert/backend/internal/testsupport/mysqltest"
"git.pcux.cn/xuans/Scert/backend/internal/testsupport/postgrestest"
)
func TestDomainDNSAccountMigration(t *testing.T) {
for _, driver := range []string{"sqlite", "mysql", "postgres"} {
t.Run(driver, func(t *testing.T) {
var cfg config.DatabaseConfig
switch driver {
case "sqlite":
cfg = sqliteBusinessUpgradeConfig(t)
case "mysql":
cfg = mysqltest.New(t).Database(t)
case "postgres":
cfg = postgrestest.New(t).Database(t)
}
db, err := Open(t.Context(), cfg)
if err != nil {
t.Fatal(err)
}
defer db.Close()
if _, err := db.DB.ExecContext(t.Context(), migrationTableSQL(driver)); err != nil {
t.Fatal(err)
}
for version := 1; version <= 27; version++ {
if err := applyMigration(t.Context(), db.DB, driver, version); err != nil {
t.Fatal(err)
}
}
seedDomainDNSMigration(t, db.DB)
if err := Migrate(t.Context(), db.DB, driver); err != nil {
t.Fatal(err)
}
checkDomainDNSConstraints(t, db.DB, driver)
if err := Migrate(t.Context(), db.DB, driver); err != nil {
t.Fatal(err)
}
var count int
if err := db.DB.QueryRowContext(t.Context(), `SELECT COUNT(*) FROM domain_dns_accounts WHERE id='binding' AND dns_account_id='account-a'`).Scan(&count); err != nil || count != 1 {
t.Fatal("重入迁移改变已有绑定", err)
}
})
}
}
func TestSQLiteDomainDNSAccountGuards(t *testing.T) {
for _, foreignKeys := range []int{0, 1} {
for _, recursive := range []int{0, 1} {
t.Run(fmt.Sprintf("foreign_keys_%d_recursive_%d", foreignKeys, recursive), func(t *testing.T) {
cfg := sqliteBusinessUpgradeConfig(t)
cfg.MaxOpenConns, cfg.MaxIdleConns = 1, 1
db, err := Open(t.Context(), cfg)
if err != nil {
t.Fatal(err)
}
defer db.Close()
for _, statement := range []string{fmt.Sprintf("PRAGMA foreign_keys=%d", foreignKeys), fmt.Sprintf("PRAGMA recursive_triggers=%d", recursive)} {
if _, err := db.DB.ExecContext(t.Context(), statement); err != nil {
t.Fatal(err)
}
}
if err := Migrate(t.Context(), db.DB, "sqlite"); err != nil {
t.Fatal(err)
}
seedDomainDNSMigration(t, db.DB)
checkDomainDNSConstraints(t, db.DB, "sqlite")
for name, want := range map[string]int{"foreign_keys": foreignKeys, "recursive_triggers": recursive} {
var value int
if err := db.DB.QueryRowContext(t.Context(), "PRAGMA "+name).Scan(&value); err != nil || value != want {
t.Fatal("迁移不应修改连接设置", name, err)
}
}
})
}
}
}
func seedDomainDNSMigration(t *testing.T, db *sql.DB) {
t.Helper()
for _, statement := range []string{
`INSERT INTO projects (id,name,description) VALUES ('binding-other','另一个项目','')`,
`INSERT INTO domains (id,name,certificate_type,sans_json,status,project_id) VALUES ('domain-a','bound.example.test','single','[]','valid','default')`,
`INSERT INTO domains (id,name,certificate_type,sans_json,status,project_id) VALUES ('domain-b','other.example.test','single','[]','valid','binding-other')`,
`INSERT INTO dns_accounts VALUES ('account-a','账户','default','old-owner','cloudflare','secret://dns/account-a','active','verified',8,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)`,
`INSERT INTO dns_accounts VALUES ('account-b','其他账户','binding-other','old-owner','cloudflare','secret://dns/account-b','active','verified',2,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)`,
} {
if _, err := db.ExecContext(t.Context(), statement); err != nil {
t.Fatal(err)
}
}
}
func checkDomainDNSConstraints(t *testing.T, db *sql.DB, driver string) {
t.Helper()
if _, err := db.ExecContext(t.Context(), `INSERT INTO domain_dns_accounts VALUES ('domain-a','binding','default','account-a',CURRENT_TIMESTAMP)`); err != nil {
t.Fatal(err)
}
for _, statement := range []string{
`INSERT INTO domain_dns_accounts VALUES ('domain-b','cross-domain','default','account-a',CURRENT_TIMESTAMP)`,
`INSERT INTO domain_dns_accounts VALUES ('domain-b','cross-account','binding-other','account-a',CURRENT_TIMESTAMP)`,
`INSERT INTO domain_dns_accounts VALUES ('missing','orphan','default','account-a',CURRENT_TIMESTAMP)`,
`UPDATE domain_dns_accounts SET dns_account_id='account-b' WHERE id='binding'`,
`UPDATE domain_dns_accounts SET domain_id='domain-b' WHERE id='binding'`,
`UPDATE domain_dns_accounts SET project_id='binding-other' WHERE id='binding'`,
`DELETE FROM dns_accounts WHERE id='account-a'`,
`DELETE FROM domains WHERE id='domain-a'`,
`UPDATE dns_accounts SET id='moved' WHERE id='account-a'`,
`UPDATE dns_accounts SET project_id='binding-other' WHERE id='account-a'`,
`UPDATE domains SET id='moved' WHERE id='domain-a'`,
`UPDATE domains SET project_id='binding-other' WHERE id='domain-a'`,
} {
if _, err := db.ExecContext(t.Context(), statement); err == nil {
t.Fatal("数据库未阻止破坏引用:", statement)
}
}
if driver == "sqlite" {
for _, statement := range []string{
`INSERT OR REPLACE INTO domains (id,name,certificate_type,sans_json,status) VALUES ('domain-a','new.example.test','single','[]','pending')`,
`INSERT OR REPLACE INTO domains (id,name,certificate_type,sans_json,status) VALUES ('new-domain','bound.example.test','single','[]','pending')`,
`INSERT OR REPLACE INTO dns_accounts VALUES ('account-a','替换','default','owner','cloudflare','secret://dns/new','active','verified',1,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)`,
`INSERT OR REPLACE INTO dns_accounts VALUES ('new-account','替换','default','owner','cloudflare','secret://dns/account-a','active','verified',1,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)`,
} {
if _, err := db.ExecContext(t.Context(), statement); err == nil {
t.Fatal("替换操作破坏绑定父记录", statement)
}
}
}
var revision int
var status string
if err := db.QueryRowContext(t.Context(), `SELECT revision FROM dns_accounts WHERE id='account-a'`).Scan(&revision); err != nil || revision != 8 {
t.Fatal("约束拒绝改变账户数据", err)
}
if err := db.QueryRowContext(t.Context(), `SELECT status FROM domains WHERE id='domain-a'`).Scan(&status); err != nil || status != "valid" {
t.Fatal("升级或拒绝改变域名状态", err)
}
}
func TestMySQLDomainDNSAccountInterruptedMigration(t *testing.T) {
f := startRealMySQL(t)
for _, boundary := range []string{"before_create", "after_create", "after_index", "after_domain_fk", "after_account_fk", "with_binding", "changed_schema_default", "bad_column", "bad_column_collation", "bad_index", "bad_foreign_key", "extra_foreign_key"} {
t.Run(boundary, func(t *testing.T) {
db := f.database(t)
mysqlPrepareVersion(t, db.DB, 27)
statements := mysqlDeploymentStatements(t, 28)
if boundary == "changed_schema_default" {
var name string
if err := db.DB.QueryRowContext(t.Context(), `SELECT DATABASE()`).Scan(&name); err != nil || !mysqlCollationIdentifier.MatchString(name) {
t.Fatal("测试数据库标识无效", err)
}
mysqlDeploymentExec(t, db.DB, "ALTER DATABASE `"+name+"` CHARACTER SET utf8mb4 COLLATE utf8mb4_bin")
}
create, _, err := mysqlDomainDNSCreateStatement(t.Context(), db.DB, statements[0])
if err != nil {
t.Fatal(err)
}
if boundary != "before_create" {
mysqlDeploymentExec(t, db.DB, create)
}
if boundary == "after_index" || boundary == "after_domain_fk" || boundary == "after_account_fk" || boundary == "with_binding" {
mysqlDeploymentExec(t, db.DB, statements[1])
}
if boundary == "after_domain_fk" || boundary == "after_account_fk" || boundary == "with_binding" {
mysqlDeploymentExec(t, db.DB, statements[2])
}
if boundary == "after_account_fk" || boundary == "with_binding" {
mysqlDeploymentExec(t, db.DB, statements[3])
}
conflict := true
switch boundary {
case "bad_column_collation":
mysqlDeploymentExec(t, db.DB, `ALTER TABLE domain_dns_accounts MODIFY COLUMN domain_id VARCHAR(36) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL`)
case "bad_column":
mysqlDeploymentExec(t, db.DB, `ALTER TABLE domain_dns_accounts MODIFY COLUMN id VARCHAR(36) NULL`)
case "bad_index":
mysqlDeploymentExec(t, db.DB, `CREATE INDEX uq_dns_accounts_project_id ON dns_accounts (project_id, id)`)
case "bad_foreign_key":
mysqlDeploymentExec(t, db.DB, `ALTER TABLE domain_dns_accounts ADD CONSTRAINT fk_domain_dns_domain FOREIGN KEY (project_id, domain_id) REFERENCES domains(project_id,id) ON DELETE CASCADE`)
case "extra_foreign_key":
mysqlDeploymentExec(t, db.DB, `ALTER TABLE domain_dns_accounts ADD CONSTRAINT unexpected_domain_dns FOREIGN KEY (project_id) REFERENCES projects(id)`)
default:
conflict = false
}
if boundary == "with_binding" {
seedDomainDNSMigration(t, db.DB)
mysqlDeploymentExec(t, db.DB, `INSERT INTO domain_dns_accounts VALUES ('domain-a','binding','default','account-a',CURRENT_TIMESTAMP)`)
}
for range 2 {
err := Migrate(t.Context(), db.DB, "mysql")
if (err != nil) != conflict {
t.Fatal("中断恢复或冲突判断错误", err)
}
version, err := currentMigrationVersion(t.Context(), db.DB)
if err != nil || (conflict && version != 27) || (!conflict && version != latestMigrationVersion) {
t.Fatal("迁移版本提前登记", version, err)
}
}
if boundary == "with_binding" {
var account string
if err := db.DB.QueryRowContext(t.Context(), `SELECT dns_account_id FROM domain_dns_accounts WHERE id='binding'`).Scan(&account); err != nil || account != "account-a" {
t.Fatal("中断恢复改变原有绑定", err)
}
}
})
}
}