Smail/server/config/snapshot_test.go
smail 70a2f39042
Some checks are pending
Security Checks / security (push) Waiting to run
完善配置快照预检与数据库迁移失败处理
2026-08-30 21:34:23 +08:00

254 lines
7.6 KiB
Go

package config
import (
"encoding/json"
"errors"
"fmt"
"sync"
"testing"
)
func TestSnapshotStoreRejectsInvalidCandidate(t *testing.T) {
initial := validRuntimeConfig()
store, err := NewSnapshotStore(initial, RuntimeValidationOptions{HasSuperAdmin: true, Production: true})
if err != nil {
t.Fatal(err)
}
invalid := validRuntimeConfig()
invalid.Email.Policy.IMAPPort = invalid.Root.HTTP.HTTPSPort
if err := store.Publish(invalid); err == nil {
t.Fatal("invalid runtime configuration was published")
}
if store.Generation() != initial.Generation {
t.Fatal("rejected publication changed the active generation")
}
}
func TestSnapshotStoreValidateDoesNotPublishCandidate(t *testing.T) {
initial := validRuntimeConfig()
store, err := NewSnapshotStore(initial, RuntimeValidationOptions{HasSuperAdmin: true, Production: true})
if err != nil {
t.Fatal(err)
}
candidate := validRuntimeConfig()
candidate.Generation = 2
candidate.Root.Generation = 2
candidate.Root.Sections = validSectionReferences(2)
candidate.SMTP.Meta.Generation = 2
candidate.Email.Meta.Generation = 2
candidate.SSL.Meta.Generation = 2
candidate.Plugins.Meta.Generation = 2
if err := store.Validate(candidate); err != nil {
t.Fatal(err)
}
if got := store.Generation(); got != initial.Generation {
t.Fatalf("validation changed active generation: got %d, want %d", got, initial.Generation)
}
}
func TestSnapshotStoreReturnsDeepCopies(t *testing.T) {
initial := validRuntimeConfig()
store, err := NewSnapshotStore(initial, RuntimeValidationOptions{HasSuperAdmin: true, Production: true})
if err != nil {
t.Fatal(err)
}
initial.Root.Domains.Accepted[0] = "mutated.example.com"
initial.Root.Sections[SectionSMTP] = SectionRef{}
plugin := initial.Plugins.Plugins["telegram_push"]
plugin.Permissions.Capabilities[0] = "filesystem"
plugin.Permissions.Network[0].Host = "mutated.example.com"
plugin.Settings[0] = '['
initial.Plugins.Plugins["telegram_push"] = plugin
first, err := store.Snapshot()
if err != nil {
t.Fatal(err)
}
if first.Root.Domains.Accepted[0] != "mail.example.com" || first.Root.Sections[SectionSMTP].File != "smtp.json" {
t.Fatal("published root snapshot shares mutable input data")
}
firstPlugin := first.Plugins.Plugins["telegram_push"]
if firstPlugin.Permissions.Capabilities[0] != "network" || firstPlugin.Permissions.Network[0].Host != "api.telegram.org" || !json.Valid(firstPlugin.Settings) {
t.Fatal("published plugin snapshot shares mutable input data")
}
first.Root.Domains.Accepted[0] = "reader.example.com"
firstPlugin.Settings[0] = '['
first.Plugins.Plugins["telegram_push"] = firstPlugin
second, err := store.Snapshot()
if err != nil {
t.Fatal(err)
}
if second.Root.Domains.Accepted[0] != "mail.example.com" || !json.Valid(second.Plugins.Plugins["telegram_push"].Settings) {
t.Fatal("caller modified the active snapshot")
}
}
func TestSnapshotStoreConcurrentReadersAndUpdates(t *testing.T) {
initial := validRuntimeConfig()
store, err := NewSnapshotStore(initial, RuntimeValidationOptions{HasSuperAdmin: true, Production: true})
if err != nil {
t.Fatal(err)
}
const updates = 64
const readers = 24
var group sync.WaitGroup
errors := make(chan error, updates+readers)
for reader := 0; reader < readers; reader++ {
group.Add(1)
go func() {
defer group.Done()
var previous uint64
for iteration := 0; iteration < updates*4; iteration++ {
snapshot, err := store.Snapshot()
if err != nil {
errors <- err
return
}
if snapshot.Generation < previous || snapshot.Root.Generation != snapshot.Generation || snapshot.SMTP.Meta.Generation != snapshot.Generation {
errors <- fmt.Errorf("inconsistent runtime generation")
return
}
previous = snapshot.Generation
}
}()
}
group.Add(1)
go func() {
defer group.Done()
for generation := uint64(2); generation <= updates+1; generation++ {
candidate := validRuntimeConfig()
candidate.Generation = generation
candidate.Root.Generation = generation
candidate.Root.Sections = validSectionReferences(generation)
candidate.SMTP.Meta.Generation = generation
candidate.Email.Meta.Generation = generation
candidate.SSL.Meta.Generation = generation
candidate.Plugins.Meta.Generation = generation
if err := store.Publish(candidate); err != nil {
errors <- err
return
}
}
}()
group.Wait()
close(errors)
for err := range errors {
t.Fatal(err)
}
if store.Generation() != updates+1 {
t.Fatalf("unexpected active generation: %d", store.Generation())
}
}
func TestSnapshotStorePreservesOldSnapshotLifecycle(t *testing.T) {
store, err := NewSnapshotStore(validRuntimeConfig(), RuntimeValidationOptions{HasSuperAdmin: true, Production: true})
if err != nil {
t.Fatal(err)
}
old, err := store.Snapshot()
if err != nil {
t.Fatal(err)
}
candidate := validRuntimeConfig()
candidate.Generation = 2
candidate.Root.Generation = 2
candidate.Root.Sections = validSectionReferences(2)
candidate.SMTP.Meta.Generation = 2
candidate.Email.Meta.Generation = 2
candidate.SSL.Meta.Generation = 2
candidate.Plugins.Meta.Generation = 2
if err := store.Publish(candidate); err != nil {
t.Fatal(err)
}
if old.Generation != 1 || old.Root.Generation != 1 {
t.Fatal("publishing a new snapshot modified an old reader copy")
}
}
func TestSnapshotStoreRejectsOlderGeneration(t *testing.T) {
store, err := NewSnapshotStore(validRuntimeConfig(), RuntimeValidationOptions{HasSuperAdmin: true, Production: true})
if err != nil {
t.Fatal(err)
}
newer := validRuntimeConfig()
newer.Generation = 3
newer.Root.Generation = 3
newer.Root.Sections = validSectionReferences(3)
newer.SMTP.Meta.Generation = 3
newer.Email.Meta.Generation = 3
newer.SSL.Meta.Generation = 3
newer.Plugins.Meta.Generation = 3
if err := store.Publish(newer); err != nil {
t.Fatal(err)
}
older := validRuntimeConfig()
older.Generation = 2
older.Root.Generation = 2
older.Root.Sections = validSectionReferences(2)
older.SMTP.Meta.Generation = 2
older.Email.Meta.Generation = 2
older.SSL.Meta.Generation = 2
older.Plugins.Meta.Generation = 2
if err := store.Publish(older); !errors.Is(err, ErrGenerationConflict) {
t.Fatalf("older publication error = %v, want generation conflict", err)
}
if got := store.Generation(); got != 3 {
t.Fatalf("active generation = %d, want 3", got)
}
}
func TestSnapshotStoreConcurrentOlderPublishesKeepNewestGeneration(t *testing.T) {
store, err := NewSnapshotStore(validRuntimeConfig(), RuntimeValidationOptions{HasSuperAdmin: true, Production: true})
if err != nil {
t.Fatal(err)
}
newer := validRuntimeConfig()
newer.Generation = 3
newer.Root.Generation = 3
newer.Root.Sections = validSectionReferences(3)
newer.SMTP.Meta.Generation = 3
newer.Email.Meta.Generation = 3
newer.SSL.Meta.Generation = 3
newer.Plugins.Meta.Generation = 3
if err := store.Publish(newer); err != nil {
t.Fatal(err)
}
const publishers = 32
var group sync.WaitGroup
errorsFound := make(chan error, publishers)
for index := 0; index < publishers; index++ {
group.Add(1)
go func() {
defer group.Done()
older := validRuntimeConfig()
older.Generation = 2
older.Root.Generation = 2
older.Root.Sections = validSectionReferences(2)
older.SMTP.Meta.Generation = 2
older.Email.Meta.Generation = 2
older.SSL.Meta.Generation = 2
older.Plugins.Meta.Generation = 2
if err := store.Publish(older); !errors.Is(err, ErrGenerationConflict) {
errorsFound <- err
}
}()
}
group.Wait()
close(errorsFound)
for err := range errorsFound {
t.Fatalf("older concurrent publication error = %v", err)
}
if got := store.Generation(); got != 3 {
t.Fatalf("active generation = %d, want 3", got)
}
}