package backup import ( "context" "errors" "testing" "time" "gitea.dooplex.hu/admin/felhom-controller/internal/settings" ) // `09` §3 decision 185 (D1): the operator's abandon_extend and abandon_stop now arrive from the hub, // with no shell and no human reading the CLI's printout. Three guarantees the CLI lever did not give, // each pinned here because the hub action relies on it: // 1. an „extend" never moves the deletion EARLIER (decision 185: no action may shorten a countdown); // 2. an „extend" is refused while the deletion is the HUB's (decision 74) — the box's own date would // change on the page while the hub still deleted on its date; // 3. a „stop" that could not cancel the hub's pending deletion reports a failure, never success. func TestD1_ExtendNeverShortensTheCountdown(t *testing.T) { start := time.Date(2026, 8, 7, 12, 0, 0, 0, time.UTC) m, sett, _ := abandonFixture(t, start) if err := m.ResetOrphanedRepo(context.Background()); err != nil { t.Fatal(err) } before := m.AbandonStatus().DueAt // start + 14 days // Day 1: „extend by 3 days" would mean day 4 — ten days EARLIER than today's due date. m.SetOffboxClock(func() time.Time { return start.AddDate(0, 0, 1) }) if _, err := m.ExtendAbandon(3); err == nil { t.Fatal("an extension that moves the deletion earlier was accepted") } if got := m.AbandonStatus().DueAt; !got.Equal(before) { t.Fatalf("a refused extension changed the due date: %v -> %v", before, got) } if got := sett.GetOffboxTarget().AbandonAt; got != before.Format(time.RFC3339) { t.Fatalf("settings.json due date moved: %q", got) } // Control: a real extension (later than today's due date) is accepted. if due, err := m.ExtendAbandon(20); err != nil || !due.After(before) { t.Fatalf("a real extension: due=%v err=%v", due, err) } } func TestD1_ExtendRefusedWhileTheDeletionIsTheHubs(t *testing.T) { m, sett := newOffboxManager(t) pinTarget(t, sett) hubDue := time.Now().Add(5 * 24 * time.Hour).UTC() if err := sett.UpdateOffboxStatus(func(o *settings.OffboxTarget) { o.AbandonRepoPath = "/home/felhom-repo.orphaned-20260901" o.AbandonHubDueAt = hubDue.Format(time.RFC3339) }); err != nil { t.Fatal(err) } if !m.AbandonStatus().HubPending { t.Fatal("fixture: expected a hub-phase deletion") } if _, err := m.ExtendAbandon(30); err == nil { t.Fatal("an extension during the hub phase was accepted — the hub would still delete on its own date") } if got := sett.GetOffboxTarget().AbandonAt; got != "" { t.Fatalf("the refused extension wrote a box-side date %q the page would show", got) } } type failingCancelAbandon struct{ fakeAbandon } func (f *failingCancelAbandon) Cancel(context.Context) error { return errors.New("hub unreachable") } func TestD1_StopReportsFailureWhenTheHubCancelFails(t *testing.T) { m, sett := newOffboxManager(t) pinTarget(t, sett) if err := sett.UpdateOffboxStatus(func(o *settings.OffboxTarget) { o.AbandonRepoPath = "/home/felhom-repo.orphaned-20260901" o.AbandonHubDueAt = time.Now().Add(5 * 24 * time.Hour).UTC().Format(time.RFC3339) }); err != nil { t.Fatal(err) } m.SetOffsiteAbandonClient(&failingCancelAbandon{}) if err := m.StopAbandon(); err == nil { t.Fatal("StopAbandon returned success while the hub's deletion is still pending") } if !m.AbandonStatus().Active { t.Fatal("fixture: the countdown must still be running after a failed cancel") } }