package web import ( "io" "log" "strings" "testing" "gitea.dooplex.hu/admin/felhom-controller/internal/config" "gitea.dooplex.hu/admin/felhom-controller/internal/i18n" "gitea.dooplex.hu/admin/felhom-controller/internal/monitor" "gitea.dooplex.hu/admin/felhom-controller/internal/settings" ) // R-516 item 8 — a disconnected drive showed TWO banners on every page: its own // (alert.storage.disconnected, in the household's language) and the health check's Hungarian // „Meghajtó leválasztva" warning for the same drive. The dedicated banner is the one kept. // // COMPANION RED-PROOF: drop the WarnKindStorageDisconnected `continue` in AlertManager.Refresh — the // first half fails with two banners. func TestR516_DisconnectedDriveHasOneBanner(t *testing.T) { const warn = "Meghajtó leválasztva: Külső (/mnt/usb)" hr := &monitor.HealthReport{Status: "warn", Warnings: []string{warn, "CPU usage high: 91%"}, WarningKinds: []string{monitor.WarnKindStorageDisconnected, ""}} cfg := &config.Config{} cfg.Hub.Enabled = false sp := []settings.StoragePath{{Path: "/mnt/usb", Label: "Külső", Disconnected: true}} count := func(alerts []Alert) (dedicated, health, cpu int) { for _, a := range alerts { switch { case strings.HasPrefix(a.ID, "storage-disconnected-"): dedicated++ case a.Message == warn: health++ case a.Message == "CPU usage high: 91%": cpu++ } } return } am := NewAlertManager(log.New(io.Discard, "", 0)) am.Refresh(hr, cfg, nil, false, "", sp) d, h, c := count(am.GetAlerts("hu")) if d != 1 || h != 0 { t.Errorf("a disconnected drive must have exactly one banner: dedicated=%d health-warning=%d", d, h) } if c != 1 { t.Errorf("positive control: another health warning must still reach the banners (got %d)", c) } // Without the storage list there is no dedicated banner, so the health warning is the only one // that can say it — it must stay. am2 := NewAlertManager(log.New(io.Discard, "", 0)) am2.Refresh(hr, cfg, nil, false, "") if d, h, _ := count(am2.GetAlerts("hu")); d != 0 || h != 1 { t.Errorf("without the storage list the health warning is the only banner: dedicated=%d health-warning=%d", d, h) } } // R-516 item 7 — the storage page printed the disconnect time as the raw stored RFC3339 UTC string // („Leválasztva: 2026-09-14T19:58:02Z"). It now goes through fmtTimeStr, the page's local-time format, // in both languages. // // COMPANION RED-PROOF: put `{{.DisconnectedAt}}` back in storage.levalasztva_2 (hu.json) — the hu half // fails with the raw string. func TestR516_DisconnectTimeIsLocalNotRawUTC(t *testing.T) { const raw = "2026-09-14T19:58:02Z" for _, tc := range []struct{ lang, prefix string }{{"hu", "Leválasztva: "}, {"en", "Disconnected: "}} { d := i18nLayoutData("storage", "Tárhely") paths := i18nStoragePaths() paths[0]["DisconnectedAt"] = raw d["StoragePaths"] = paths h := renderBackupPageLang(t, tc.lang, "storage", d) if strings.Contains(h, raw) { t.Errorf("%s: the raw UTC timestamp reached the page", tc.lang) } if want := tc.prefix + fmtRFC3339Local(raw); !strings.Contains(h, want) { t.Errorf("%s: the page must carry %q", tc.lang, want) } } } // R-516 item 10 — a health warning with a dashboard sentence shows it in the household's language; // a warning without one shows its wire text verbatim (the fail-open fallback). func TestR516_HealthBannersFollowTheHousehold(t *testing.T) { hr := &monitor.HealthReport{Status: "fail", Issues: []string{"SSD disk usage critical: 95%"}, IssueMsgs: []monitor.MsgRef{{Key: "health.system_disk_critical", Args: []interface{}{95.0}}}, Warnings: []string{"SSD disk usage high: 90%", "Docker: something"}, WarningKinds: []string{"", ""}, WarningMsgs: []monitor.MsgRef{{Key: "health.system_disk_high", Args: []interface{}{90.0}}, {}}} cfg := &config.Config{} cfg.Hub.Enabled = false am := NewAlertManager(log.New(io.Discard, "", 0)) am.Refresh(hr, cfg, nil, false, "") got := map[string][]string{} for _, lang := range []string{"hu", "en"} { for _, a := range am.GetAlerts(lang) { if strings.HasPrefix(a.ID, "health-") { got[lang] = append(got[lang], a.Message) } } } want := map[string][]string{ "hu": {"A rendszer tárhelye majdnem megtelt: 95% foglalt", "A rendszer tárhelye fogy: 90% foglalt", "Docker: something"}, "en": {"System storage is almost full: 95% used", "System storage is filling up: 90% used", "Docker: something"}, } for lang, w := range want { if strings.Join(got[lang], "|") != strings.Join(w, "|") { t.Errorf("%s banners:\n got %q\n want %q", lang, got[lang], w) } } } // R-516 item 12 — the last formal („ön") forms in the dashboard copy are in the product's te-form. // The i18n gate's ratchet (HU_FORMAL_CEILING = 0) keeps new ones out; this pins the reworded bytes. func TestR516_FormalFormsAreGone(t *testing.T) { b, err := i18n.Shared() if err != nil { t.Fatal(err) } want := map[string]string{ "alert.deadapp.group": "%d telepített alkalmazás nem fut — nézd meg a rendszermonitort", "flash.login.password_changed": "Jelszó sikeresen módosítva. Jelentkezz be az új jelszóval.", "launcher.biztosan_kikapcsolja_a_megosztast_a": "Biztosan kikapcsolod a megosztást? A link azonnal érvénytelenné válik.", "launcher.olvassa_be_telefonnal_a_gyors": "Olvasd be telefonnal a gyors megnyitáshoz.", "deploy.a_kontener_leallt_ellenorizze_a": "A konténer leállt. Ellenőrizd a naplókat.", "debug.kattintson_a_gombra_a_telemetria": "Kattints a gombra a telemetria futtatásához.", "app_import.a_feltoltes_megszakadt_ellenorizze_a": "A feltöltés megszakadt — ellenőrizd a kapcsolatot, majd próbáld újra.", } for key, w := range want { got, fellBack, ok := b.Text("hu", key) if !ok || fellBack || got != w { t.Errorf("%s: %q, want %q", key, got, w) } } // Negative control over EVERY Hungarian value: the formal stems this row removed do not return. // ASCII-folded, so an accent difference cannot hide one. fold := strings.NewReplacer("á", "a", "é", "e", "í", "i", "ó", "o", "ö", "o", "ő", "o", "ú", "u", "ü", "u", "ű", "u") stems := []string{"kerjuk, jelentkezzen", "nezze meg", "olvassa be", "kattintson a", "ellenorizze", "ujratelepiti az", "vegye fel a kapcsolatot", "huzza ide"} for _, key := range b.Keys("hu") { v, _, _ := b.Text("hu", key) lv := fold.Replace(strings.ToLower(v)) for _, st := range stems { if strings.Contains(lv, st) { t.Errorf("%s still carries the formal %q: %q", key, st, v) } } } // Positive control: the fold must see a te-form stem, or the loop above judged nothing. if lv := fold.Replace(strings.ToLower(want["deploy.a_kontener_leallt_ellenorizze_a"])); !strings.Contains(lv, "ellenorizd") { t.Fatal("the ASCII fold does not work — the negative control proves nothing") } }