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
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 写入了运行文件")
}
}