214 lines
9.5 KiB
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)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|