R-101 + F-DIAG: the restore dialog names the last SUCCESSFUL copy (v0.182.0)
Tier2LastRun is the attempt clock and was rendered as 'Legutóbbi másolat' in the restore confirm dialog. New LastSuccess + SuccessTracked anchor; tier2Update makes the three rebuild sites safe by construction. F-DIAG: six distinct causes, target-aware redaction.
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// R-101 — THE CUSTOMER MUST NOT BE TOLD A FAILED BACKUP IS A COPY.
|
||||
//
|
||||
// `Tier2LastRun` is the ATTEMPT clock: `recordTier2Failure` writes it too. It was rendered as
|
||||
// „Legutóbbi másolat" in the restore confirm dialog — the exact moment a customer decides whether to
|
||||
// restore, guarding a restore that fills in MISSING files without touching existing ones. So a
|
||||
// customer whose Tier-2 had been failing was told a copy existed from last night, restored, and
|
||||
// silently received OLDER files while believing they were recent. Misinformation at a decision point.
|
||||
//
|
||||
// These tests render the PRODUCTION template tree and assert on the STRING THE CUSTOMER READS. A test
|
||||
// that asserted the template variable would prove nothing about the wording, which is the defect.
|
||||
|
||||
// tier2Row builds a configured Tier-2 row. Only the anchor fields vary between scenarios.
|
||||
func tier2Row(lastRun, lastSuccess, status string, tracked, stale bool) AppBackupRow {
|
||||
return AppBackupRow{
|
||||
StackName: "calibre-web", DisplayName: "Calibre-Web",
|
||||
Tier2Configured: true, Tier2Dest: "hdd_1", Tier2Schedule: "Naponta",
|
||||
Tier2LastRun: lastRun, Tier2LastStatus: status,
|
||||
Tier2LastSuccess: lastSuccess, Tier2SuccessTracked: tracked, Tier2StaleCopy: stale,
|
||||
Tier2StatusBadge: "Sikeres",
|
||||
}
|
||||
}
|
||||
|
||||
func renderTier2(t *testing.T, row AppBackupRow) string {
|
||||
t.Helper()
|
||||
return renderBackupPage(t, "backups_apps", baseBackupData([]AppBackupRow{row}))
|
||||
}
|
||||
|
||||
// SCENARIO A — the dialog names the last SUCCESSFUL copy, not last night's failed attempt.
|
||||
//
|
||||
// RED-PROOF: put `{{fmtTimeStr .Tier2LastRun}}` back into the data-confirm → this fails with
|
||||
// "the dialog names the FAILED attempt (2026-07-28 03:30) as the latest copy".
|
||||
func TestTier2Dialog_NamesTheLastSuccessfulCopy(t *testing.T) {
|
||||
// succeeded 3 days ago; last night's attempt failed
|
||||
html := renderTier2(t, tier2Row("2026-07-28T01:30:00Z", "2026-07-25T01:30:00Z", "error", true, true))
|
||||
|
||||
if !strings.Contains(html, "Legutóbbi sikeres másolat: 2026-07-25 03:30") {
|
||||
t.Fatalf("the dialog does not name the last SUCCESSFUL copy:\n%s", confirmOf(t, html))
|
||||
}
|
||||
if strings.Contains(html, "2026-07-28 03:30") {
|
||||
t.Errorf("the dialog names the FAILED attempt (2026-07-28 03:30) as the latest copy:\n%s", confirmOf(t, html))
|
||||
}
|
||||
// and it must not still say the bare „Legutóbbi másolat" of the old wording
|
||||
if strings.Contains(html, "Legutóbbi másolat:") {
|
||||
t.Errorf("the old bare `Legutóbbi másolat:` wording survives — that is the claim being fixed:\n%s", confirmOf(t, html))
|
||||
}
|
||||
}
|
||||
|
||||
// SCENARIO B — the failed attempt is DISCLOSED at the decision point. Showing only the old success and
|
||||
// hiding the failure is a quieter lie: the customer would not know the files may be older than usual.
|
||||
func TestTier2Dialog_DisclosesTheFailedAttempt(t *testing.T) {
|
||||
html := renderTier2(t, tier2Row("2026-07-28T01:30:00Z", "2026-07-25T01:30:00Z", "error", true, true))
|
||||
for _, want := range []string{
|
||||
"a legutóbbi mentési kísérlet nem sikerült",
|
||||
"régebbiek lehetnek",
|
||||
} {
|
||||
if !strings.Contains(html, want) {
|
||||
t.Errorf("the dialog hides that the newest attempt failed (missing %q):\n%s", want, confirmOf(t, html))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SCENARIO C — NEVER SUCCEEDED shows no copy at all. This is the worst case and the easiest to miss:
|
||||
// today a tier that has attempted and never succeeded still renders a timestamp, so the dialog
|
||||
// promises a copy that does not exist and the restore returns nothing.
|
||||
//
|
||||
// RED-PROOF: gate the restore form on `.Tier2LastRun` again instead of `.Tier2LastSuccess` → this
|
||||
// fails with "a tier that has NEVER succeeded still offers a restore".
|
||||
func TestTier2_NeverSucceededOffersNoCopy(t *testing.T) {
|
||||
html := renderTier2(t, tier2Row("2026-07-28T01:30:00Z", "", "error", true, false))
|
||||
|
||||
if !strings.Contains(html, "Még nincs sikeres másolat") {
|
||||
t.Fatalf("a never-succeeded tier does not say so:\n%s", html[:min(len(html), 400)])
|
||||
}
|
||||
if strings.Contains(html, "Fájlok visszaállítása") {
|
||||
t.Errorf("a tier that has NEVER succeeded still offers a restore — the dialog would promise a copy that does not exist")
|
||||
}
|
||||
if !strings.Contains(html, "amiből vissza lehetne állítani") {
|
||||
t.Errorf("the restore action does not say plainly that there is nothing to restore from")
|
||||
}
|
||||
// no timestamp may be presented as a copy
|
||||
if strings.Contains(html, "Legutóbbi sikeres másolat") || strings.Contains(html, "Utolsó sikeres:") {
|
||||
t.Errorf("a timestamp is presented despite no successful copy existing")
|
||||
}
|
||||
}
|
||||
|
||||
// SCENARIO D — a HEALTHY tier is visually unchanged: no warning, no caution, no tonal shift. If every
|
||||
// customer's dashboard grows a caution because the wording got defensive, the fix made things worse.
|
||||
//
|
||||
// RED-PROOF: make the stale-copy warning unconditional (drop `{{if .Tier2StaleCopy}}`) → this fails
|
||||
// with "a HEALTHY tier shows the failed-attempt caution".
|
||||
func TestTier2_HealthyTierIsUnchanged(t *testing.T) {
|
||||
html := renderTier2(t, tier2Row("2026-07-28T01:30:00Z", "2026-07-28T01:30:00Z", "ok", true, false))
|
||||
|
||||
for _, forbidden := range []string{
|
||||
"nem sikerült",
|
||||
"régebbiek lehetnek",
|
||||
"Még nincs sikeres másolat",
|
||||
} {
|
||||
if strings.Contains(html, forbidden) {
|
||||
t.Errorf("a HEALTHY tier shows the failed-attempt caution (%q):\n%s", forbidden, confirmOf(t, html))
|
||||
}
|
||||
}
|
||||
if !strings.Contains(html, "Legutóbbi sikeres másolat: 2026-07-28 03:30") {
|
||||
t.Errorf("a healthy tier lost its copy timestamp:\n%s", confirmOf(t, html))
|
||||
}
|
||||
if !strings.Contains(html, "Fájlok visszaállítása") {
|
||||
t.Error("a healthy tier lost its restore action")
|
||||
}
|
||||
}
|
||||
|
||||
// SCENARIO E — a LEGACY row (written before the anchor existed) renders EXACTLY as it did before.
|
||||
// Every one of the 7 Tier-2 rows on the fleet was in this state at deploy; rendering
|
||||
// „Még nincs sikeres másolat" for them would have told every customer at once that their backups do
|
||||
// not exist.
|
||||
func TestTier2_LegacyRowRendersAsBefore(t *testing.T) {
|
||||
html := renderTier2(t, tier2Row("2026-07-28T01:30:00Z", "", "ok", false /* not tracked */, false))
|
||||
|
||||
if strings.Contains(html, "Még nincs sikeres másolat") {
|
||||
t.Fatalf("a legacy row was rendered as never-succeeded — this would frighten every existing customer at once")
|
||||
}
|
||||
if !strings.Contains(html, "Utolsó: ") {
|
||||
t.Errorf("the legacy row lost today's rendering:\n%s", confirmOf(t, html))
|
||||
}
|
||||
if !strings.Contains(html, "Fájlok visszaállítása") {
|
||||
t.Error("the legacy row lost its restore action — behaviour must be unchanged for it")
|
||||
}
|
||||
}
|
||||
|
||||
// The dialog must be human-readable. A raw RFC3339 stamp ("2026-07-25T01:30:00Z") is a machine
|
||||
// timestamp in UTC, shown to a Hungarian customer deciding whether to restore.
|
||||
func TestTier2Dialog_TimestampIsHumanReadable(t *testing.T) {
|
||||
html := renderTier2(t, tier2Row("2026-07-28T01:30:00Z", "2026-07-25T01:30:00Z", "error", true, true))
|
||||
if strings.Contains(html, "2026-07-25T01:30:00Z") {
|
||||
t.Errorf("the dialog prints a raw RFC3339 UTC stamp:\n%s", confirmOf(t, html))
|
||||
}
|
||||
if !strings.Contains(html, "2026-07-25 03:30") { // Budapest local
|
||||
t.Errorf("the dialog does not render a Budapest-local date-time:\n%s", confirmOf(t, html))
|
||||
}
|
||||
}
|
||||
|
||||
// confirmOf extracts the data-confirm attribute for error messages, so a failure shows the string the
|
||||
// customer would actually read rather than a wall of HTML.
|
||||
func confirmOf(t *testing.T, html string) string {
|
||||
t.Helper()
|
||||
i := strings.Index(html, "data-confirm=\"")
|
||||
if i < 0 {
|
||||
return "(no data-confirm rendered)"
|
||||
}
|
||||
rest := html[i+len("data-confirm=\""):]
|
||||
j := strings.Index(rest, "\"")
|
||||
if j < 0 {
|
||||
return "(unterminated data-confirm)"
|
||||
}
|
||||
return rest[:j]
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
Reference in New Issue
Block a user