R-100: record the offsite last-SUCCESS anchor (v0.181.0)
LastRun records an attempt, not a result. New OffboxTarget.LastSuccess, set only on the success branch via the pure offboxAnchorAfterRun rule, carried to the hub as last_success. Closes two silent-wipe sites (settings save, hub re-apply).
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
||||
)
|
||||
|
||||
// R-100 — an ordinary settings save must not erase the staleness anchor.
|
||||
//
|
||||
// offboxConfigHandler builds a FRESH OffboxTarget from the form and copies runtime status across from
|
||||
// the previous one, field by field. Every field omitted from that copy is silently zeroed. LastSuccess
|
||||
// is the hub's staleness anchor, so omitting it would reset an established tier to "never succeeded"
|
||||
// every time the customer edited the host or the path — and the reset would be invisible until the
|
||||
// hub's verdict changed days later.
|
||||
//
|
||||
// This is the "seam built but never wired" shape: the field exists, the writer sets it, and a routine
|
||||
// unrelated code path throws it away.
|
||||
//
|
||||
// RED-PROOF: remove `tgt.LastSuccess = prev.LastSuccess` from offboxConfigHandler → this fails with
|
||||
// "a settings save erased LastSuccess".
|
||||
func TestOffboxEdit_PreservesLastSuccess(t *testing.T) {
|
||||
s, sett, _ := newOffboxWebServer(t)
|
||||
const anchor = "2026-07-26T02:15:00Z"
|
||||
if err := sett.SetOffboxTarget(&settings.OffboxTarget{
|
||||
Enabled: true, Host: "nas.local", User: "felhom", RepoPath: "/srv/repo",
|
||||
EscrowState: "escrowed",
|
||||
LastRun: "2026-07-27T02:15:00Z", // a later FAILED attempt
|
||||
LastStatus: "error",
|
||||
LastSuccess: anchor,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// The customer edits the repo path — nothing to do with run history.
|
||||
form := url.Values{
|
||||
"enabled": {"on"}, "host": {"nas.local"}, "user": {"felhom"},
|
||||
"repo_path": {"/srv/repo-moved"}, "port": {"22"},
|
||||
// First-setup guard: the handler requires both secrets when none are on disk yet.
|
||||
"ssh_key": {"-----BEGIN OPENSSH PRIVATE KEY-----\nZmFrZQ==\n-----END OPENSSH PRIVATE KEY-----\n"},
|
||||
"known_hosts": {"nas.local ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFAKEFAKEFAKEFAKEFAKEFAKEFAKEFAKEFAKEFAKEFAK"},
|
||||
}
|
||||
req := httptest.NewRequest("POST", "/backup/offbox/config", strings.NewReader(form.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
rec := httptest.NewRecorder()
|
||||
s.offboxConfigHandler(rec, req)
|
||||
// NOTE: the redirect carries a flash about escrow STAGING failing — there is no agent in this
|
||||
// fixture. The target itself is persisted before that step, which is what this test is about, and
|
||||
// the repo_path assertion below is the real precondition.
|
||||
|
||||
got := sett.GetOffboxTarget()
|
||||
if got == nil {
|
||||
t.Fatal("target vanished after the save")
|
||||
}
|
||||
if got.RepoPath != "/srv/repo-moved" {
|
||||
t.Fatalf("precondition: the edit did not apply (repo_path = %q)", got.RepoPath)
|
||||
}
|
||||
if got.LastSuccess != anchor {
|
||||
t.Errorf("a settings save erased LastSuccess (got %q, want %q) — the tier would read as never-succeeded",
|
||||
got.LastSuccess, anchor)
|
||||
}
|
||||
// and the attempt clock is preserved too, as it already was
|
||||
if got.LastRun != "2026-07-27T02:15:00Z" {
|
||||
t.Errorf("the save also lost LastRun (got %q)", got.LastRun)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user