179 lines
6.2 KiB
Go
179 lines
6.2 KiB
Go
package cli
|
||
|
||
import (
|
||
"crypto/tls"
|
||
"os"
|
||
"path/filepath"
|
||
"reflect"
|
||
"strings"
|
||
"testing"
|
||
|
||
"git.pcux.cn/xuans/Scert/backend/internal/config"
|
||
"git.pcux.cn/xuans/Scert/backend/internal/database"
|
||
"git.pcux.cn/xuans/Scert/backend/internal/deployment"
|
||
"git.pcux.cn/xuans/Scert/backend/internal/repository"
|
||
"git.pcux.cn/xuans/Scert/backend/internal/secretstore"
|
||
"golang.org/x/crypto/bcrypt"
|
||
)
|
||
|
||
func TestSQLiteBusinessBackupRestore(t *testing.T) {
|
||
fixture := newSQLiteBusinessFixture(t)
|
||
cfg := fixture.config
|
||
previous := configPath
|
||
configPath = fixture.path
|
||
t.Cleanup(func() { configPath = previous })
|
||
expected := snapshotSQLiteBusiness(t, cfg)
|
||
input := filepath.Join(t.TempDir(), "business-backup")
|
||
backup := newBackupCommand()
|
||
var output strings.Builder
|
||
backup.SetOut(&output)
|
||
backup.SetErr(&output)
|
||
backup.SetArgs([]string{"--output", input})
|
||
if err := backup.ExecuteContext(t.Context()); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := validateBackupManifest(input); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
// 备份后改变数据库、配置、加密凭据和证书,确保恢复不是空操作。
|
||
db, err := database.Open(t.Context(), cfg.Database)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer db.Close()
|
||
for _, query := range []string{
|
||
"UPDATE settings SET value = '变更后'",
|
||
"UPDATE users SET status = 'disabled', groups_json = '[]'",
|
||
"UPDATE domains SET status = 'failed'",
|
||
"UPDATE servers SET name = '变更后服务器'",
|
||
"DELETE FROM domain_servers",
|
||
"UPDATE tasks SET message = '变更后任务'",
|
||
"DELETE FROM task_attempts",
|
||
"INSERT INTO audit_logs (action, resource_type, resource_id, status, message) VALUES ('after-backup', 'test', 'test', 'success', '不应进入恢复结果')",
|
||
} {
|
||
if _, err := db.DB.ExecContext(t.Context(), query); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
if err := db.Close(); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
changed := cfg
|
||
changed.Server.Port = 19005
|
||
if err := changed.Save(fixture.path); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
store, err := secretstore.Open(cfg.Paths.Config)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
for reference := range fixture.secrets {
|
||
if err := store.Put(t.Context(), reference, map[string]string{"changed": "不应保留"}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
if err := os.WriteFile(fixture.certificate.CertificatePath, []byte("已替换的证书"), 0o600); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
for _, round := range []string{"first_restore", "repeat_restore"} {
|
||
t.Run(round, func(t *testing.T) {
|
||
restore := newRestoreCommand()
|
||
restore.SetOut(&output)
|
||
restore.SetErr(&output)
|
||
restore.SetArgs([]string{"--input", input})
|
||
if err := restore.ExecuteContext(t.Context()); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
actual := snapshotSQLiteBusiness(t, cfg)
|
||
if !reflect.DeepEqual(actual, expected) {
|
||
for table, want := range expected {
|
||
if actual[table] != want {
|
||
t.Errorf("恢复后的表或模式不一致:%s", table)
|
||
}
|
||
}
|
||
if len(actual) != len(expected) {
|
||
t.Error("恢复后的表清单数量不一致")
|
||
}
|
||
}
|
||
assertSQLiteBusinessUsable(t, fixture)
|
||
// 同一份备份可重复恢复,读取和恢复均不能修改归档清单。
|
||
if err := validateBackupManifest(input); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func assertSQLiteBusinessUsable(t *testing.T, fixture sqliteBusinessFixture) {
|
||
t.Helper()
|
||
cfg, err := config.Load(fixture.path)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if cfg.Server.Port != fixture.config.Server.Port || cfg.DNS != fixture.config.DNS {
|
||
t.Fatal("服务配置或 DNS 归属未恢复")
|
||
}
|
||
store, err := secretstore.Open(cfg.Paths.Config)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
for reference, want := range fixture.secrets {
|
||
actual, err := store.Get(t.Context(), reference)
|
||
if err != nil || !reflect.DeepEqual(actual, want) {
|
||
t.Fatalf("恢复后的加密凭据无法正确读取:%v", err)
|
||
}
|
||
}
|
||
if _, err := tls.LoadX509KeyPair(fixture.certificate.CertificatePath, fixture.certificate.PrivateKeyPath); err != nil {
|
||
t.Fatalf("恢复后的证书与私钥不可用:%v", err)
|
||
}
|
||
db, err := database.Open(t.Context(), cfg.Database)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer db.Close()
|
||
users := repository.NewSQLUserRepository(db.DB)
|
||
user, err := users.FindByIdentifier(t.Context(), "recovery-owner")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if user.Status != "active" || user.Role != "super_admin" || bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(fixture.password)) != nil {
|
||
t.Fatal("恢复后的用户密码或角色不可用")
|
||
}
|
||
member, err := users.GetUserByID(t.Context(), "member")
|
||
if err != nil || !reflect.DeepEqual(member.Groups, []string{"测试分组"}) {
|
||
t.Fatalf("用户分组未恢复:%v", err)
|
||
}
|
||
domains := repository.NewSQLDomainRepository(db.DB)
|
||
item, err := domains.GetByID(t.Context(), "domain")
|
||
if err != nil || item.Status != "issued" || item.ExpiresAt == nil || !item.ExpiresAt.Equal(fixture.certificate.NotAfter) {
|
||
t.Fatalf("域名和到期时间未恢复:%v", err)
|
||
}
|
||
servers := repository.NewSQLServerRepository(db.DB)
|
||
server, err := servers.GetServerByID(t.Context(), "server")
|
||
if err != nil || server.AuthMethod != deployment.SSHAuthPassword || server.PasswordRef != "" {
|
||
t.Fatalf("服务器认证方式未按密码模式恢复:%v", err)
|
||
}
|
||
tasks := repository.NewSQLTaskRepository(db.DB)
|
||
task, err := tasks.GetTask(t.Context(), "task")
|
||
if err != nil || task.DomainID != item.ID || task.OperatorID != user.ID || task.CertificateVersionID != fixture.certificate.Version {
|
||
t.Fatalf("任务资源关联未恢复:%v", err)
|
||
}
|
||
attempts, err := tasks.ListAttempts(t.Context(), task.ID)
|
||
if err != nil || len(attempts) != 1 || attempts[0].Status != "success" {
|
||
t.Fatalf("任务尝试未恢复:%v", err)
|
||
}
|
||
var bindings int
|
||
if err := db.DB.QueryRowContext(t.Context(), "SELECT COUNT(*) FROM domain_servers WHERE domain_id = ? AND server_id = ?", item.ID, server.ID).Scan(&bindings); err != nil || bindings != 1 {
|
||
t.Fatalf("域名部署服务器关联未恢复:%v", err)
|
||
}
|
||
result, err := db.DB.ExecContext(t.Context(), "INSERT INTO audit_logs (action, resource_type, resource_id, status, message) VALUES ('after-restore', 'test', 'test', 'success', '恢复后写入')")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if id, err := result.LastInsertId(); err != nil || id != 2 {
|
||
t.Fatalf("审计自增序列未恢复:编号=%d,错误=%v", id, err)
|
||
}
|
||
}
|