package web
import (
"fmt"
"io/fs"
"net/http"
"net/http/httptest"
"net/url"
"regexp"
"strings"
"testing"
"time"
"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+"