package web import ( "strings" "testing" "gitea.dooplex.hu/admin/felhom-controller/internal/appbackup" "gitea.dooplex.hu/admin/felhom-controller/internal/i18n" "gitea.dooplex.hu/admin/felhom-controller/internal/stacks" ) // appInfoWithCards overlays the R-75 folder card onto the shared app_info fixture. It reuses // appInfoData (lifecycle_test.go) deliberately — that helper passes a *Metadata for a reason // documented there (value vs pointer receivers at render time); do not fork it. func appInfoWithCards(st stacks.Stack, cards []DataPathCard) map[string]interface{} { d := appInfoData(st) if len(cards) > 0 { d["DataPathCards"] = cards } return d } // SEAM-WIRING RULE: a conditional affordance ships with a render test PER BRANCH of its gate. // Branch 1 — cards present: the block renders, with the label, the deep link and the copy. func TestAppInfo_DataPathCardRenders(t *testing.T) { st := stacks.Stack{Name: "paperless-ngx", Deployed: true, State: stacks.StateRunning, Meta: stacks.Metadata{Slug: "paperless-ngx", DisplayName: "Paperless-ngx"}} cards := []DataPathCard{{ Label: "Beolvasandó dokumentumok", Link: importFolderLink("demo-felhom.eu", "paperless"), Consequence: consequenceFor(appbackup.ClassExcluded, stacks.RoleImport, bundleMsg(t, "hu")), IsImport: true, FreeSpace: "44.8 GB szabad", }} html := renderBackupPage(t, "app_info", appInfoWithCards(st, cards)) if !strings.Contains(html, "Hova tegyem a fájlokat?") { t.Error("the folder card heading must render") } if !strings.Contains(html, "Beolvasandó dokumentumok") { t.Error("the catalog label must render") } // The deep link must reach the page INTACT — this is the assertion the brief asks for, and it is // what would have caught the v0.150.0 class of bug (a key rendered where another belonged). if !strings.Contains(html, "https://files.demo-felhom.eu/files/Beolvas%C3%A1s/paperless") { t.Errorf("the FileBrowser deep link must render intact; body:\n%s", excerpt(html, "datapath")) } if !strings.Contains(html, "nem készül róla biztonsági mentés") { t.Error("an excluded drop-zone must say it is unbacked") } if !strings.Contains(html, "44.8 GB szabad") { t.Error("the system-drive free space must render on an import row") } // The copy must not promise a single click — a cold deep link goes through the FileBrowser login. if !strings.Contains(html, "be kell jelentkezned") { t.Error("the card must warn that a FileBrowser sign-in may be needed") } } // Branch 2 — no cards: nothing renders, and in particular no empty heading. func TestAppInfo_NoDataPathCardsRendersNothing(t *testing.T) { st := stacks.Stack{Name: "docmost", Deployed: true, State: stacks.StateRunning, Meta: stacks.Metadata{Slug: "docmost", DisplayName: "Docmost"}} html := renderBackupPage(t, "app_info", appInfoWithCards(st, nil)) if strings.Contains(html, "Hova tegyem a fájlokat?") { t.Error("an app with no data_paths must not render the folder card") } if strings.Contains(html, "datapath-row") { t.Error("no folder rows must render") } } // bundleMsg is the production lookup the handler uses, bound to one language. func bundleMsg(t *testing.T, lang string) func(string) string { t.Helper() b, err := i18n.Shared() if err != nil { t.Fatalf("bundle: %v", err) } return func(key string) string { return b.Msg(lang, key) } } // Fork-4: the consequence line is CLASS-driven, so the UI can never promise a backup the engines do // not make. `excluded` is dropped at every tier — it must say so. // // R-590 (v0.258.0): the sentence now follows the household's language, so the CONSEQUENCE is // asserted in BOTH — the mechanism (does it read the bundle?) is not the property that matters. It // is a promise about the customer's files: an English household that cannot read „nem készül róla // biztonsági mentés" may leave originals in a drop-zone the backup filter discards at every tier. func TestConsequenceIsClassDriven(t *testing.T) { cases := []struct { lang string tempAndUnbacked []string // an excluded import folder must say BOTH of these promise string // a kept folder must say this denial string // and must NOT say this }{ {"hu", []string{"törli innen", "nem készül róla biztonsági mentés"}, "Biztonsági mentés készül", "nem készül"}, {"en", []string{"deletes them from here", "is not backed up"}, "It is backed up", "not backed up"}, } for _, c := range cases { msg := bundleMsg(t, c.lang) imp := consequenceFor(appbackup.ClassExcluded, stacks.RoleImport, msg) for _, want := range c.tempAndUnbacked { if !strings.Contains(imp, want) { t.Errorf("[%s] an excluded import folder must say %q: %q", c.lang, want, imp) } } for _, cls := range []appbackup.BindClass{appbackup.ClassMandatory, appbackup.ClassOptional} { lib := consequenceFor(cls, stacks.RoleLibrary, msg) if !strings.Contains(lib, c.promise) { t.Errorf("[%s] class %q must promise a backup: %q", c.lang, cls, lib) } if strings.Contains(lib, c.denial) { t.Errorf("[%s] class %q must NOT say unbacked: %q", c.lang, cls, lib) } } // An unclassified bind says NOTHING rather than guessing — an unverified backup promise // about a customer's files is worse than no sentence. In every language. if got := consequenceFor("", stacks.RoleLibrary, msg); got != "" { t.Errorf("[%s] an unclassified path must produce no promise, got %q", c.lang, got) } } // And the two languages must not say the same thing — that would mean the bundle fell back and // the English household is reading Hungarian, which is the defect this row was opened for. hu := consequenceFor(appbackup.ClassMandatory, stacks.RoleLibrary, bundleMsg(t, "hu")) en := consequenceFor(appbackup.ClassMandatory, stacks.RoleLibrary, bundleMsg(t, "en")) if hu == en { t.Errorf("the English consequence line fell back to Hungarian: %q", en) } } // A card is only built for a DEPLOYED app (Fork-2: deployed-only in the UI). func TestBuildDataPathCards_UndeployedYieldsNothing(t *testing.T) { s := testServer(t) st := &stacks.Stack{Name: "paperless-ngx", Deployed: false, Meta: stacks.Metadata{Slug: "paperless-ngx", DataPaths: []stacks.DataPath{ {Path: "paperless", Root: appbackup.RootImport, Role: stacks.RoleImport, Label: "x"}, }}} if got := s.buildDataPathCards(st, "hu"); got != nil { t.Errorf("an undeployed app must get no folder card, got %v", got) } } func excerpt(html, needle string) string { i := strings.Index(html, needle) if i < 0 { return "(needle not found)" } end := i + 400 if end > len(html) { end = len(html) } return html[i:end] }