- bouncer TestPhase10CookieGuard: cookie read without Bearer, Bearer wins, empty cookie, frontend audience and blacklisted jti rejected - boardwalk TestPhase10BoardwalkServing: HEAD, query strings, encoded traversal, index by name, nested prefix, MIME fallback, constructor errors - cabana TestPhase10Coverage: mounted unsafe routes vs the CSRF walk, option and filter edges, read-only labels, relation message defaults, bundle fallback locale, cookie refresh of an expired token in the refresh window - phrasebook override precedence, new locale, Bundle merge order, Forms shapes - surf prefix collision for deeper paths and the default /backend prefix - swagger2openapi TestUnionRewrite and converter branch tests - framework tests no longer name the application (acme fixtures instead)
334 lines
12 KiB
Go
334 lines
12 KiB
Go
package phrasebook
|
|
|
|
import (
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
"testing/fstest"
|
|
|
|
"git.golem15.com/golem15/summercms/backpack"
|
|
)
|
|
|
|
func TestPhase10Forms(t *testing.T) {
|
|
cat := NewCatalog()
|
|
if err := cat.Load("acme.demo", fstest.MapFS{
|
|
"lang/en/lang.yaml": {Data: []byte(`plain: Hello :name
|
|
plural:
|
|
one: ":count apple"
|
|
other: ":count apples"
|
|
pipe: ":count apple|:count apples"
|
|
exact: "{0} No apples|[1,*] :count apples"
|
|
`)},
|
|
"lang/pl/lang.yaml": {Data: []byte(`pipe: ":count jabłko|:count jabłka|:count jabłek|:count jabłka"
|
|
`)},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tr := NewTranslator(cat, Options{Locale: "en", Fallback: "en"})
|
|
|
|
check := func(locale, key string, want map[string]string) {
|
|
t.Helper()
|
|
got, ok := tr.Forms(locale, key)
|
|
if !ok {
|
|
t.Fatalf("%s %s: not convertible", locale, key)
|
|
}
|
|
if len(got) != len(want) {
|
|
t.Fatalf("%s %s = %v want %v", locale, key, got, want)
|
|
}
|
|
for cat, text := range want {
|
|
if got[cat] != text {
|
|
t.Fatalf("%s %s = %v want %v", locale, key, got, want)
|
|
}
|
|
}
|
|
}
|
|
check("en", "acme.demo::lang.plain", map[string]string{"other": "Hello :name"})
|
|
check("en", "acme.demo::lang.plural", map[string]string{"one": ":count apple", "other": ":count apples"})
|
|
check("en", "acme.demo::lang.pipe", map[string]string{"one": ":count apple", "other": ":count apples"})
|
|
check("pl", "acme.demo::lang.pipe", map[string]string{"one": ":count jabłko", "few": ":count jabłka", "many": ":count jabłek", "other": ":count jabłka"})
|
|
// pl falls back to en for a key it does not define.
|
|
check("pl", "acme.demo::lang.plain", map[string]string{"other": "Hello :name"})
|
|
|
|
if forms, ok := tr.Forms("en", "acme.demo::lang.exact"); ok {
|
|
t.Fatalf("exact/range pipe converted to %v", forms)
|
|
}
|
|
if forms, ok := tr.Forms("en", "acme.demo::lang.missing"); ok {
|
|
t.Fatalf("missing key converted to %v", forms)
|
|
}
|
|
|
|
got, ok := tr.Forms("en", "acme.demo::lang.plural")
|
|
if !ok {
|
|
t.Fatal("plural not convertible")
|
|
}
|
|
got["one"] = "mutated"
|
|
if again, _ := tr.Forms("en", "acme.demo::lang.plural"); again["one"] != ":count apple" {
|
|
t.Fatal("Forms returned the catalog's own map")
|
|
}
|
|
}
|
|
|
|
type overridePlugin struct {
|
|
id string
|
|
lang fs.FS
|
|
overrides fs.FS
|
|
}
|
|
|
|
func (p overridePlugin) ID() string { return p.id }
|
|
func (p overridePlugin) LangFS() fs.FS { return p.lang }
|
|
func (p overridePlugin) LangOverridesFS() fs.FS { return p.overrides }
|
|
|
|
func activateWith(t *testing.T, plugins ...overridePlugin) (*Translator, error) {
|
|
t.Helper()
|
|
app := backpack.New(nil)
|
|
if err := Activate(app, plugins); err != nil {
|
|
return nil, err
|
|
}
|
|
tr, ok := app.Lookup[*Translator]()
|
|
if !ok || tr == nil {
|
|
t.Fatal("translator was not published")
|
|
}
|
|
return tr, nil
|
|
}
|
|
|
|
func TestPhase10LangOverride(t *testing.T) {
|
|
demo := fstest.MapFS{"lang/en/lang.yaml": {Data: []byte("title: Demo\n")}}
|
|
tr, err := activateWith(t, overridePlugin{id: "acme.demo", lang: demo, overrides: fstest.MapFS{
|
|
"lang/pl/backend/lang.yaml": {Data: []byte("form:\n save: Zachowaj\n extra: Dodatkowy\n")},
|
|
"lang/de/backend/lang.yaml": {Data: []byte("form:\n save: Speichern\n")},
|
|
"lang/en/acme.demo/lang.yaml": {Data: []byte("title: Demo app\n")},
|
|
}})
|
|
if err != nil {
|
|
t.Fatalf("activate: %v", err)
|
|
}
|
|
for _, tc := range []struct{ locale, key, want string }{
|
|
{"pl", "backend::lang.form.save", "Zachowaj"},
|
|
{"pl", "backend::lang.form.extra", "Dodatkowy"},
|
|
{"pl", "backend::lang.form.cancel", "Anuluj"},
|
|
{"de", "backend::lang.form.save", "Speichern"},
|
|
{"en", "backend::lang.form.save", "Save"},
|
|
{"en", "acme.demo::lang.title", "Demo app"},
|
|
} {
|
|
if got := tr.GetIn(tc.locale, tc.key, nil); got != tc.want {
|
|
t.Fatalf("%s %s = %q want %q", tc.locale, tc.key, got, tc.want)
|
|
}
|
|
}
|
|
if bundle := tr.Bundle("de", "backend::lang."); bundle["backend::lang.form.save"]["other"] != "Speichern" || bundle["backend::lang.form.cancel"]["other"] != "Cancel" {
|
|
t.Fatalf("de bundle did not merge the fallback: save=%v cancel=%v", bundle["backend::lang.form.save"], bundle["backend::lang.form.cancel"])
|
|
}
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
overrides fstest.MapFS
|
|
want []string
|
|
}{
|
|
{"missing namespace segment", fstest.MapFS{"lang/pl/lang.yaml": {Data: []byte("a: b\n")}}, []string{"acme.demo", "malformed override path"}},
|
|
{"not yaml", fstest.MapFS{"lang/pl/backend/lang.txt": {Data: []byte("a: b\n")}}, []string{"acme.demo", "malformed override path"}},
|
|
{"too deep", fstest.MapFS{"lang/pl/backend/sub/lang.yaml": {Data: []byte("a: b\n")}}, []string{"acme.demo", "malformed override path"}},
|
|
{"unknown namespace", fstest.MapFS{"lang/pl/acme.missing/lang.yaml": {Data: []byte("a: b\n")}}, []string{"acme.demo", "acme.missing"}},
|
|
{"unconvertible backend key", fstest.MapFS{"lang/en/backend/lang.yaml": {Data: []byte("list:\n results: \"{0} No results|[1,*] :count results\"\n")}}, []string{"backend::lang.list.results", "CLDR"}},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, err := activateWith(t, overridePlugin{id: "acme.demo", lang: demo, overrides: tc.overrides})
|
|
if err == nil {
|
|
t.Fatal("activation accepted a bad override")
|
|
}
|
|
for _, want := range tc.want {
|
|
if !strings.Contains(err.Error(), want) {
|
|
t.Fatalf("err=%v missing %q", err, want)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
var spaKey = regexp.MustCompile(`backend::lang\.[A-Za-z0-9_.]*[A-Za-z0-9_]`)
|
|
|
|
// TestPhase10SPAKeysResolve walks the admin SPA sources and resolves every
|
|
// backend::lang key literal in pl and en, each without falling back to the
|
|
// other locale.
|
|
func TestPhase10SPAKeysResolve(t *testing.T) {
|
|
_, file, _, ok := runtime.Caller(0)
|
|
if !ok {
|
|
t.Fatal("caller")
|
|
}
|
|
root := filepath.Join(filepath.Dir(file), "..", "admin", "src")
|
|
keys := map[string]bool{}
|
|
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if d.IsDir() || !(strings.HasSuffix(path, ".ts") || strings.HasSuffix(path, ".vue")) {
|
|
return nil
|
|
}
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, key := range spaKey.FindAllString(string(raw), -1) {
|
|
keys[key] = true
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(keys) < 10 {
|
|
t.Fatalf("found only %d backend::lang keys under %s", len(keys), root)
|
|
}
|
|
sorted := make([]string, 0, len(keys))
|
|
for key := range keys {
|
|
sorted = append(sorted, key)
|
|
}
|
|
sort.Strings(sorted)
|
|
cat := NewCatalog()
|
|
if err := cat.Load("backend", backendLang()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, locale := range []string{"pl", "en"} {
|
|
tr := NewTranslator(cat, Options{Locale: locale, Fallback: locale})
|
|
for _, key := range sorted {
|
|
forms, ok := tr.Forms(locale, key)
|
|
if !ok || forms["other"] == "" {
|
|
t.Errorf("%s: %s does not resolve", locale, key)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestPhase10LangPrecedence pins the D-20 layering: an override beats the
|
|
// plugin's own strings and the framework's backend strings, a later plugin's
|
|
// override beats an earlier one, and an override can add a whole locale.
|
|
func TestPhase10LangPrecedence(t *testing.T) {
|
|
demoLang := fstest.MapFS{
|
|
"lang/en/lang.yaml": {Data: []byte("title: Demo\nsubtitle: Widgets\n")},
|
|
"lang/pl/lang.yaml": {Data: []byte("title: Demo PL\n")},
|
|
}
|
|
first := overridePlugin{id: "acme.demo", lang: demoLang, overrides: fstest.MapFS{
|
|
"lang/pl/backend/lang.yaml": {Data: []byte("form:\n save: Pierwszy\n cancel: Pierwsze anuluj\n")},
|
|
"lang/pl/acme.demo/lang.yaml": {Data: []byte("title: Demo nadpisane\n")},
|
|
}}
|
|
second := overridePlugin{id: "acme.site", overrides: fstest.MapFS{
|
|
"lang/pl/backend/lang.yaml": {Data: []byte("form:\n save: Drugi\n")},
|
|
"lang/fr/backend/lang.yaml": {Data: []byte("form:\n save: Enregistrer\n")},
|
|
"lang/fr/acme.demo/lang.yaml": {Data: []byte("title: Démo\n")},
|
|
}}
|
|
tr, err := activateWith(t, first, second)
|
|
if err != nil {
|
|
t.Fatalf("activate: %v", err)
|
|
}
|
|
for _, tc := range []struct{ locale, key, want string }{
|
|
// Framework string untouched by any override.
|
|
{"pl", "backend::lang.form.delete", "Usuń"},
|
|
// Override beats framework; the later plugin's override wins.
|
|
{"pl", "backend::lang.form.save", "Drugi"},
|
|
{"pl", "backend::lang.form.cancel", "Pierwsze anuluj"},
|
|
// Override beats the plugin's own string; the plugin beats its fallback.
|
|
{"pl", "acme.demo::lang.title", "Demo nadpisane"},
|
|
{"en", "acme.demo::lang.title", "Demo"},
|
|
{"pl", "acme.demo::lang.subtitle", "Widgets"},
|
|
// A locale added only through overrides.
|
|
{"fr", "backend::lang.form.save", "Enregistrer"},
|
|
{"fr", "acme.demo::lang.title", "Démo"},
|
|
{"fr", "backend::lang.form.delete", "Delete"},
|
|
} {
|
|
if got := tr.GetIn(tc.locale, tc.key, nil); got != tc.want {
|
|
t.Fatalf("%s %s = %q want %q", tc.locale, tc.key, got, tc.want)
|
|
}
|
|
}
|
|
if got := tr.Resolved("fr", "backend::lang."); got != "fr" {
|
|
t.Fatalf("Resolved(fr) = %q", got)
|
|
}
|
|
if got := tr.Resolved("de", "backend::lang."); got != "en" {
|
|
t.Fatalf("Resolved(de) = %q, want the en fallback", got)
|
|
}
|
|
}
|
|
|
|
// TestPhase10BundleMerge proves Bundle layers the requested locale over its
|
|
// region parent and the fallback, filters by prefix and skips keys that do
|
|
// not convert to CLDR forms.
|
|
func TestPhase10BundleMerge(t *testing.T) {
|
|
cat := NewCatalog()
|
|
if err := cat.Load("acme.demo", fstest.MapFS{
|
|
"lang/en/lang.yaml": {Data: []byte("a: A en\nb: B en\nc: C en\nexact: \"{0} none|[1,*] :count\"\n")},
|
|
"lang/pt/lang.yaml": {Data: []byte("a: A pt\nb: B pt\n")},
|
|
"lang/pt-BR/lang.yaml": {Data: []byte("a: A pt-BR\n")},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := cat.Load("acme.other", fstest.MapFS{"lang/en/lang.yaml": {Data: []byte("a: other\n")}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tr := NewTranslator(cat, Options{Locale: "en", Fallback: "en"})
|
|
bundle := tr.Bundle("pt-BR", "acme.demo::")
|
|
want := map[string]string{
|
|
"acme.demo::lang.a": "A pt-BR",
|
|
"acme.demo::lang.b": "B pt",
|
|
"acme.demo::lang.c": "C en",
|
|
}
|
|
if len(bundle) != len(want) {
|
|
t.Fatalf("bundle = %v", bundle)
|
|
}
|
|
for key, text := range want {
|
|
if bundle[key]["other"] != text {
|
|
t.Fatalf("%s = %v want %q", key, bundle[key], text)
|
|
}
|
|
}
|
|
if _, ok := bundle["acme.demo::lang.exact"]; ok {
|
|
t.Fatal("an exact/range pipe entered the bundle")
|
|
}
|
|
if got := tr.Resolved("pt-BR", "acme.demo::"); got != "pt-BR" {
|
|
t.Fatalf("Resolved = %q", got)
|
|
}
|
|
var nilTr *Translator
|
|
if b := nilTr.Bundle("en", ""); len(b) != 0 {
|
|
t.Fatalf("nil translator bundle = %v", b)
|
|
}
|
|
if _, ok := nilTr.Forms("en", "x"); ok {
|
|
t.Fatal("nil translator produced forms")
|
|
}
|
|
}
|
|
|
|
// TestPhase10FormsShapes converts every entry shape the catalog stores: plain
|
|
// text, a YAML plural map, a category pipe, and pipes that cannot convert
|
|
// (exact, range, or without an other form).
|
|
func TestPhase10FormsShapes(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
e entry
|
|
want map[string]string
|
|
ok bool
|
|
}{
|
|
{"plain", entry{text: "Hi"}, map[string]string{"other": "Hi"}, true},
|
|
{"empty plain", entry{}, map[string]string{"other": ""}, true},
|
|
{"plural map", entry{plurals: map[string]string{"one": "1", "other": "n"}}, map[string]string{"one": "1", "other": "n"}, true},
|
|
{"category pipe", entry{pipes: []pipePart{{cat: "one", text: "1"}, {cat: "other", text: "n"}}}, map[string]string{"one": "1", "other": "n"}, true},
|
|
{"pipe without other", entry{pipes: []pipePart{{cat: "one", text: "1"}}}, nil, false},
|
|
{"pipe with an uncategorised part", entry{pipes: []pipePart{{text: "x"}, {cat: "other", text: "n"}}}, nil, false},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got, ok := entryForms(tc.e)
|
|
if ok != tc.ok {
|
|
t.Fatalf("ok=%v want %v (%v)", ok, tc.ok, got)
|
|
}
|
|
if len(got) != len(tc.want) {
|
|
t.Fatalf("forms=%v want %v", got, tc.want)
|
|
}
|
|
for cat, text := range tc.want {
|
|
if got[cat] != text {
|
|
t.Fatalf("forms=%v want %v", got, tc.want)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
one, two := 0.0, 1.0
|
|
if _, ok := entryForms(entry{pipes: []pipePart{{exact: &one, text: "none"}, {cat: "other", text: "n"}}}); ok {
|
|
t.Fatal("exact pipe converted")
|
|
}
|
|
if _, ok := entryForms(entry{pipes: []pipePart{{lo: &two, text: "some"}, {cat: "other", text: "n"}}}); ok {
|
|
t.Fatal("range pipe converted")
|
|
}
|
|
}
|