7c05b59708
gates / gates (push) Successful in 24s
179 Hungarian sentences were built deep inside a package with fmt.Errorf and printed by whoever caught them: too late to translate where they are shown, too early where they are made. Every one now carries its key across that gap. ZERO Hungarian error literals remain. util.MsgError does three things at once, each earned: - Error() is the Hungarian, byte for byte, so every un-converted printer is unchanged; - errors.Is answers for the kind AND for a wrapped cause (KindErrorf dropped the cause); - an error ARGUMENT renders recursively, so "formázás sikertelen: %w" translates whole. A foreign error — restic, docker, ssh, the stdlib — prints verbatim. It is not ours. 76 display sites go through errText, and TestNoErrErrorInPageOutput convicts any that do not. memoryVerdict returns an error rather than a sentence, so the deploy's 409 and the household's language come from one value; UpdateRefusal gained a Cause to carry it. Plurals, one rule, stated once: a key with .one/.other takes its COUNT first. Not a per-call-site flag — the producer somebody forgot would read "3 app is not running". The guard caught a real key collision (alert.deadapp.one) the day the rule landed. TWO DEFECTS FOUND IN MY OWN TOOLING, recorded rather than quietly fixed. The bulk converter silently dropped multi-line concatenations, damaging 7 producers — and the parity gate could not see it, because every surviving fragment WAS a real base literal while the CALL had lost text; two behaviour tests caught it. And the counting script was case-sensitive, so it said "0 left" while five remained. MinAgent: 0.131.0 (unchanged). No hub release needed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
180 lines
7.3 KiB
Go
180 lines
7.3 KiB
Go
package web
|
|
|
|
import (
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
|
)
|
|
|
|
// v0.124.0 IA split (§10 nav/state): each backups sub-page renders ONLY its mapped sections —
|
|
// a marker string per section, asserted present on its page and absent on every other page.
|
|
|
|
func splitTestData() map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"Page": "backups", "Title": "Biztonsági mentés",
|
|
"Backup": &backup.FullBackupStatus{
|
|
AppDataInfo: []backup.AppBackupInfo{{StackName: "calibre-web", DisplayName: "Calibre-Web"}},
|
|
},
|
|
"AppBackupRows": []AppBackupRow{
|
|
{StackName: "calibre-web", DisplayName: "Calibre-Web", OffboxEnabled: true, Tier3State: "active"},
|
|
},
|
|
"DBSectionState": "dumps",
|
|
"Offbox": &settings.OffboxTarget{
|
|
Enabled: true, Host: "nas.local", LastStatus: "ok", EscrowState: "escrowed",
|
|
},
|
|
"OffboxConfigured": true,
|
|
"OffboxApps": []OffboxAppRow{{Name: "calibre-web", DisplayName: "Calibre-Web", Enabled: true}},
|
|
"OffboxToggledCount": 1,
|
|
// R-237: the restore list is driven by the STORE. The happy fixture is "the app is in the
|
|
// repository AND installed here" — the pre-rebuild shape.
|
|
"OffsiteRestoreRows": []OffsiteRestoreRow{
|
|
{App: "calibre-web", DisplayName: "Calibre-Web", InStore: true, Installed: true, Enabled: true},
|
|
},
|
|
"OffsiteStoreState": string(offsiteStoreKnown),
|
|
"OffboxQuotaPct": 0,
|
|
"GuestBackup": map[string]interface{}{"Available": false, "Note": "n/a"},
|
|
}
|
|
}
|
|
|
|
func TestBackupsSplit_SectionsOnExactlyOnePage(t *testing.T) {
|
|
pages := map[string]string{
|
|
"backups": renderBackupPage(t, "backups", splitTestData()),
|
|
"backups_remote": renderBackupPage(t, "backups_remote", splitTestData()),
|
|
"backups_apps": renderBackupPage(t, "backups_apps", splitTestData()),
|
|
"backups_restore": renderBackupPage(t, "backups_restore", splitTestData()),
|
|
}
|
|
|
|
// marker → the ONE page it belongs to
|
|
markers := map[string]string{
|
|
"Tárhely áttekintés": "backups", // Section 0
|
|
"Rendszermentés (teljes mentés)": "backups", // whole-guest
|
|
">Adatmentés<": "backups", // stat cards (neutral branch)
|
|
`id="offbox-section"`: "backups_remote", // the offbox anchor target
|
|
"Távoli mentési cél beállítása": "backups_remote", // manual-target form
|
|
"Mely alkalmazások mentődnek": "backups_remote", // toggle list
|
|
"<h3>Ütemezés</h3>": "backups_apps", // schedule
|
|
"<h3>Adatbázisok</h3>": "backups_apps", // databases
|
|
"Alkalmazások mentési állapota": "backups_apps", // per-app rows
|
|
`id="restore-app"`: "backups_restore", // restore panel
|
|
"Visszaállítás a távoli tárolóból": "backups_restore", // the offsite restore list
|
|
`href="/backups/restore/app?name=`: "backups_restore", // R-48: the ONE entry per app
|
|
}
|
|
for marker, home := range markers {
|
|
for page, html := range pages {
|
|
has := strings.Contains(html, marker)
|
|
if page == home && !has {
|
|
t.Errorf("%s: missing its own section marker %q", page, marker)
|
|
}
|
|
if page != home && has {
|
|
t.Errorf("%s: renders %q which belongs to %s only", page, marker, home)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// The tier-3 row actions must deep-link cross-page to the offbox section (the pre-split
|
|
// #offbox-section anchors would be orphaned on the apps page).
|
|
func TestBackupsSplit_CrossPageAnchors(t *testing.T) {
|
|
data := splitTestData()
|
|
data["AppBackupRows"] = []AppBackupRow{
|
|
{StackName: "radarr", DisplayName: "Radarr", Tier3State: "off"},
|
|
}
|
|
html := renderBackupPage(t, "backups_apps", data)
|
|
if !strings.Contains(html, `href="/backups/remote#offbox-section"`) {
|
|
t.Error("tier-3 'Bekapcsolás' must link cross-page to /backups/remote#offbox-section")
|
|
}
|
|
if strings.Contains(html, `href="#offbox-section"`) {
|
|
t.Error("orphaned same-page #offbox-section anchor survives on the apps page")
|
|
}
|
|
}
|
|
|
|
// Felhom-offsite status card (v0.124.0, decision 2): three honest states, display-only —
|
|
// the card itself must never carry a form or button (no state change from the card, ever).
|
|
func TestBackupsRemote_OffsiteStatusCardStates(t *testing.T) {
|
|
cardOf := func(html string) string {
|
|
i := strings.Index(html, `id="felhom-offsite-card"`)
|
|
if i < 0 {
|
|
return ""
|
|
}
|
|
j := strings.Index(html[i:], "</div>")
|
|
return html[i : i+j]
|
|
}
|
|
|
|
// State 1: no target configured → the opt-in pitch (and no "Aktív" headline).
|
|
data := splitTestData()
|
|
data["OffboxConfigured"] = false
|
|
data["Offbox"] = nil
|
|
data["OffboxToggledCount"] = 0
|
|
html := renderBackupPage(t, "backups_remote", data)
|
|
if !strings.Contains(html, "Felhom offsite tárhely — igényelhető szolgáltatás. Titkosított, házon kívüli másolat a mentéseidről. Érdeklődj az üzemeltetőnél.") {
|
|
t.Error("state 1: the opt-in pitch is missing")
|
|
}
|
|
if strings.Contains(html, "Aktív — nincs kijelölt alkalmazás") {
|
|
t.Error("state 1 must not render the state-2 headline")
|
|
}
|
|
if card := cardOf(html); strings.Contains(card, "<form") || strings.Contains(card, "<button") {
|
|
t.Error("the status card must be display-only (no form/button)")
|
|
}
|
|
|
|
// State 2: applied, zero apps toggled → the honest active-but-empty headline.
|
|
data = splitTestData()
|
|
data["OffboxToggledCount"] = 0
|
|
data["OffboxApps"] = []OffboxAppRow{{Name: "calibre-web", DisplayName: "Calibre-Web", Enabled: false}}
|
|
html = renderBackupPage(t, "backups_remote", data)
|
|
if !strings.Contains(html, "Aktív — nincs kijelölt alkalmazás") {
|
|
t.Error("state 2: 'Aktív — nincs kijelölt alkalmazás' missing")
|
|
}
|
|
if strings.Contains(html, "igényelhető szolgáltatás") {
|
|
t.Error("state 2 must not render the state-1 pitch")
|
|
}
|
|
if card := cardOf(html); strings.Contains(card, "<form") || strings.Contains(card, "<button") {
|
|
t.Error("the status card must be display-only (no form/button)")
|
|
}
|
|
|
|
// State 3: applied + toggled → NO card; the regular status block carries the state.
|
|
html = renderBackupPage(t, "backups_remote", splitTestData())
|
|
if strings.Contains(html, `id="felhom-offsite-card"`) {
|
|
t.Error("state 3 must not render the status card (the status block is the state)")
|
|
}
|
|
if !strings.Contains(html, "Utolsó távoli mentés") {
|
|
t.Error("state 3: the regular status block is missing")
|
|
}
|
|
}
|
|
|
|
// Route → handler wiring: each sub-route sets its own .Page id, so the sidebar child renders
|
|
// active on the right page (backupMgr nil → the empty state renders, the nav still does).
|
|
func TestBackupsSplit_RoutesSetPageIDs(t *testing.T) {
|
|
s := testServer(t)
|
|
s.loadTemplates()
|
|
|
|
cases := []struct {
|
|
path string
|
|
active string
|
|
}{
|
|
{"/backups/remote", `href="/backups/remote" class="active"`},
|
|
{"/backups/apps", `href="/backups/apps" class="active"`},
|
|
{"/backups/restore", `href="/backups/restore" class="active"`},
|
|
}
|
|
for _, c := range cases {
|
|
rr := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", c.path, nil)
|
|
switch c.path {
|
|
case "/backups/remote":
|
|
s.backupsRemoteHandler(rr, req)
|
|
case "/backups/apps":
|
|
s.backupsAppsHandler(rr, req)
|
|
case "/backups/restore":
|
|
s.backupsRestoreHandler(rr, req)
|
|
}
|
|
if rr.Code != 200 {
|
|
t.Fatalf("%s: status %d", c.path, rr.Code)
|
|
}
|
|
if !strings.Contains(rr.Body.String(), c.active) {
|
|
t.Errorf("%s: sidebar child not active (want %q)", c.path, c.active)
|
|
}
|
|
}
|
|
}
|