package backup import ( "errors" "os" "strings" "testing" "time" "gitea.dooplex.hu/admin/felhom-controller/internal/settings" "gitea.dooplex.hu/admin/felhom-controller/internal/util" ) // R-553 — the off-site failure classifier must keep telling a quota over-run apart after the sentence // is translated. It used to look for the Hungarian word „tárhelykeretet" inside the error; slice 2 // translates that sentence, and the customer would then be told the copy failed „ismeretlen okból" // — unknown cause — for the one failure with a clear, actionable cause. // // RED-PROOF (REPORT): put `case strings.Contains(s, "tárhelykeretet")` back and delete the // errors.Is arm → the translated row falls to OffsiteFailUnknown and this fails. func TestR553_OffsiteQuota_DecisionSurvivesWordingChange(t *testing.T) { cases := []struct { name string err error want OffsiteFailureClass }{ {"quota refusal, Hungarian as shipped", util.KindErrorf(ErrOffsiteQuota, "A távoli mentés túllépte a tárhelykeretet (%d/%d GB) — törölj régi mentéseket vagy kérj nagyobb keretet.", 51, 50), OffsiteFailQuota}, {"quota refusal, TRANSLATED", util.KindErrorf(ErrOffsiteQuota, "The remote backup is over its storage quota (%d/%d GB) — delete old backups or ask for more space.", 51, 50), OffsiteFailQuota}, {"quota refusal wrapped by a caller", util.KindErrorf(ErrOffsiteQuota, "over quota"), OffsiteFailQuota}, // Negative controls: output we do NOT write stays matched by its text, on purpose. {"restic: no repository", errors.New("Fatal: unable to open config file: Stat: file does not exist"), OffsiteFailNoRepo}, {"ssh: unreachable", errors.New("dial tcp 10.0.0.9:22: connect: connection refused"), OffsiteFailTransport}, {"nothing to copy", errors.New("off-box backup produced no snapshots: 2 app(s) toggled"), OffsiteFailNoUnits}, {"unknown stays unknown", errors.New("something else entirely"), OffsiteFailUnknown}, {"no error", nil, ""}, } for _, c := range cases { if got := ClassifyOffsiteFailure(c.err); got != c.want { t.Errorf("%s: ClassifyOffsiteFailure = %q, want %q", c.name, got, c.want) } } } // The consequence, not only the mechanism: the head line the customer reads on /backups/remote is the // quota one for a TRANSLATED quota error. (offsiteFailureMessage is the only caller of the classifier.) func TestR553_OffsiteQuota_HeadLineSurvivesWordingChange(t *testing.T) { tgt := &settings.OffboxTarget{Host: "nas.local", User: "felhom", RepoPath: "/srv/repo"} translated := util.KindErrorf(ErrOffsiteQuota, "The remote backup is over its storage quota (51/50 GB).") msg := offsiteFailureMessage(tgt, translated, 12*time.Second, "hu") if !strings.HasPrefix(msg, "A távoli mentés nem fért el a tárhelykereten belül") { t.Errorf("a translated quota failure is reported with the wrong cause line: %q", msg) } unknown := errors.New("The remote backup is over its storage quota (51/50 GB).") if m := offsiteFailureMessage(tgt, unknown, time.Second, "hu"); strings.HasPrefix(m, "A távoli mentés nem fért el") { t.Errorf("an error WITHOUT the kind must not be guessed into the quota class from its words: %q", m) } } // The producer keeps its Hungarian sentence byte-for-byte while carrying the kind. func TestR553_QuotaProducerKeepsItsWords(t *testing.T) { err := util.KindErrorf(ErrOffsiteQuota, "A távoli mentés túllépte a tárhelykeretet (%d/%d GB) — törölj régi mentéseket vagy kérj nagyobb keretet.", 51, 50) want := "A távoli mentés túllépte a tárhelykeretet (51/50 GB) — törölj régi mentéseket vagy kérj nagyobb keretet." if err.Error() != want { t.Errorf("message CHANGED:\n got %q\nwant %q", err.Error(), want) } if !errors.Is(err, ErrOffsiteQuota) { t.Error("the quota refusal carries no kind") } } // The zero-selection run must RECORD its kind beside the sentence, or the page falls back to reading // the words for ever. A real off-site run needs restic and an SSH target, so this is pinned at the // source — the defect it guards is a producer written the old way, and the display half is covered by // TestR553_StaleNoteDecisionSurvivesWordingChange in internal/web. func TestR553_OffboxRunRecordsTheKind(t *testing.T) { src, err := os.ReadFile("offbox.go") if err != nil { t.Fatal(err) } body := string(src) i := strings.Index(body, `warns = append(warns, "Sikeres — nincs mentésre jelölt alkalmazás")`) if i < 0 { t.Fatal("the zero-selection sentence is gone from the run — this test no longer reads what it thinks it reads") } if !strings.Contains(body[i:i+400], "warnKind = OffboxWarnNoAppsSelected") { t.Error("the zero-selection run records its sentence but not its KIND — the Távoli mentés page " + "is left reading Hungarian words, which localisation slice 2 will change (R-553)") } if !strings.Contains(body, "o.LastWarningKind = warnKind") { t.Error("the recorded kind is never persisted, so the page sees nothing after a restart") } for _, clear := range []string{`o.LastWarning = "" o.LastWarningKind = ""`} { if !strings.Contains(body, clear) { t.Error("a run that clears LastWarning must clear its kind too, or a stale kind outlives its text") } } }