Placement hardening (F-3a-1..4) + enlarge-blocked delivery chain (Task 3a-fix, v0.134.1)
PlaceOffsiteRestore: live target via raw GetStackHDDPath not AppNamespaceRoot (F-3a-1a: no SSD merge; undeployed refused), placement headroom gate (F-3a-1b), stat pre-pass over all placements before any copy (F-3a-4: no partial writes), scratch removed on success/kept on failure (F-3a-2). mapOffsiteRestorePaths refuses the namespace root itself (F-3a-3). Delivery chain: DefaultEnabledEvents + GetNotificationPrefs append-if-absent migration + settings checkbox + handler slice; paired with hub v0.55.0 allowlist (no customerMessages entry — raw dynamic message survives). +8 tests; all 6 controller §10 red-proofs verified.
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func contains(list []string, want string) bool {
|
||||
for _, e := range list {
|
||||
if e == want {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
func count(list []string, want string) int {
|
||||
n := 0
|
||||
for _, e := range list {
|
||||
if e == want {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// F2a: the new warning type is on by default for new customers.
|
||||
func TestDefaultEnabledEvents_ContainsEnlargeBlocked(t *testing.T) {
|
||||
if !contains(DefaultEnabledEvents, "offbox_enlarge_blocked") {
|
||||
t.Error("DefaultEnabledEvents must contain offbox_enlarge_blocked (new customers get it)")
|
||||
}
|
||||
}
|
||||
|
||||
// F2b: an EXISTING customer's stored prefs (predating the type) gain it via append-if-absent —
|
||||
// idempotent (two reads → one entry) and their OTHER choices are preserved.
|
||||
func TestGetNotificationPrefs_MigratesExisting(t *testing.T) {
|
||||
s, err := Load(filepath.Join(t.TempDir(), "settings.json"), discardLog())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// A customer who kept only two events and never had the new one.
|
||||
if err := s.SetNotificationPrefs(&NotificationPrefs{
|
||||
Email: "c@example.com",
|
||||
EnabledEvents: []string{"backup_failed", "disk_warning"},
|
||||
CooldownHours: 6,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
p1 := s.GetNotificationPrefs()
|
||||
if !contains(p1.EnabledEvents, "offbox_enlarge_blocked") {
|
||||
t.Error("existing prefs must gain offbox_enlarge_blocked (append-if-absent migration)")
|
||||
}
|
||||
if !contains(p1.EnabledEvents, "backup_failed") || !contains(p1.EnabledEvents, "disk_warning") {
|
||||
t.Error("the customer's existing choices must be preserved")
|
||||
}
|
||||
|
||||
// idempotent: a second read still has exactly ONE entry.
|
||||
p2 := s.GetNotificationPrefs()
|
||||
if c := count(p2.EnabledEvents, "offbox_enlarge_blocked"); c != 1 {
|
||||
t.Errorf("migration must be idempotent, got %d entries", c)
|
||||
}
|
||||
}
|
||||
|
||||
// A customer who already has the type keeps exactly one (no duplication).
|
||||
func TestGetNotificationPrefs_AlreadyPresentNoDuplicate(t *testing.T) {
|
||||
s, err := Load(filepath.Join(t.TempDir(), "settings.json"), discardLog())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.SetNotificationPrefs(&NotificationPrefs{
|
||||
EnabledEvents: []string{"offbox_enlarge_blocked", "backup_failed"},
|
||||
CooldownHours: 6,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c := count(s.GetNotificationPrefs().EnabledEvents, "offbox_enlarge_blocked"); c != 1 {
|
||||
t.Errorf("already-present type must not duplicate, got %d", c)
|
||||
}
|
||||
}
|
||||
@@ -241,6 +241,7 @@ var DefaultEnabledEvents = []string{
|
||||
"health_critical",
|
||||
"expected_backup_missed",
|
||||
"expected_dbdump_missed",
|
||||
"offbox_enlarge_blocked", // 3a-fix (warning-class): remote enlargement refused by the quota gate
|
||||
}
|
||||
|
||||
// PendingEvent is an event queued for the next Hub push cycle.
|
||||
@@ -522,10 +523,23 @@ func (s *Settings) GetNotificationPrefs() *NotificationPrefs {
|
||||
// Return a copy of the slice
|
||||
events := make([]string, len(prefs.EnabledEvents))
|
||||
copy(events, prefs.EnabledEvents)
|
||||
prefs.EnabledEvents = events
|
||||
// 3a-fix append-if-absent migration: an existing customer's stored prefs predate
|
||||
// offbox_enlarge_blocked, so they cannot have deliberately disabled it — surface it enabled so the
|
||||
// checkbox renders checked and the startup sync (main.go:782) carries it to the hub. Idempotent.
|
||||
prefs.EnabledEvents = appendIfAbsent(events, "offbox_enlarge_blocked")
|
||||
return &prefs
|
||||
}
|
||||
|
||||
// appendIfAbsent appends want to list only if it is not already present (idempotent).
|
||||
func appendIfAbsent(list []string, want string) []string {
|
||||
for _, e := range list {
|
||||
if e == want {
|
||||
return list
|
||||
}
|
||||
}
|
||||
return append(list, want)
|
||||
}
|
||||
|
||||
// SetNotificationPrefs updates notification preferences and saves to disk.
|
||||
// H17: Deep-copies prefs so caller mutations after the call don't affect stored state.
|
||||
func (s *Settings) SetNotificationPrefs(prefs *NotificationPrefs) error {
|
||||
|
||||
Reference in New Issue
Block a user