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) } }