package web import ( "fmt" "io" "io/fs" "net/http" "net/http/httptest" "net/url" "os" "path/filepath" "regexp" "strings" "testing" "time" "golang.org/x/crypto/bcrypt" "gitea.dooplex.hu/admin/felhom-controller/internal/i18n" "gitea.dooplex.hu/admin/felhom-controller/internal/stacks" ) // ── i18n (v0.247.0) — wiring, keys and context safety ───────────────────────────────────────────── var i18nScriptRe = regexp.MustCompile(`(?s)]*>(.*?)`) var i18nMarkerRe = regexp.MustCompile(`\{\{\s*T\s+"([A-Za-z0-9_.\-]+)"\s*\}\}`) func templateSources(t *testing.T) map[string]string { t.Helper() names, err := fs.Glob(templateFS, "templates/*.html") if err != nil || len(names) == 0 { t.Fatalf("no templates: %v", err) } out := map[string]string{} for _, n := range names { b, err := templateFS.ReadFile(n) if err != nil { t.Fatal(err) } out[n] = string(b) } return out } // Every key a template or the Go side names exists in Hungarian. (The loader refuses an undefined // template key too; this test names the key instead of a parse error, and covers the Go-side keys the // loader never sees.) func TestBundleKeysUsedExistInHungarian(t *testing.T) { b, err := i18n.Shared() if err != nil { t.Fatal(err) } used := 0 for name, src := range templateSources(t) { for _, m := range i18nMarkerRe.FindAllStringSubmatch(src, -1) { used++ if !b.Has(i18n.Default, m[1]) { t.Errorf("%s: marker key %q is not in hu.json", name, m[1]) } } } if used < 200 { t.Fatalf("only %d markers found — the scan is not reading the converted templates", used) } goKeys := []string{"page.title.launcher", "page.title.backups", "func.time.now", "func.time.minutes_ago", "func.time.hours_ago", "func.time.yesterday", "func.time.days_ago", "func.time.today_at", "func.time.tomorrow_at"} for _, st := range []string{"pending", "restoring", "done", "failed", "skipped"} { goKeys = append(goKeys, "func.restore_status."+st) } for _, st := range allContainerStates() { goKeys = append(goKeys, stateLabelKey(st)) } for _, k := range goKeys { if !b.Has(i18n.Default, k) { t.Errorf("Go-side key %q is not in hu.json", k) } } } func allContainerStates() []stacks.ContainerState { return []stacks.ContainerState{stacks.StateRunning, stacks.StateStarting, stacks.StateDeploying, stacks.StateUnhealthy, stacks.StateDegraded, stacks.StateStopped, stacks.StateExited, stacks.StateRestarting, stacks.StateNotDeployed, stacks.StatePaused, stacks.ContainerState("something-new")} } // Expansion is textual, so a translation lands in the source exactly where the Hungarian was. Inside a // JS string literal a bare quote or backslash ends or corrupts the string; inside a double-quoted // attribute a `"` ends the attribute. Neither is caught by html/template, which sees the expanded text as // the author's own source. So: a value may carry a quote character only where the Hungarian carries the // same one (the author already made it safe there). func TestI18nJSContextValuesAreSafe(t *testing.T) { b, err := i18n.Shared() if err != nil { t.Fatal(err) } checked := 0 check := func(where, key string, forbidden string) { hu, _, _ := b.Text(i18n.Default, key) for _, lang := range i18n.Supported { for _, form := range []string{key, key + ".one", key + ".other"} { v, fellBack, ok := b.Text(lang, form) if !ok || fellBack { continue } checked++ for _, c := range forbidden { if strings.ContainsRune(v, c) && !strings.ContainsRune(hu, c) { t.Errorf("%s: %s %q contains %q, which breaks its context", where, lang, form, c) } } } } } for name, src := range templateSources(t) { for _, sm := range i18nScriptRe.FindAllStringSubmatch(src, -1) { for _, m := range i18nMarkerRe.FindAllStringSubmatch(sm[1], -1) { check(name+"