e4774e6a06
- item 7: the storage page shows the disconnect time in local time (fmtTimeStr), not the raw RFC3339 UTC. - item 8: a disconnected drive no longer has two banners - the health check's warning for it is dropped when the dedicated alert.storage.disconnected banner was built (it stays on the wire). - item 9: alert.deadapp.group says "nezd meg" (te-form); formal ceiling 14 -> 13. - item 10: the disk/memory/CPU/temperature health banners show the dashboard's own sentence (health.* keys, hu + en) via HealthReport.WarningMsgs/IssueMsgs; the wire text is unchanged. checkResources split out of RunHealthCheck as the test seam. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
220 lines
9.0 KiB
Go
220 lines
9.0 KiB
Go
package web
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/i18n"
|
|
)
|
|
|
|
// Localisation slice 2 (R-557), scenario S1 — a flash follows the language of the request that READS
|
|
// it, and a link minted by an older controller still reads correctly.
|
|
//
|
|
// Why this is not obvious: a flash does not travel in the response that produced it. The handler
|
|
// redirects, the browser asks for the destination, and a SECOND request renders the line. Before
|
|
// v0.252.0 the sentence itself rode in the query string, so it was written in the language of the
|
|
// handler that redirected — which is the household's language only by luck. The value is now a bundle
|
|
// key; the reader renders it.
|
|
//
|
|
// The compatibility half is the load-bearing one. A customer's open tab, a bookmark, the browser's
|
|
// back-forward cache and a mail client can all replay a URL minted by 0.251.0, whose `flash` is
|
|
// Hungarian prose. Such a value is shown verbatim, never as a raw key and never dropped.
|
|
|
|
func flashServer(t *testing.T) *Server {
|
|
t.Helper()
|
|
b, err := i18n.Load()
|
|
if err != nil {
|
|
t.Fatalf("bundle: %v", err)
|
|
}
|
|
return &Server{i18n: b}
|
|
}
|
|
|
|
func flashRequest(t *testing.T, rawQuery string) *http.Request {
|
|
t.Helper()
|
|
u, err := url.Parse("/launcher?" + rawQuery)
|
|
if err != nil {
|
|
t.Fatalf("query %q: %v", rawQuery, err)
|
|
}
|
|
return httptest.NewRequest(http.MethodGet, u.String(), nil)
|
|
}
|
|
|
|
func TestFlashKeyRoundTrip(t *testing.T) {
|
|
s := flashServer(t)
|
|
|
|
// The writer's side: launcherShareRedirect builds this.
|
|
q := flashQuery("flash", "flash.share.enabled")
|
|
if got := flashRequest(t, q).URL.Query().Get("flash"); got != "flash.share.enabled" {
|
|
t.Fatalf("the key did not survive the URL: %q", got)
|
|
}
|
|
|
|
cases := []struct {
|
|
name, query, lang, want string
|
|
}{
|
|
{
|
|
name: "hungarian reads the Hungarian sentence, byte for byte",
|
|
query: q, lang: "hu",
|
|
want: "A megosztás bekapcsolva.",
|
|
},
|
|
{
|
|
name: "english reads English",
|
|
query: q, lang: "en",
|
|
want: "Sharing is on.",
|
|
},
|
|
{
|
|
// A URL minted by v0.251.0 or earlier. `+` is a space in a query string.
|
|
name: "a legacy link carries prose and is shown verbatim",
|
|
query: "flash=Sikeres+ment%C3%A9s", lang: "en",
|
|
want: "Sikeres mentés",
|
|
},
|
|
{
|
|
name: "an unknown key-shaped value is not invented into a sentence",
|
|
query: "flash=flash.share.no_such_key", lang: "en",
|
|
want: "flash.share.no_such_key",
|
|
},
|
|
{
|
|
// The escaping case. flashText returns the value; html/template escapes it at render, as
|
|
// it always did. What must NOT happen is the value being treated as a key or dropped.
|
|
name: "a hand-typed script tag survives as text, to be escaped at render",
|
|
query: "flash=%3Cscript%3Ealert(1)%3C%2Fscript%3E", lang: "en",
|
|
want: "<script>alert(1)</script>",
|
|
},
|
|
{
|
|
name: "no flash at all is empty, not a key",
|
|
query: "", lang: "en",
|
|
want: "",
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
r := flashRequest(t, tc.query+"&lang="+tc.lang)
|
|
if got := s.flashFrom(r, "flash"); got != tc.want {
|
|
t.Errorf("flash\n got %q\n want %q", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestFlashKeyCarriesParameters — a flash whose sentence names something (an app, a drive) carries
|
|
// that name as a separate `fa` parameter, so English may put it somewhere else in the sentence.
|
|
func TestFlashKeyCarriesParameters(t *testing.T) {
|
|
s := flashServer(t)
|
|
// The parameter-carrying flash in the bundle today. Asserted through the real bundle rather than a
|
|
// fixture, and self-arming: if no flash message takes a parameter any more the test says so out
|
|
// loud instead of passing vacuously.
|
|
const key = "flash.offbox.config_invalid"
|
|
if !s.i18n.Has(i18n.Default, key) {
|
|
t.Fatalf("%s left the bundle — this test no longer covers a parameter-carrying flash", key)
|
|
}
|
|
if !strings.Contains(s.i18n.Msg(i18n.Default, key), "%s") {
|
|
t.Fatalf("%s no longer takes a parameter — point this test at one that does", key)
|
|
}
|
|
r := flashRequest(t, flashQuery("flash_error", key, "a port nem szam")+"&lang=hu")
|
|
got := s.flashFrom(r, "flash_error")
|
|
if want := "Érvénytelen beállítás: a port nem szam"; got != want {
|
|
t.Errorf("parameter did not reach the message\n got %q\n want %q", got, want)
|
|
}
|
|
// And the same key, same parameter, in English: the parameter survives a different word order.
|
|
r = flashRequest(t, flashQuery("flash_error", key, "a port nem szam")+"&lang=en")
|
|
if got := s.flashFrom(r, "flash_error"); !strings.Contains(got, "a port nem szam") {
|
|
t.Errorf("the parameter was lost in English: %q", got)
|
|
}
|
|
}
|
|
|
|
// TestFlashOnAServerWithNoBundleField — a Server built without going through loadTemplates still
|
|
// renders SENTENCES, never raw keys. s.bundle() falls back to the embedded i18n.Shared() for exactly
|
|
// this: a page that shows „flash.share.enabled" to a household is the failure mode §4 of the
|
|
// localisation design forbids, and it was reachable from any construction path but the real one.
|
|
//
|
|
// RED-PROOF (REPORT): make s.bundle() return s.i18n unchanged → this test reads back the key.
|
|
func TestFlashOnAServerWithNoBundleField(t *testing.T) {
|
|
s := &Server{} // i18n field never set
|
|
if got := s.flashFrom(flashRequest(t, "flash=flash.share.enabled"), "flash"); got != "A megosztás bekapcsolva." {
|
|
t.Errorf("a Server with no bundle field showed %q instead of the sentence", got)
|
|
}
|
|
// And a legacy sentence still passes through untouched.
|
|
if got := s.flashFrom(flashRequest(t, "flash=Sikeres+ment%C3%A9s"), "flash"); got != "Sikeres mentés" {
|
|
t.Errorf("got %q, want the legacy text verbatim", got)
|
|
}
|
|
}
|
|
|
|
// flashSentence resolves the flash a redirect Location carries, in Hungarian.
|
|
//
|
|
// It exists because v0.252.0 moved the SENTENCE out of the URL and put a bundle KEY there instead.
|
|
// A test that searched the Location header for „elindult" or „letét" was never asserting the URL —
|
|
// it was asserting what the customer is told — and that is still exactly checkable, one lookup
|
|
// later. Tests assert through this rather than against the key, so a key renamed with its message
|
|
// intact stays green and a message REWORDED still fails, which is the right way round.
|
|
func flashSentence(t *testing.T, location string) string {
|
|
t.Helper()
|
|
u, err := url.Parse(location)
|
|
if err != nil {
|
|
t.Fatalf("Location %q: %v", location, err)
|
|
}
|
|
s := flashServer(t)
|
|
r := httptest.NewRequest(http.MethodGet, location, nil)
|
|
for _, p := range []string{"flash", "flash_error"} {
|
|
if v := u.Query().Get(p); v != "" {
|
|
return s.flashText(r, v)
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// TestPluralEnglish — localisation slice 2 release B (R-557), scenario S4.
|
|
//
|
|
// English needs two forms where the noun or the verb changes with the count; Hungarian does not
|
|
// inflect after a numeral and therefore has ONE form, which is the form it already had. So this test
|
|
// asserts two different things about the same message: that English changes with the count, and that
|
|
// Hungarian does NOT — the second is the parity half, and it is the one that would fail silently.
|
|
//
|
|
// It goes through the real producer (buildDeadAppAlerts → GetAlerts), not through the bundle, so it
|
|
// also pins that the alert path carries the count as its first parameter, which is what the plural
|
|
// rule in i18n.Bundle.form depends on.
|
|
func TestPluralEnglish(t *testing.T) {
|
|
dead := func(n int) []DeadApp {
|
|
out := make([]DeadApp, 0, n)
|
|
for i := 0; i < n; i++ {
|
|
out = append(out, DeadApp{Name: "app" + strconv.Itoa(i), DisplayName: "App " + strconv.Itoa(i)})
|
|
}
|
|
return out
|
|
}
|
|
// Above deadAppGroupThreshold the banner collapses to ONE counted sentence — that is the message
|
|
// with a count in it. 4 and 7 are both "other"; there is no "one" case for the grouped banner by
|
|
// construction, so the singular is checked on the bundle directly below.
|
|
am := NewAlertManager(nil)
|
|
am.SetDeadAppAlerts(dead(4))
|
|
hu4 := am.GetAlerts("hu")[0].Message
|
|
en4 := am.GetAlerts("en")[0].Message
|
|
am.SetDeadAppAlerts(dead(7))
|
|
hu7 := am.GetAlerts("hu")[0].Message
|
|
en7 := am.GetAlerts("en")[0].Message
|
|
|
|
if hu4 != "4 telepített alkalmazás nem fut — nézd meg a rendszermonitort" { // R-516 item 9: te-form
|
|
t.Errorf("the Hungarian sentence moved: %q", hu4)
|
|
}
|
|
if hu7 != "7 telepített alkalmazás nem fut — nézd meg a rendszermonitort" { // R-516 item 9: te-form
|
|
t.Errorf("the Hungarian sentence moved: %q", hu7)
|
|
}
|
|
if !strings.Contains(en4, "4 installed apps are not running") {
|
|
t.Errorf("English plural: %q", en4)
|
|
}
|
|
if !strings.Contains(en7, "7 installed apps are not running") {
|
|
t.Errorf("English plural: %q", en7)
|
|
}
|
|
|
|
// The singular, straight off the bundle: same key, count 1.
|
|
b := flashServer(t).i18n
|
|
if got := b.Msgf("en", "alert.deadapp.group", 1); !strings.Contains(got, "1 installed app is not running") {
|
|
t.Errorf("English singular: %q", got)
|
|
}
|
|
// And Hungarian at 1 is the SAME single form — no .one, no change.
|
|
if got := b.Msgf("hu", "alert.deadapp.group", 1); got != "1 telepített alkalmazás nem fut — nézd meg a rendszermonitort" {
|
|
t.Errorf("Hungarian must have one form at every count: %q", got)
|
|
}
|
|
}
|