保存资源项目、所有者和操作人,接口从会话记录归属,普通更新保留创建信息。 新增三库第十一版迁移、同项目绑定约束和 SQLite 触发器保护,补充迁移回滚重试、仓储、接口及备份恢复测试。 本次整理验证:后端构建、静态检查、关联改造针对性测试和 CLI 包测试通过。 范围说明:项目成员权限及其他资源归属尚未完成;本次未重新执行外部数据库实库、独立程序及生产部署验收。
120 lines
5.7 KiB
Go
120 lines
5.7 KiB
Go
package database
|
||
|
||
import (
|
||
"fmt"
|
||
"testing"
|
||
)
|
||
|
||
func TestSQLiteResourceProjectMigration(t *testing.T) {
|
||
for _, foreignKeys := range []int{0, 1} {
|
||
t.Run(fmt.Sprintf("foreign_keys_%d", foreignKeys), func(t *testing.T) {
|
||
db, err := Open(t.Context(), sqliteBusinessUpgradeConfig(t))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer db.Close()
|
||
db.DB.SetMaxOpenConns(1)
|
||
createSQLiteHistoricalBusiness(t, db.DB, 10)
|
||
for _, query := range []string{
|
||
fmt.Sprintf("PRAGMA foreign_keys = %d", foreignKeys),
|
||
`CREATE VIEW resource_names AS SELECT id, name FROM domains`,
|
||
`CREATE TABLE resource_events (name TEXT)`,
|
||
`CREATE TRIGGER resource_name_event AFTER UPDATE OF name ON domains BEGIN INSERT INTO resource_events VALUES (new.name); END`,
|
||
`CREATE INDEX custom_resource_name ON domains(name, id)`,
|
||
} {
|
||
if _, err := db.DB.ExecContext(t.Context(), query); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
before := snapshotSQLiteHistoricalBusiness(t, db.DB)
|
||
objectsQuery := `SELECT type, name, sql FROM sqlite_master WHERE name IN ('resource_names', 'resource_events', 'resource_name_event', 'custom_resource_name') ORDER BY name`
|
||
objects := snapshotSQLiteUpgradeQuery(t, db.DB, objectsQuery)
|
||
for round := 0; round < 2; round++ {
|
||
if err := Migrate(t.Context(), db.DB, "sqlite"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
assertSQLiteHistoricalBusiness(t, db.DB, before)
|
||
if actual := snapshotSQLiteUpgradeQuery(t, db.DB, objectsQuery); actual != objects {
|
||
t.Fatal("资源升级改写已有视图、索引或触发器")
|
||
}
|
||
for _, table := range []string{"domains", "servers"} {
|
||
var project, owner, creator, updater string
|
||
if err := db.DB.QueryRowContext(t.Context(), "SELECT project_id, owner_id, created_by, updated_by FROM "+table).Scan(&project, &owner, &creator, &updater); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if project != "default" || owner != "" || creator != "" || updater != "" {
|
||
t.Fatal("历史归属回填不正确,不能编造历史操作者")
|
||
}
|
||
}
|
||
for _, statement := range []string{
|
||
`UPDATE domains SET project_id = 'missing'`,
|
||
`DELETE FROM projects WHERE id = 'default'`,
|
||
`UPDATE domain_servers SET server_id = 'missing'`,
|
||
`INSERT OR REPLACE INTO domains (id, name, certificate_type, sans_json, status) SELECT 'replacement', name, certificate_type, sans_json, status FROM domains WHERE id = 'domain'`,
|
||
`INSERT OR REPLACE INTO domains (id, name, certificate_type, sans_json, status) SELECT id, name, certificate_type, sans_json, status FROM domains WHERE id = 'domain'`,
|
||
`INSERT OR REPLACE 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) SELECT id, name, address, port, username, private_key_ref, host_key_fingerprint, certificate_path, private_key_path, certificate_chain_path, web_server, reload_command_json FROM servers`,
|
||
} {
|
||
if _, err := db.DB.ExecContext(t.Context(), statement); err == nil {
|
||
t.Errorf("资源约束被绕过:%s", statement)
|
||
}
|
||
}
|
||
assertSQLiteHistoricalBusiness(t, db.DB, before)
|
||
var enabled int
|
||
if err := db.DB.QueryRowContext(t.Context(), "PRAGMA foreign_keys").Scan(&enabled); err != nil || enabled != foreignKeys {
|
||
t.Fatalf("迁移改变原连接外键配置:%d,%v", enabled, err)
|
||
}
|
||
if _, err := db.DB.ExecContext(t.Context(), `UPDATE domains SET name = 'after.example.test' WHERE id = 'domain'`); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
var count int
|
||
if err := db.DB.QueryRowContext(t.Context(), "SELECT COUNT(*) FROM resource_events").Scan(&count); err != nil || count != 1 {
|
||
t.Fatalf("已有触发器升级后未执行:%d,%v", count, err)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestSQLiteResourceProjectMigrationRollback(t *testing.T) {
|
||
for _, failure := range []struct{ name, setup, repair string }{
|
||
{"version", `CREATE TRIGGER resource_version_failure BEFORE INSERT ON schema_migrations WHEN new.version = 11 BEGIN SELECT RAISE(ABORT, 'test-only'); END`, `DROP TRIGGER resource_version_failure`},
|
||
{"orphan_domain", `INSERT INTO domain_servers (domain_id, server_id) VALUES ('missing', 'server')`, `DELETE FROM domain_servers WHERE domain_id = 'missing'`},
|
||
{"orphan_server", `INSERT INTO domain_servers (domain_id, server_id) VALUES ('domain', 'missing')`, `DELETE FROM domain_servers WHERE server_id = 'missing'`},
|
||
{"missing_project", `DELETE FROM projects WHERE id = 'default'`, `INSERT INTO projects (id, name, description) VALUES ('default', '默认项目', '')`},
|
||
} {
|
||
t.Run(failure.name, func(t *testing.T) {
|
||
db, err := Open(t.Context(), sqliteBusinessUpgradeConfig(t))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer db.Close()
|
||
createSQLiteHistoricalBusiness(t, db.DB, 10)
|
||
if _, err := db.DB.ExecContext(t.Context(), failure.setup); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
before := snapshotSQLiteHistoricalBusiness(t, db.DB)
|
||
schemaQuery := `SELECT type, name, sql FROM sqlite_master ORDER BY name`
|
||
schema := snapshotSQLiteUpgradeQuery(t, db.DB, schemaQuery)
|
||
if err := Migrate(t.Context(), db.DB, "sqlite"); err == nil {
|
||
t.Fatal("故障未阻止资源迁移")
|
||
}
|
||
assertSQLiteHistoricalBusiness(t, db.DB, before)
|
||
if actual := snapshotSQLiteUpgradeQuery(t, db.DB, schemaQuery); actual != schema {
|
||
t.Fatal("资源迁移失败后未完整回滚模式")
|
||
}
|
||
version, err := currentMigrationVersion(t.Context(), db.DB)
|
||
if err != nil || version != 10 {
|
||
t.Fatalf("资源迁移失败仍登记版本:%d,%v", version, err)
|
||
}
|
||
if _, err := db.DB.ExecContext(t.Context(), failure.repair); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
for round := 0; round < 2; round++ {
|
||
if err := Migrate(t.Context(), db.DB, "sqlite"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|