174 lines
5.2 KiB
Go
174 lines
5.2 KiB
Go
package config
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.pcux.cn/xuans/Smail/utils/credential"
|
|
"git.pcux.cn/xuans/Smail/utils/runtimepath"
|
|
)
|
|
|
|
func TestDeploymentStateRecoversEveryCommitPhase(t *testing.T) {
|
|
for _, phase := range []TransactionPhase{TransactionPrepared, TransactionSectionsCommitted, TransactionRootCommitted, TransactionVerified} {
|
|
t.Run(string(phase), func(t *testing.T) {
|
|
root := t.TempDir()
|
|
if err := InitializeDeployment(root); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
current := validRuntimeConfig()
|
|
writeRuntimeFixture(t, root, current)
|
|
candidate := cloneRuntimeConfig(current)
|
|
setCandidateGeneration(&candidate, 2)
|
|
candidate.Email.Policy.MaxMessageBytes = 9 << 20
|
|
validation := RuntimeValidationOptions{HasSuperAdmin: true, Production: true}
|
|
store := NewTransactionStore(root, validation).WithHooks(TransactionHooks{BeforePhase: func(actual TransactionPhase) error {
|
|
if actual == phase {
|
|
return errors.New("模拟进程中断")
|
|
}
|
|
return nil
|
|
}})
|
|
if err := store.Commit(current, candidate); err == nil {
|
|
t.Fatal("未注入中断")
|
|
}
|
|
if err := NewTransactionStore(root, validation).Recover(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
loaded, err := NewConfigLoader(root, ConfigLoaderOptions{Validation: validation}).Load()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := uint64(1)
|
|
if phase == TransactionRootCommitted || phase == TransactionVerified {
|
|
want = 2
|
|
}
|
|
if loaded.Generation != want {
|
|
t.Fatalf("恢复代次错误:%d", loaded.Generation)
|
|
}
|
|
if err := store.Recover(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
entries, _ := os.ReadDir(filepath.Join(root, "config"))
|
|
if len(entries) != 5 {
|
|
t.Fatal("config 生成了额外文件")
|
|
}
|
|
entries, _ = os.ReadDir(filepath.Join(root, "data"))
|
|
for _, entry := range entries {
|
|
if entry.Name() != "smail-state.db" {
|
|
t.Fatalf("data 中产生额外文件:%s", entry.Name())
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDeploymentStateImportsHistoryWithoutChangingSecret(t *testing.T) {
|
|
source, target := t.TempDir(), t.TempDir()
|
|
current := validMySQLRuntimeConfig(t, source)
|
|
writeRuntimeFixture(t, source, current)
|
|
validation := RuntimeValidationOptions{HasSuperAdmin: true}
|
|
old := NewTransactionStore(source, validation)
|
|
backup, err := old.CreateBackup(current)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
before, _ := os.ReadFile(filepath.Join(backup, "files", "config.json"))
|
|
if err := InitializeDeployment(target); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := runtimepath.SecureDirectory(target, "ssl"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
key, err := os.ReadFile(filepath.Join(source, "data", "credential.key"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(target, "ssl", "credential.key"), key, 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
writeRuntimeFixture(t, target, current)
|
|
if err := ImportDeploymentHistory(source, target, validation); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
store := NewTransactionStore(target, validation)
|
|
loaded, err := store.LoadBackup(current.Generation)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
vault, err := credential.NewVault(target)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := OpenStoredSecret(vault, loaded.Root.Database.Password); err != nil {
|
|
t.Fatal("迁移后密文无法解密")
|
|
}
|
|
items, err := store.stateBackups()
|
|
if err != nil || len(items) != 1 {
|
|
t.Fatalf("历史导入失败:%v", err)
|
|
}
|
|
if !bytes.Equal(before, items[0].Original["config.json"]) {
|
|
t.Fatal("历史原始内容被改写")
|
|
}
|
|
for index := 0; index < 6; index++ {
|
|
if _, err := store.createBackupAt(current, time.Now().Add(time.Duration(index)*time.Hour)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
items, err = store.stateBackups()
|
|
if err != nil || len(items) != 4 {
|
|
t.Fatalf("历史保留策略错误:%v", err)
|
|
}
|
|
}
|
|
|
|
func TestDeploymentStateRejectsTamperAndConcurrentWriter(t *testing.T) {
|
|
root := t.TempDir()
|
|
if err := InitializeDeployment(root); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
current := validRuntimeConfig()
|
|
writeRuntimeFixture(t, root, current)
|
|
store := NewTransactionStore(root, RuntimeValidationOptions{HasSuperAdmin: true})
|
|
if _, err := store.CreateBackup(current); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
unlock, err := store.lockState()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if another, err := store.lockState(); err == nil {
|
|
another()
|
|
t.Fatal("并行写入没有被拒绝")
|
|
}
|
|
unlock()
|
|
db, err := store.openState()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err = db.Exec("UPDATE config_records SET payload=? WHERE kind='backup'", []byte("{}"))
|
|
db.Close()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := store.LoadBackup(1); !errors.Is(err, ErrConfigDigestMismatch) {
|
|
t.Fatalf("篡改未被拒绝:%v", err)
|
|
}
|
|
}
|
|
|
|
func TestDeploymentManagedReferenceUsesArgumentRoot(t *testing.T) {
|
|
root := t.TempDir()
|
|
if err := InitializeDeployment(root); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
private, certificate := testManagedCertificate(t, "example.com", time.Now().Add(-time.Hour), time.Now().Add(time.Hour), 2048)
|
|
ref, err := importManagedSSLAtRoot(root, ManagedSSLModeUpload, private, certificate, nil, []string{"example.com"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if ref.CertificateRef != "ssl/managed/upload/current/public.crt" {
|
|
t.Fatalf("引用错误:%s", ref.CertificateRef)
|
|
}
|
|
}
|