修正回滚预览按实际业务差异展示变更分区
Some checks are pending
Security Checks / security (push) Waiting to run

This commit is contained in:
smail 2026-09-28 13:03:14 +08:00
parent 6daec2fe3e
commit 0fd576a680
2 changed files with 80 additions and 2 deletions

View File

@ -528,7 +528,8 @@ func (m *ConfigManager) PreviewRollback(ctx context.Context, generation uint64,
return nil, err
}
m.mu.RLock()
currentGeneration := m.current.Generation
current := cloneRuntimeConfig(m.current)
currentGeneration := current.Generation
pending := m.pending != nil
m.mu.RUnlock()
if pending || generation >= currentGeneration {
@ -547,11 +548,22 @@ func (m *ConfigManager) PreviewRollback(ctx context.Context, generation uint64,
if err := validateRollbackSSL(m.loader.root, backup); err != nil {
return nil, err
}
changed := make(map[Section]bool)
for _, change := range AuditChanges(current, backup) {
section, _, _ := strings.Cut(change.Field, ".")
changed[Section(section)] = true
}
changedSections := make([]Section, 0, len(changed))
for _, section := range []Section{SectionRoot, SectionSMTP, SectionEmail, SectionSSL, SectionPlugins} {
if changed[section] {
changedSections = append(changedSections, section)
}
}
return &RollbackPreview{
CurrentGeneration: currentGeneration,
TargetGeneration: generation,
CandidateGeneration: currentGeneration + 1,
ChangedSections: RequiredPartitionSections(),
ChangedSections: changedSections,
}, nil
}

View File

@ -6,6 +6,7 @@ import (
"errors"
"os"
"path/filepath"
"slices"
"strings"
"sync"
"testing"
@ -1720,6 +1721,71 @@ func TestConfigManagerRollbackTicketIsBoundAndSingleUse(t *testing.T) {
}
}
func TestConfigManagerRollbackPreviewChangedSections(t *testing.T) {
tests := []struct {
name string
sections []Section
mutate bool
want []Section
}{
{name: "root_only", sections: []Section{SectionRoot}, mutate: true, want: []Section{SectionRoot}},
{name: "email_only", sections: []Section{SectionEmail}, mutate: true, want: []Section{SectionEmail}},
{name: "stable_order", sections: []Section{SectionEmail, SectionRoot}, mutate: true, want: []Section{SectionRoot, SectionEmail}},
{name: "metadata_only", sections: []Section{SectionEmail}, want: []Section{}},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
root := t.TempDir()
writeRuntimeFixture(t, root, validRuntimeConfig())
options := ConfigLoaderOptions{Validation: RuntimeValidationOptions{HasSuperAdmin: true, Production: true}}
manager, err := NewConfigManager(root, options)
if err != nil {
t.Fatal(err)
}
for _, section := range test.sections {
current := manager.Snapshot()
var value any
switch section {
case SectionRoot:
if test.mutate {
current.Root.LogLevel = "warn"
}
value = current.Root
case SectionEmail:
if test.mutate {
current.Email.Policy.MaxMessageBytes = 8 << 20
}
value = current.Email
}
payload, err := json.Marshal(value)
if err != nil {
t.Fatal(err)
}
updated, err := manager.Update(context.Background(), UpdateRequest{
Section: section, ExpectedGeneration: current.Generation,
Payload: payload, Actor: authorizedConfigActor(),
})
if err != nil {
t.Fatal(err)
}
if _, err := manager.Apply(context.Background(), updated.Generation); err != nil {
t.Fatal(err)
}
}
preview, err := manager.PreviewRollback(context.Background(), 1, authorizedConfigActor())
if err != nil {
t.Fatal(err)
}
if !slices.Equal(preview.ChangedSections, test.want) {
t.Fatalf("changed sections = %v, want %v", preview.ChangedSections, test.want)
}
if preview.ChangedSections == nil {
t.Fatal("changed sections must be an array, not null")
}
})
}
}
func TestConfigManagerRollbackRequiresStepUpButNotRecoveryCodeForSuperAdmin(t *testing.T) {
root := t.TempDir()
current := validRuntimeConfig()