Smail/backend/config/manager_security_test.go
smail 560605a8e1
Some checks are pending
Security Checks / security (push) Waiting to run
统一配置JSON缩进排版并保持摘要与历史兼容
2026-09-08 21:08:36 +08:00

211 lines
6.3 KiB
Go

package config
import (
"encoding/json"
"errors"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func TestManagerApplySynchronizesConfigGeneration(t *testing.T) {
manager, err := NewManager(testRootConfig())
if err != nil {
t.Fatal(err)
}
candidate := manager.Snapshot().Config
candidate.Generation = 999
candidate.SetupState = SetupStateReady
updated, err := manager.Apply(candidate, 1)
if err != nil {
t.Fatal(err)
}
if updated.Generation != 2 || updated.Config.Generation != updated.Generation {
t.Fatalf("inconsistent generation: %+v", updated)
}
current := manager.Snapshot()
if current.Generation != 2 || current.Config.Generation != 2 {
t.Fatalf("inconsistent current snapshot: %+v", current)
}
}
func TestManagerRollbackCreatesANewGeneration(t *testing.T) {
root := t.TempDir()
manager, err := NewManager(testRootConfig())
if err != nil {
t.Fatal(err)
}
candidate := manager.Snapshot().Config
candidate.SetupState = SetupStateReady
updated, backupPath, err := manager.ApplyPersisted(root, candidate, 1, time.Date(2026, 8, 26, 1, 0, 0, 0, time.UTC))
if err != nil {
t.Fatal(err)
}
rolledBack, rollbackBackup, err := manager.RollbackPersisted(root, backupPath, updated.Generation, time.Date(2026, 8, 26, 2, 0, 0, 0, time.UTC))
if err != nil {
t.Fatal(err)
}
if rolledBack.Generation != 3 || rolledBack.Config.Generation != 3 || rolledBack.Config.SetupState != SetupStateBootstrap {
t.Fatalf("unexpected rollback snapshot: %+v", rolledBack)
}
if rollbackBackup == "" || rollbackBackup == backupPath {
t.Fatalf("unexpected rollback backup path: %q", rollbackBackup)
}
}
func TestManagerRollbackRejectsBackupOutsideManagedDirectory(t *testing.T) {
root := t.TempDir()
manager, err := NewManager(testRootConfig())
if err != nil {
t.Fatal(err)
}
outside := filepath.Join(t.TempDir(), "config-00000000000000000001-outside.json")
data, err := json.Marshal(manager.Snapshot())
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(outside, data, 0600); err != nil {
t.Fatal(err)
}
if _, _, err := manager.RollbackPersisted(root, outside, 1, time.Now()); !errors.Is(err, ErrNoBackup) {
t.Fatalf("outside backup error = %v", err)
}
}
func TestLoadManagerRewritesLegacyVersion(t *testing.T) {
root := t.TempDir()
directory := filepath.Join(root, "config")
if err := os.MkdirAll(directory, 0700); err != nil {
t.Fatal(err)
}
legacy := testRootConfig()
legacy.SchemaVersion = 0
legacy.Generation = 0
legacy.Version = 7
data, err := json.Marshal(legacy)
if err != nil {
t.Fatal(err)
}
path := filepath.Join(directory, "config.json")
if err := os.WriteFile(path, data, 0600); err != nil {
t.Fatal(err)
}
manager, err := LoadManager(root, testRootConfig())
if err != nil {
t.Fatal(err)
}
if snapshot := manager.Snapshot(); snapshot.Generation != 7 || snapshot.Config.SchemaVersion != CurrentSchemaVersion {
t.Fatalf("legacy snapshot = %+v", snapshot)
}
rewritten, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
var saved RootConfig
if err := json.Unmarshal(rewritten, &saved); err != nil {
t.Fatal(err)
}
if strings.Contains(string(rewritten), `"version"`) || saved.SchemaVersion != CurrentSchemaVersion || saved.Generation != 7 {
t.Fatal("旧配置未正确更新版本和代次")
}
}
func TestManagerRejectsFutureSchemaVersion(t *testing.T) {
candidate := testRootConfig()
candidate.SchemaVersion = CurrentSchemaVersion + 1
if _, err := NewManager(candidate); err == nil {
t.Fatal("future schema version was accepted")
}
}
func TestLoadManagerRejectsSymlinkConfigFile(t *testing.T) {
root := t.TempDir()
directory := filepath.Join(root, "config")
if err := os.MkdirAll(directory, 0700); err != nil {
t.Fatal(err)
}
outside := filepath.Join(t.TempDir(), "outside.json")
data, err := json.Marshal(testRootConfig())
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(outside, data, 0600); err != nil {
t.Fatal(err)
}
if err := os.Symlink(outside, filepath.Join(directory, "config.json")); err != nil {
t.Skipf("symlink is unavailable: %v", err)
}
if _, err := LoadManager(root, testRootConfig()); err == nil {
t.Fatal("symlink configuration file was accepted")
}
}
func TestBackupRejectsSymlinkDirectoryAndEntry(t *testing.T) {
t.Run("directory", func(t *testing.T) {
root := t.TempDir()
if err := os.MkdirAll(filepath.Join(root, "data"), 0700); err != nil {
t.Fatal(err)
}
if err := os.Symlink(t.TempDir(), filepath.Join(root, "data", "backups")); err != nil {
t.Skipf("symlink is unavailable: %v", err)
}
if _, err := BackupConfig(root, RuntimeSnapshot{Config: testRootConfig(), Generation: 1}, time.Now()); err == nil {
t.Fatal("symlink backup directory was accepted")
}
})
t.Run("entry", func(t *testing.T) {
root := t.TempDir()
directory := filepath.Join(root, "data", "backups")
if err := os.MkdirAll(directory, 0700); err != nil {
t.Fatal(err)
}
outside := filepath.Join(t.TempDir(), "outside.json")
if err := os.WriteFile(outside, []byte("{}"), 0600); err != nil {
t.Fatal(err)
}
if err := os.Symlink(outside, filepath.Join(directory, "config-malicious.json")); err != nil {
t.Skipf("symlink is unavailable: %v", err)
}
if _, err := BackupConfig(root, RuntimeSnapshot{Config: testRootConfig(), Generation: 1}, time.Now()); err == nil {
t.Fatal("symlink backup entry was accepted")
}
})
}
func TestLoadBackupRejectsGenerationMismatch(t *testing.T) {
path := filepath.Join(t.TempDir(), "backup.json")
snapshot := RuntimeSnapshot{Config: testRootConfig(), Generation: 2}
data, err := json.Marshal(snapshot)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, data, 0600); err != nil {
t.Fatal(err)
}
if _, err := LoadBackup(path); !errors.Is(err, ErrNoBackup) {
t.Fatalf("generation mismatch error = %v", err)
}
}
func TestBackupRetentionKeepsExactlyFourRecentManagedFiles(t *testing.T) {
root := t.TempDir()
base := time.Date(2026, 8, 26, 0, 0, 0, 0, time.UTC)
for generation := uint64(1); generation <= 6; generation++ {
cfg := testRootConfig()
cfg.Generation = generation
_, err := BackupConfig(root, RuntimeSnapshot{Config: cfg, Generation: generation}, base.Add(time.Duration(generation)*time.Hour))
if err != nil {
t.Fatal(err)
}
}
entries, err := os.ReadDir(filepath.Join(root, "data", "backups"))
if err != nil {
t.Fatal(err)
}
if len(entries) != 4 {
t.Fatalf("backup count = %d", len(entries))
}
}