b93c1543da
gates / gates (push) Successful in 14s
Operator ruling 2026-09-13. The update precondition walks Tier 2, Tier 1 (own recovery unit, "helyi") and Tier 3 (off-site, 15 s bound; unreachable counts as absent with a WARN) and leans on the first FRESH copy; the backup_max_age rule applies to whichever tier is chosen. No copy anywhere: back up first. Refused only when nothing exists and no backup can be taken. RunAppBackupNow tolerates a Tier-2 failure (WARN) and marks the captured unit proven current. The hold names the tier (második meghajtó / saját meghajtó / távoli mentés) and the date; pre-v0.239.0 holds keep their text. A successful off-site restore now lifts an update hold. The backups page still uses Tier2UnitRestorePoint unchanged. Scenarios G-M tested; red-proofs M, L, the tail and the off-site clear in felhom.eu documentation/audits/rulings-r472-r475-2026-09-13/.
75 lines
2.9 KiB
Go
75 lines
2.9 KiB
Go
package web
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
|
)
|
|
|
|
// Slice 4 — the predicate moved from this package into backup.Tier2UnitRestorePoint. The page must
|
|
// render IDENTICALLY: the row's four unit-restore fields are compared against the coverage computed
|
|
// the old way (the inline expression that used to live in buildAppBackupRows).
|
|
func TestSlice4_BackupRowUnitFieldsAreUnchangedByTheExtraction(t *testing.T) {
|
|
s, sett, m := newOffboxWebServer(t)
|
|
dest := t.TempDir()
|
|
if err := sett.AddStoragePath(settings.StoragePath{Path: dest, Label: "flash"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := sett.SetCrossDriveConfig("app", &settings.CrossDriveBackup{
|
|
Enabled: true, Method: "rsync", DestinationPath: dest,
|
|
LastRun: "2026-09-13T01:30:00Z", LastStatus: "ok", LastSuccess: "2026-09-13T01:30:00Z", SuccessTracked: true,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
unit := filepath.Join(dest, "backups", "secondary", "app", "recovery-unit")
|
|
if err := os.MkdirAll(unit, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(unit, "manifest.json"), []byte(`{"schema_version":2,"app_name":"app","created_at":"2026-09-12T02:15:29Z"}`), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(dest, "backups", "secondary", "app", ".felhom-tier2-layout"), []byte("2"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cov, err := m.Tier2RestoreCoverage("app")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantDate, wantStale := cov.UnitRestoreDate()
|
|
rows := s.buildAppBackupRows(&backup.FullBackupStatus{AppDataInfo: []backup.AppBackupInfo{{StackName: "app", DisplayName: "App"}}})
|
|
row := findRow(rows, "app")
|
|
if row == nil {
|
|
t.Fatal("no row")
|
|
}
|
|
if !cov.CanRestoreUnit() {
|
|
t.Fatal("fixture: the copy must hold an openable unit")
|
|
}
|
|
if row.Tier2UnitRestorable != cov.CanRestoreUnit() || row.Tier2CopyDate != wantDate ||
|
|
row.Tier2CopyDateProven != (cov.CopyLastSuccess != "") ||
|
|
row.Tier2UnitConfirm != tier2UnitConfirmWithStaleness(wantDate, cov.CopyLastSuccess != "", wantStale) {
|
|
t.Errorf("the row changed: restorable=%v date=%q proven=%v confirm=%q", row.Tier2UnitRestorable, row.Tier2CopyDate, row.Tier2CopyDateProven, row.Tier2UnitConfirm)
|
|
}
|
|
}
|
|
|
|
// The drive-return gate starts apps UNATTENDED. A held app must be skipped.
|
|
//
|
|
// COMPANION RED-PROOF (REPORT.md): delete the appHeld check from restartStacks. The server has NO stack
|
|
// manager here on purpose, so the unguarded StartStack call panics and this test fails.
|
|
func TestSlice4_DriveReturnGateSkipsAHeldApp(t *testing.T) {
|
|
s, _, m := newOffboxWebServer(t)
|
|
if err := m.HoldAfterFailedUpdate("held", time.Now(), time.Now(), backup.UpdateTierLocal); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Fatalf("the drive-return gate tried to START a held app: %v", r)
|
|
}
|
|
}()
|
|
s.restartStacks([]string{"held"})
|
|
}
|