// SPDX-Licence-Identifier: EUPL-1.2 package html import ( "testing" i18n "dappco.re/go/core/i18n" "github.com/stretchr/testify/require" ) type localeTranslator struct { language string } func (t *localeTranslator) T(key string, args ...any) string { if key == "prompt.yes" && t.language == "fr" { return "o" } if key == "prompt.yes" && t.language == "en" { return "y" } return key } func (t *localeTranslator) SetLanguage(language string) error { t.language = language return nil } func (t *localeTranslator) Clone() Translator { if t == nil { return (*localeTranslator)(nil) } clone := *t return &clone } type resettingTranslator struct { language string } func (t *resettingTranslator) T(key string, args ...any) string { if key == "prompt.yes" && t.language == "fr" { return "o" } if key == "prompt.yes" && t.language == "en" { return "y" } return key } func (t *resettingTranslator) SetLanguage(language string) error { t.language = language return nil } func (t *resettingTranslator) Clone() Translator { if t == nil { return (*resettingTranslator)(nil) } return &resettingTranslator{} } func TestContext_NewContextWithService_AppliesLocale(t *testing.T) { svc := &localeTranslator{} ctx := NewContextWithService(svc, "fr-FR") if svc.language != "fr" { t.Fatalf("NewContextWithService should apply locale to translator, got %q", svc.language) } if got := Text("prompt.yes").Render(ctx); got != "o" { t.Fatalf("NewContextWithService locale translation = %q, want %q", got, "o") } } func TestContext_NewContext_AppliesLocaleToDefaultService(t *testing.T) { ctx := NewContext("fr-FR") if got := Text("prompt.yes").Render(ctx); got != "o" { t.Fatalf("NewContext(locale) translation = %q, want %q", got, "o") } } func TestContext_NewContextWithService_UsesLocale(t *testing.T) { svc := &localeTranslator{} ctx := NewContextWithService(svc, "en-GB") if svc.language != "en" { t.Fatalf("NewContextWithService should apply locale to translator, got %q", svc.language) } if got := Text("prompt.yes").Render(ctx); got != "y" { t.Fatalf("NewContextWithService translation = %q, want %q", got, "y") } } func TestContext_SetLocale_ReappliesToTranslator(t *testing.T) { svc := &localeTranslator{} ctx := NewContextWithService(svc, "en-GB") ctx.SetLocale("fr-FR") if ctx.Locale != "fr-FR" { t.Fatalf("SetLocale should update context locale, got %q", ctx.Locale) } if svc.language != "fr" { t.Fatalf("SetLocale should reapply locale to translator, got %q", svc.language) } if got := Text("prompt.yes").Render(ctx); got != "o" { t.Fatalf("SetLocale translation = %q, want %q", got, "o") } } func TestContext_SetService_ReappliesCurrentLocale(t *testing.T) { ctx := NewContext("fr-FR") svc := &localeTranslator{} if got := ctx.SetService(svc); got != ctx { t.Fatalf("SetService should return the same context") } if ctx.service != svc { t.Fatalf("SetService should replace the active translator") } if svc.language != "fr" { t.Fatalf("SetService should apply the existing locale to the new translator, got %q", svc.language) } if got := Text("prompt.yes").Render(ctx); got != "o" { t.Fatalf("SetService translation = %q, want %q", got, "o") } } func TestContext_SetEntitlements_UpdatesFeatureGate(t *testing.T) { ctx := NewContext() if got := ctx.SetEntitlements(func(feature string) bool { return feature == "premium" }); got != ctx { t.Fatalf("SetEntitlements should return the same context") } if ctx.Entitlements == nil { t.Fatal("SetEntitlements should store the callback") } if !ctx.Entitlements("premium") { t.Fatal("SetEntitlements should preserve the requested callback") } } func TestContext_SetIdentity_UpdatesIdentity(t *testing.T) { ctx := NewContext() if got := ctx.SetIdentity("user-123"); got != ctx { t.Fatalf("SetIdentity should return the same context") } if ctx.Identity != "user-123" { t.Fatalf("SetIdentity should update context identity, got %q", ctx.Identity) } if ctx.Data == nil { t.Fatal("SetIdentity should preserve initialised Data map") } } func TestContext_SetData_StoresValue(t *testing.T) { ctx := NewContext() if got := ctx.SetData("theme", "dark"); got != ctx { t.Fatalf("SetData should return the same context") } if got := ctx.Data["theme"]; got != "dark" { t.Fatalf("SetData should store the requested value, got %v", got) } } func TestContext_SetData_InitialisesNilMap(t *testing.T) { ctx := &Context{} ctx.SetData("theme", "light") if ctx.Data == nil { t.Fatal("SetData should initialise the Data map on demand") } if got := ctx.Data["theme"]; got != "light" { t.Fatalf("SetData should store the requested value in a nil map context, got %v", got) } } func TestContext_CloneCopiesDataWithoutSharingMap(t *testing.T) { svc := &localeTranslator{} ctx := NewContextWithService(svc, "en-GB") ctx.SetIdentity("user-123") ctx.SetData("theme", "dark") clone := ctx.Clone() if clone == ctx { t.Fatal("Clone should return a distinct context instance") } if clone.service == ctx.service { t.Fatal("Clone should duplicate cloneable translators") } if clone.Locale != ctx.Locale { t.Fatalf("Clone should preserve locale, got %q want %q", clone.Locale, ctx.Locale) } if clone.Identity != ctx.Identity { t.Fatalf("Clone should preserve identity, got %q want %q", clone.Identity, ctx.Identity) } clone.SetData("theme", "light") if got := ctx.Data["theme"]; got != "dark" { t.Fatalf("Clone should not share Data map with original, got %v", got) } } func TestContext_CloneDoesNotShareDefaultTranslator(t *testing.T) { ctx := NewContext("en-GB") clone := ctx.Clone() if clone == nil { t.Fatal("Clone should return a context") } clone.SetLocale("fr-FR") if got := Text("prompt.yes").Render(ctx); got != "y" { t.Fatalf("Clone should not mutate original default translator, got %q", got) } if got := Text("prompt.yes").Render(clone); got != "o" { t.Fatalf("Clone should keep its own default translator, got %q", got) } } func TestContext_CloneClonesMutableTranslator(t *testing.T) { svc := &localeTranslator{} ctx := NewContextWithService(svc, "en-GB") clone := ctx.Clone() if clone == nil { t.Fatal("Clone should return a context") } if clone.service == ctx.service { t.Fatal("Clone should isolate cloneable translators") } clone.SetLocale("fr-FR") if got := svc.language; got != "en" { t.Fatalf("Clone should not mutate the original translator, got %q", got) } if got := Text("prompt.yes").Render(ctx); got != "y" { t.Fatalf("Clone should leave original context translation unchanged, got %q", got) } if got := Text("prompt.yes").Render(clone); got != "o" { t.Fatalf("Clone should reapply locale to the cloned translator, got %q", got) } } func TestContext_CloneReappliesLocaleAfterTranslatorClone(t *testing.T) { svc := &resettingTranslator{} ctx := NewContextWithService(svc, "fr-FR") clone := ctx.Clone() if clone == nil { t.Fatal("Clone should return a context") } if clone.service == ctx.service { t.Fatal("Clone should duplicate cloneable translators") } if got := Text("prompt.yes").Render(clone); got != "o" { t.Fatalf("Clone should reapply locale after cloning the translator, got %q", got) } if got := clone.Locale; got != "fr-FR" { t.Fatalf("Clone should preserve locale, got %q", got) } } func TestContext_WithDataReturnsClonedContext(t *testing.T) { ctx := NewContext() ctx.SetData("theme", "dark") next := ctx.WithData("locale", "fr-FR") if next == ctx { t.Fatal("WithData should return a cloned context") } if got := ctx.Data["locale"]; got != nil { t.Fatalf("WithData should not mutate the original context, got %v", got) } if got := next.Data["locale"]; got != "fr-FR" { t.Fatalf("WithData should set the requested value on the clone, got %v", got) } if got := next.Data["theme"]; got != "dark" { t.Fatalf("WithData should preserve existing data on the clone, got %v", got) } } func TestContext_WithIdentityReturnsClonedContext(t *testing.T) { ctx := NewContext() ctx.SetIdentity("user-001") ctx.SetData("theme", "dark") next := ctx.WithIdentity("user-123") if next == ctx { t.Fatal("WithIdentity should return a cloned context") } if got := ctx.Identity; got != "user-001" { t.Fatalf("WithIdentity should not mutate the original context, got %q", got) } if got := next.Identity; got != "user-123" { t.Fatalf("WithIdentity should set the requested identity on the clone, got %q", got) } if got := next.Data["theme"]; got != "dark" { t.Fatalf("WithIdentity should preserve existing data on the clone, got %v", got) } } func TestContext_WithLocaleReturnsClonedContext(t *testing.T) { svc := &localeTranslator{} ctx := NewContextWithService(svc, "en-GB") ctx.SetIdentity("user-001") ctx.SetData("theme", "dark") next := ctx.WithLocale("fr-FR") if next == ctx { t.Fatal("WithLocale should return a cloned context") } if got := ctx.Locale; got != "en-GB" { t.Fatalf("WithLocale should not mutate the original context locale, got %q", got) } if got := next.Locale; got != "fr-FR" { t.Fatalf("WithLocale should set the requested locale on the clone, got %q", got) } if got := next.service; got == ctx.service { t.Fatal("WithLocale should duplicate cloneable translators on the clone") } if svc.language != "en" { t.Fatalf("WithLocale should not mutate the original translator, got %q", svc.language) } if got := Text("prompt.yes").Render(next); got != "o" { t.Fatalf("WithLocale should reapply locale to the cloned service, got %q", got) } if got := next.Data["theme"]; got != "dark" { t.Fatalf("WithLocale should preserve existing data on the clone, got %v", got) } } func TestContext_WithServiceReturnsClonedContext(t *testing.T) { ctx := NewContext("fr-FR") ctx.SetIdentity("user-001") ctx.SetData("theme", "dark") svc := &localeTranslator{} next := ctx.WithService(svc) if next == ctx { t.Fatal("WithService should return a cloned context") } if got := ctx.service; got == svc { t.Fatal("WithService should not mutate the original context service") } if got := next.service; got != svc { t.Fatalf("WithService should set the requested service on the clone, got %v", got) } if svc.language != "fr" { t.Fatalf("WithService should apply the existing locale to the new translator, got %q", svc.language) } if got := next.Data["theme"]; got != "dark" { t.Fatalf("WithService should preserve existing data on the clone, got %v", got) } if got := next.Locale; got != "fr-FR" { t.Fatalf("WithService should preserve the locale on the clone, got %q", got) } } func TestContext_WithEntitlementsReturnsClonedContext(t *testing.T) { ctx := NewContext() ctx.SetIdentity("user-001") ctx.SetData("theme", "dark") ctx.SetEntitlements(func(feature string) bool { return feature == "basic" }) next := ctx.WithEntitlements(func(feature string) bool { return feature == "premium" }) if next == ctx { t.Fatal("WithEntitlements should return a cloned context") } if ctx.Entitlements == nil { t.Fatal("WithEntitlements should not clear the original callback") } if !ctx.Entitlements("basic") { t.Fatal("WithEntitlements should preserve the original callback") } if next.Entitlements == nil { t.Fatal("WithEntitlements should store the new callback on the clone") } if next.Entitlements("basic") { t.Fatal("WithEntitlements should replace the callback on the clone") } if !next.Entitlements("premium") { t.Fatal("WithEntitlements should preserve the new callback") } if got := next.Data["theme"]; got != "dark" { t.Fatalf("WithEntitlements should preserve existing data on the clone, got %v", got) } if got := next.Identity; got != "user-001" { t.Fatalf("WithEntitlements should preserve identity on the clone, got %q", got) } } func TestContext_Setters_NilReceiver(t *testing.T) { var ctx *Context if got := ctx.SetIdentity("user-123"); got != nil { t.Fatalf("nil Context.SetIdentity should return nil, got %v", got) } if got := ctx.SetData("theme", "dark"); got != nil { t.Fatalf("nil Context.SetData should return nil, got %v", got) } if got := ctx.SetLocale("en-GB"); got != nil { t.Fatalf("nil Context.SetLocale should return nil, got %v", got) } if got := ctx.SetService(&localeTranslator{}); got != nil { t.Fatalf("nil Context.SetService should return nil, got %v", got) } if got := ctx.SetEntitlements(func(string) bool { return true }); got != nil { t.Fatalf("nil Context.SetEntitlements should return nil, got %v", got) } if got := ctx.WithLocale("en-GB"); got != nil { t.Fatalf("nil Context.WithLocale should return nil, got %v", got) } if got := ctx.WithService(&localeTranslator{}); got != nil { t.Fatalf("nil Context.WithService should return nil, got %v", got) } if got := ctx.WithEntitlements(func(string) bool { return true }); got != nil { t.Fatalf("nil Context.WithEntitlements should return nil, got %v", got) } } func TestText_RenderFallsBackToDefaultTranslator(t *testing.T) { svc, _ := i18n.New() i18n.SetDefault(svc) require.NoError(t, svc.SetLanguage("fr")) ctx := &Context{} if got := Text("prompt.yes").Render(ctx); got != "o" { t.Fatalf("Text() fallback translation = %q, want %q", got, "o") } }