Smail/test/backend/data_paths_test.go
smail a045a0cd17
Some checks are pending
Security Checks / security (push) Waiting to run
实现部署启动自动归档密钥与证书双版本保留
2026-09-08 20:34:38 +08:00

131 lines
4.5 KiB
Go

package config
import (
"bytes"
"context"
"os"
"path/filepath"
"strings"
"testing"
"time"
"git.pcux.cn/xuans/Smail/services/externalemail"
"git.pcux.cn/xuans/Smail/utils/credential"
"git.pcux.cn/xuans/Smail/utils/runtimepath"
)
func TestDataMigrationKeepsSecretsCertificatesAndBackupRestorable(t *testing.T) {
root := t.TempDir()
runtime := validMySQLRuntimeConfig(t, root)
vault, err := credential.NewVault(root)
if err != nil {
t.Fatal(err)
}
external, err := externalemail.NewVault(root)
if err != nil {
t.Fatal(err)
}
emailCipher, _ := external.Encrypt("test@example.com")
fingerprint, _ := external.Fingerprint("test@example.com")
private, certificate := testManagedCertificate(t, "example.com", time.Now().Add(-time.Hour), time.Now().Add(time.Hour), 2048)
reference, err := stageManagedSSLAtRoot(root, ManagedSSLModeUpload, private, certificate, nil, []string{"example.com"})
if err != nil {
t.Fatal(err)
}
runtime.SSL.TLS = SSLConfig{
Mode: ManagedSSLModeUpload, Management: SSLManagementCustom,
DefaultDirectory: "config/ssl", CustomDirectory: "config/ssl",
CertificateRef: strings.Replace(reference.CertificateRef, "data/", "config/", 1),
PrivateKeyRef: strings.Replace(reference.PrivateKeyRef, "data/", "config/", 1),
}
writeRuntimeFixture(t, root, runtime)
validation := RuntimeValidationOptions{HasSuperAdmin: true}
store := NewTransactionStore(root, validation)
if _, err := store.CreateBackup(runtime); err != nil {
t.Fatal(err)
}
writer, err := NewFileAuditWriter(root)
if err != nil {
t.Fatal(err)
}
if err := writer.WriteConfigAudit(context.Background(), ConfigAuditEvent{
Operation: ConfigOperationUpdate, Section: SectionSMTP, ActorID: "1", ActorRole: "super_admin",
FromGeneration: 1, ToGeneration: 2, Result: "success", CreatedAt: time.Now().UTC(),
}); err != nil {
t.Fatal(err)
}
// 构造旧版磁盘布局;文件内容及密文均来自真实实现,而不是手写假密钥。
// 模拟停服时释放暂存租约,真实进程退出后操作系统也会释放文件锁。
releaseManagedSSL(root, SSLConfig{CertificateRef: reference.CertificateRef})
entries, _ := os.ReadDir(filepath.Join(root, "data"))
for _, entry := range entries {
if err := os.Rename(filepath.Join(root, "data", entry.Name()), filepath.Join(root, "config", entry.Name())); err != nil {
t.Fatal(err)
}
}
before, _ := os.ReadFile(filepath.Join(root, "config", "ssl.json"))
if _, err := credential.NewVault(root); err == nil {
t.Fatal("未迁移的旧密钥必须阻止生成新密钥")
}
if _, err := runtimepath.Migrate(root); err != nil {
t.Fatal(err)
}
after, _ := os.ReadFile(filepath.Join(root, "config", "ssl.json"))
if !bytes.Equal(before, after) {
t.Fatal("目录迁移不得篡改当前配置代次或摘要")
}
newVault, err := credential.NewVault(root)
if err != nil {
t.Fatal(err)
}
secret, err := OpenStoredSecret(newVault, runtime.Root.Database.Password)
expected, _ := OpenStoredSecret(vault, runtime.Root.Database.Password)
if err != nil || secret != expected {
t.Fatal("配置密文无法继续解密")
}
newExternal, err := externalemail.NewVault(root)
if err != nil {
t.Fatal(err)
}
plain, err := newExternal.Decrypt(emailCipher)
newFingerprint, _ := newExternal.Fingerprint("test@example.com")
if err != nil || plain != "test@example.com" || newFingerprint != fingerprint {
t.Fatal("外部邮箱密文或指纹发生改变")
}
loaded, err := NewConfigLoader(root, ConfigLoaderOptions{Validation: validation}).Load()
if err != nil || !strings.HasPrefix(loaded.SSL.TLS.CertificateRef, "data/") {
t.Fatalf("旧配置未兼容新目录:%v", err)
}
resolved, err := resolveManagedSSLReferencesAtRoot(root, loaded.SSL.TLS)
if err != nil {
t.Fatal(err)
}
material, _ := os.ReadFile(resolved.CertificatePath)
if !bytes.Equal(material, certificate) {
t.Fatal("证书发生改变")
}
backup, err := store.LoadBackup(runtime.Generation)
if err != nil {
t.Fatal(err)
}
if _, err := resolveManagedSSLReferencesAtRoot(root, backup.SSL.TLS); err != nil {
t.Fatal("历史备份中的旧证书引用无法使用")
}
setCandidateGeneration(&backup, loaded.Generation+1)
if err := store.Commit(loaded, backup); err != nil {
t.Fatal(err)
}
writer, err = NewFileAuditWriter(root)
if err != nil {
t.Fatal(err)
}
items, err := writer.Query(context.Background(), ConfigAuditQuery{})
if err != nil || len(items) != 1 {
t.Fatal("历史审计丢失")
}
entries, _ = os.ReadDir(filepath.Join(root, "config"))
if len(entries) != 5 {
t.Fatal("保存或备份恢复又向 config 写入了运行文件")
}
}