515 lines
16 KiB
Go
515 lines
16 KiB
Go
package langpack
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"telesrv/internal/domain"
|
|
"telesrv/internal/store"
|
|
"telesrv/internal/store/memory"
|
|
)
|
|
|
|
func TestServiceNormalizesWebAliasAndRejectsUnknownCatalogEntries(t *testing.T) {
|
|
ctx := context.Background()
|
|
packs := memory.NewLangPackStore()
|
|
svc := NewService(packs)
|
|
for _, pack := range []domain.LangPack{
|
|
{
|
|
LangPack: "webk",
|
|
LangCode: "en",
|
|
Version: 7,
|
|
Strings: []domain.LangPackString{{Key: "lng_settings_language", Value: "Language"}},
|
|
},
|
|
{
|
|
LangPack: "webk",
|
|
LangCode: "zh-hans",
|
|
Version: 9,
|
|
Strings: []domain.LangPackString{{Key: "lng_settings_language", Value: "语言"}},
|
|
},
|
|
} {
|
|
if err := packs.UpsertPack(ctx, pack); err != nil {
|
|
t.Fatalf("seed %s/%s: %v", pack.LangPack, pack.LangCode, err)
|
|
}
|
|
}
|
|
|
|
languages, err := svc.ListLanguages(ctx, " WEB ")
|
|
if err != nil {
|
|
t.Fatalf("list web languages: %v", err)
|
|
}
|
|
if len(languages) != 2 || findLanguage(languages, "zh-hans") == nil {
|
|
t.Fatalf("web languages = %+v, want canonical webk catalog", languages)
|
|
}
|
|
|
|
full, err := svc.GetLangPack(ctx, "web", "ZH_HANS")
|
|
if err != nil {
|
|
t.Fatalf("get web langpack: %v", err)
|
|
}
|
|
if full.LangPack != "webk" || full.LangCode != "zh-hans" || full.Version != 9 || stringValue(full.Strings, "lng_settings_language") != "语言" {
|
|
t.Fatalf("web langpack = %+v, want canonical webk/zh-hans", full)
|
|
}
|
|
|
|
diff, err := svc.GetDifference(ctx, "web", "zh-hans", 1)
|
|
if err != nil {
|
|
t.Fatalf("get web difference: %v", err)
|
|
}
|
|
if diff.LangPack != "webk" || diff.FromVersion != 1 || len(diff.Strings) != 1 {
|
|
t.Fatalf("web difference = %+v, want canonical webk delta", diff)
|
|
}
|
|
|
|
selected, err := svc.GetStrings(ctx, "web", "zh-hans", []string{"lng_settings_language"})
|
|
if err != nil {
|
|
t.Fatalf("get web strings: %v", err)
|
|
}
|
|
if selected.LangPack != "webk" || stringValue(selected.Strings, "lng_settings_language") != "语言" {
|
|
t.Fatalf("web strings = %+v, want selected webk string", selected)
|
|
}
|
|
|
|
invalidPackCalls := map[string]func() error{
|
|
"list": func() error {
|
|
_, err := svc.ListLanguages(ctx, "web-invalid")
|
|
return err
|
|
},
|
|
"full": func() error {
|
|
_, err := svc.GetLangPack(ctx, "web-invalid", "en")
|
|
return err
|
|
},
|
|
"difference": func() error {
|
|
_, err := svc.GetDifference(ctx, "web-invalid", "en", 1)
|
|
return err
|
|
},
|
|
"strings": func() error {
|
|
_, err := svc.GetStrings(ctx, "web-invalid", "en", []string{"key"})
|
|
return err
|
|
},
|
|
}
|
|
for name, call := range invalidPackCalls {
|
|
if err := call(); !errors.Is(err, domain.ErrLangPackInvalid) {
|
|
t.Fatalf("%s invalid pack error = %v, want ErrLangPackInvalid", name, err)
|
|
}
|
|
}
|
|
|
|
unsupportedCodeCalls := map[string]func() error{
|
|
"full": func() error {
|
|
_, err := svc.GetLangPack(ctx, "web", "fr")
|
|
return err
|
|
},
|
|
"difference": func() error {
|
|
_, err := svc.GetDifference(ctx, "web", "fr", 1)
|
|
return err
|
|
},
|
|
"strings": func() error {
|
|
_, err := svc.GetStrings(ctx, "web", "fr", []string{"key"})
|
|
return err
|
|
},
|
|
}
|
|
for name, call := range unsupportedCodeCalls {
|
|
if err := call(); !errors.Is(err, domain.ErrLangCodeNotSupported) {
|
|
t.Fatalf("%s unsupported code error = %v, want ErrLangCodeNotSupported", name, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestServiceNormalizesWebARawLangCode(t *testing.T) {
|
|
ctx := context.Background()
|
|
packs := memory.NewLangPackStore()
|
|
svc := NewService(packs)
|
|
seed := domain.LangPack{
|
|
LangPack: "android",
|
|
LangCode: "en",
|
|
Version: 7,
|
|
Strings: []domain.LangPackString{
|
|
{Key: "LogOutTitle", Value: "Log Out"},
|
|
{Key: "NewMessageTitle", Value: "New Message"},
|
|
},
|
|
}
|
|
if err := packs.UpsertPack(ctx, seed); err != nil {
|
|
t.Fatalf("seed langpack: %v", err)
|
|
}
|
|
webASeed := domain.LangPack{
|
|
LangPack: "weba",
|
|
LangCode: "en",
|
|
Version: 12,
|
|
Strings: []domain.LangPackString{
|
|
{Key: "AccDescrPollVoteDown", Value: "Go to next unread poll vote"},
|
|
{Key: "NewMessageTitle", Value: "New Message from WebA"},
|
|
},
|
|
}
|
|
if err := packs.UpsertPack(ctx, webASeed); err != nil {
|
|
t.Fatalf("seed weba langpack: %v", err)
|
|
}
|
|
|
|
pack, err := svc.GetLangPack(ctx, "android", "EN-raw")
|
|
if err != nil {
|
|
t.Fatalf("get langpack: %v", err)
|
|
}
|
|
if pack.LangCode != "en" || pack.Version != webASeed.Version || len(pack.Strings) != len(seed.Strings)+1 {
|
|
t.Fatalf("pack = %+v, want normalized en pack", pack)
|
|
}
|
|
if got := stringValue(pack.Strings, "AccDescrPollVoteDown"); got != "Go to next unread poll vote" {
|
|
t.Fatalf("AccDescrPollVoteDown = %q, want WebA fallback", got)
|
|
}
|
|
if got := stringValue(pack.Strings, "NewMessageTitle"); got != "New Message" {
|
|
t.Fatalf("NewMessageTitle = %q, want source pack to keep precedence", got)
|
|
}
|
|
|
|
selected, err := svc.GetStrings(ctx, "android", "en-raw", []string{"LogOutTitle", "AccDescrPollVoteDown"})
|
|
if err != nil {
|
|
t.Fatalf("get strings: %v", err)
|
|
}
|
|
if got := stringValue(selected.Strings, "LogOutTitle"); got != "Log Out" {
|
|
t.Fatalf("LogOutTitle = %q, want source pack value", got)
|
|
}
|
|
if got := stringValue(selected.Strings, "AccDescrPollVoteDown"); got != "Go to next unread poll vote" {
|
|
t.Fatalf("AccDescrPollVoteDown = %q, want WebA fallback", got)
|
|
}
|
|
|
|
notModified, err := svc.GetDifference(ctx, "android", "en-raw", seed.Version)
|
|
if err != nil {
|
|
t.Fatalf("get difference: %v", err)
|
|
}
|
|
if notModified.LangCode != "en" || len(notModified.Strings) != 0 {
|
|
t.Fatalf("difference = %+v, want normalized not-modified source pack", notModified)
|
|
}
|
|
}
|
|
|
|
func TestServiceRebrandsEveryLanguagePackProjection(t *testing.T) {
|
|
ctx := context.Background()
|
|
base := memory.NewLangPackStore()
|
|
seed := domain.LangPack{
|
|
LangPack: "weba",
|
|
LangCode: "en",
|
|
Version: 7,
|
|
Strings: []domain.LangPackString{
|
|
{Key: "AppName", Value: "Telegram", Pluralized: true, ZeroValue: "No Telegram accounts", OneValue: "One Telegram account", TwoValue: "Two Telegram accounts", FewValue: "Few Telegram accounts", ManyValue: "Many Telegram accounts", OtherValue: "Other Telegram accounts"},
|
|
{Key: "TranslationLink", Value: "https://translations.telegram.org/en"},
|
|
{Key: "RuntimeIdentifier", Value: "org.telegram.messenger"},
|
|
},
|
|
}
|
|
if err := base.UpsertPack(ctx, seed); err != nil {
|
|
t.Fatalf("seed langpack: %v", err)
|
|
}
|
|
storeWithMetadata := &metadataLangPackStore{
|
|
LangPackStore: base,
|
|
languages: []domain.LangPackLanguage{{
|
|
LangPack: "weba",
|
|
LangCode: "en",
|
|
Name: "Telegram English",
|
|
NativeName: "Telegram English",
|
|
TranslationsURL: "https://translations.telegram.org/en",
|
|
}},
|
|
}
|
|
svc := NewService(storeWithMetadata, WithPublicBaseURL("https://chat.example/root/"))
|
|
|
|
for name, load := range map[string]func() (domain.LangPack, error){
|
|
"full": func() (domain.LangPack, error) { return svc.GetLangPack(ctx, "weba", "en") },
|
|
"difference": func() (domain.LangPack, error) { return svc.GetDifference(ctx, "weba", "en", 1) },
|
|
"keys": func() (domain.LangPack, error) {
|
|
return svc.GetStrings(ctx, "weba", "en", []string{"AppName", "TranslationLink", "RuntimeIdentifier"})
|
|
},
|
|
} {
|
|
pack, err := load()
|
|
if err != nil {
|
|
t.Fatalf("%s projection: %v", name, err)
|
|
}
|
|
appName := findLangPackString(pack.Strings, "AppName")
|
|
if appName == nil || appName.Value != "Telesrv" || appName.ZeroValue != "No Telesrv accounts" || appName.OneValue != "One Telesrv account" || appName.TwoValue != "Two Telesrv accounts" || appName.FewValue != "Few Telesrv accounts" || appName.ManyValue != "Many Telesrv accounts" || appName.OtherValue != "Other Telesrv accounts" {
|
|
t.Fatalf("%s AppName = %+v, want all value forms rebranded", name, appName)
|
|
}
|
|
if got := stringValue(pack.Strings, "TranslationLink"); got != "https://chat.example/root/en" {
|
|
t.Fatalf("%s TranslationLink = %q", name, got)
|
|
}
|
|
if got := stringValue(pack.Strings, "RuntimeIdentifier"); got != "org.telegram.messenger" {
|
|
t.Fatalf("%s RuntimeIdentifier = %q, want protocol identifier unchanged", name, got)
|
|
}
|
|
}
|
|
|
|
languages, err := svc.ListLanguages(ctx, "weba")
|
|
if err != nil {
|
|
t.Fatalf("list languages: %v", err)
|
|
}
|
|
if len(languages) != 1 || languages[0].Name != "Telesrv English" || languages[0].NativeName != "Telesrv English" || languages[0].TranslationsURL != "https://chat.example/root/en" {
|
|
t.Fatalf("languages = %+v, want branded metadata", languages)
|
|
}
|
|
}
|
|
|
|
func TestListLanguagesUsesSeededPacks(t *testing.T) {
|
|
ctx := context.Background()
|
|
packs := memory.NewLangPackStore()
|
|
svc := NewService(packs)
|
|
if err := packs.UpsertPack(ctx, domain.LangPack{
|
|
LangPack: "tdesktop",
|
|
LangCode: "fr",
|
|
Version: 7,
|
|
Strings: []domain.LangPackString{
|
|
{Key: "lng_language_name", Value: "Français"},
|
|
{Key: "lng_test", Value: "Test"},
|
|
},
|
|
}); err != nil {
|
|
t.Fatalf("seed fr langpack: %v", err)
|
|
}
|
|
if err := packs.UpsertPack(ctx, domain.LangPack{
|
|
LangPack: "android",
|
|
LangCode: "fa",
|
|
Version: 8,
|
|
Strings: []domain.LangPackString{
|
|
{Key: "LanguageName", Value: "انگلیسی"},
|
|
{Key: "TranslateLanguageFA", Value: "فارسی"},
|
|
{Key: "lng_test", Value: "Test"},
|
|
},
|
|
}); err != nil {
|
|
t.Fatalf("seed fa langpack: %v", err)
|
|
}
|
|
if err := packs.UpsertPack(ctx, domain.LangPack{
|
|
LangPack: "tdesktop",
|
|
LangCode: "ckb",
|
|
Version: 9,
|
|
Strings: []domain.LangPackString{{Key: "lng_language_name", Value: "کوردی"}},
|
|
}); err != nil {
|
|
t.Fatalf("seed ckb langpack: %v", err)
|
|
}
|
|
|
|
tdesktop, err := svc.ListLanguages(ctx, "tdesktop")
|
|
if err != nil {
|
|
t.Fatalf("list tdesktop languages: %v", err)
|
|
}
|
|
fr := findLanguage(tdesktop, "fr")
|
|
if fr == nil || fr.Name != "Français" || fr.NativeName != "Français" || fr.PluralCode != "fr" || fr.StringsCount != 2 {
|
|
t.Fatalf("fr language = %+v", fr)
|
|
}
|
|
ckb := findLanguage(tdesktop, "ckb")
|
|
if ckb == nil || !ckb.Rtl {
|
|
t.Fatalf("ckb language = %+v, want file-derived rtl", ckb)
|
|
}
|
|
|
|
android, err := svc.ListLanguages(ctx, "android")
|
|
if err != nil {
|
|
t.Fatalf("list android languages: %v", err)
|
|
}
|
|
fa := findLanguage(android, "fa")
|
|
if fa == nil || fa.NativeName != "فارسی" || !fa.Rtl || fa.PluralCode != "fa" {
|
|
t.Fatalf("fa language = %+v", fa)
|
|
}
|
|
}
|
|
|
|
func TestServiceCachesLanguageResourcesAfterFirstRequest(t *testing.T) {
|
|
ctx := context.Background()
|
|
base := memory.NewLangPackStore()
|
|
for _, pack := range []domain.LangPack{
|
|
{
|
|
LangPack: "android",
|
|
LangCode: "en",
|
|
Version: 7,
|
|
Strings: []domain.LangPackString{
|
|
{Key: "LogOutTitle", Value: "Log Out"},
|
|
{Key: "NewMessageTitle", Value: "New Message"},
|
|
},
|
|
},
|
|
{
|
|
LangPack: "weba",
|
|
LangCode: "en",
|
|
Version: 12,
|
|
Strings: []domain.LangPackString{
|
|
{Key: "AccDescrPollVoteDown", Value: "Go to next unread poll vote"},
|
|
{Key: "NewMessageTitle", Value: "New Message from WebA"},
|
|
},
|
|
},
|
|
} {
|
|
if err := base.UpsertPack(ctx, pack); err != nil {
|
|
t.Fatalf("seed %s: %v", pack.LangPack, err)
|
|
}
|
|
}
|
|
counting := &countingLangPackStore{LangPackStore: base}
|
|
svc := NewService(counting)
|
|
|
|
first, err := svc.GetLangPack(ctx, "android", "en")
|
|
if err != nil {
|
|
t.Fatalf("first get langpack: %v", err)
|
|
}
|
|
first.Strings[0].Value = "caller mutation"
|
|
second, err := svc.GetLangPack(ctx, "android", "en")
|
|
if err != nil {
|
|
t.Fatalf("second get langpack: %v", err)
|
|
}
|
|
if got := stringValue(second.Strings, "LogOutTitle"); got != "Log Out" {
|
|
t.Fatalf("cached pack was mutated through caller alias: %q", got)
|
|
}
|
|
selected, err := svc.GetStrings(ctx, "android", "en", []string{"AccDescrPollVoteDown"})
|
|
if err != nil || stringValue(selected.Strings, "AccDescrPollVoteDown") == "" {
|
|
t.Fatalf("cached get strings = %+v, %v", selected, err)
|
|
}
|
|
if _, err := svc.GetDifference(ctx, "android", "en", 1); err != nil {
|
|
t.Fatalf("cached get difference: %v", err)
|
|
}
|
|
|
|
languages, err := svc.ListLanguages(ctx, "android")
|
|
if err != nil || len(languages) != 1 {
|
|
t.Fatalf("first list languages = %+v, %v", languages, err)
|
|
}
|
|
languages[0].Name = "caller mutation"
|
|
languages, err = svc.ListLanguages(ctx, "android")
|
|
if err != nil || len(languages) != 1 || languages[0].Name == "caller mutation" {
|
|
t.Fatalf("cached languages alias = %+v, %v", languages, err)
|
|
}
|
|
|
|
getPack, getStrings, listLanguages := counting.counts()
|
|
if getPack != 2 || getStrings != 0 || listLanguages != 1 {
|
|
t.Fatalf("store calls = getPack:%d getStrings:%d list:%d, want 2/0/1", getPack, getStrings, listLanguages)
|
|
}
|
|
}
|
|
|
|
func TestServiceCollapsesConcurrentLanguagePackLoads(t *testing.T) {
|
|
ctx := context.Background()
|
|
base := memory.NewLangPackStore()
|
|
if err := base.UpsertPack(ctx, domain.LangPack{
|
|
LangPack: "weba",
|
|
LangCode: "en",
|
|
Version: 3,
|
|
Strings: []domain.LangPackString{{Key: "NewMessageTitle", Value: "New Message"}},
|
|
}); err != nil {
|
|
t.Fatalf("seed weba: %v", err)
|
|
}
|
|
counting := &countingLangPackStore{LangPackStore: base, delay: 10 * time.Millisecond}
|
|
svc := NewService(counting)
|
|
start := make(chan struct{})
|
|
errs := make(chan error, 32)
|
|
var wg sync.WaitGroup
|
|
for range 32 {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
<-start
|
|
_, err := svc.GetLangPack(ctx, "weba", "en")
|
|
errs <- err
|
|
}()
|
|
}
|
|
close(start)
|
|
wg.Wait()
|
|
close(errs)
|
|
for err := range errs {
|
|
if err != nil {
|
|
t.Fatalf("concurrent get langpack: %v", err)
|
|
}
|
|
}
|
|
getPack, _, _ := counting.counts()
|
|
if getPack != 1 {
|
|
t.Fatalf("concurrent store getPack calls = %d, want 1", getPack)
|
|
}
|
|
}
|
|
|
|
func TestServiceLanguagePackCacheIsBounded(t *testing.T) {
|
|
ctx := context.Background()
|
|
base := memory.NewLangPackStore()
|
|
for _, code := range []string{"en", "fr"} {
|
|
if err := base.UpsertPack(ctx, domain.LangPack{
|
|
LangPack: "weba",
|
|
LangCode: code,
|
|
Version: 1,
|
|
Strings: []domain.LangPackString{{Key: "key", Value: code}},
|
|
}); err != nil {
|
|
t.Fatalf("seed %s: %v", code, err)
|
|
}
|
|
}
|
|
counting := &countingLangPackStore{LangPackStore: base}
|
|
svc := newServiceWithCacheLimits(counting, 1<<20, 1, 1)
|
|
for _, code := range []string{"en", "fr", "en"} {
|
|
if _, err := svc.GetLangPack(ctx, "weba", code); err != nil {
|
|
t.Fatalf("get %s: %v", code, err)
|
|
}
|
|
}
|
|
getPack, _, _ := counting.counts()
|
|
if getPack != 3 {
|
|
t.Fatalf("LRU store getPack calls = %d, want 3", getPack)
|
|
}
|
|
|
|
oversized := &countingLangPackStore{LangPackStore: base}
|
|
svc = newServiceWithCacheLimits(oversized, 1, 8, 1)
|
|
if _, err := svc.GetLangPack(ctx, "weba", "en"); err != nil {
|
|
t.Fatalf("first oversized get: %v", err)
|
|
}
|
|
if _, err := svc.GetLangPack(ctx, "weba", "en"); err != nil {
|
|
t.Fatalf("second oversized get: %v", err)
|
|
}
|
|
getPack, _, _ = oversized.counts()
|
|
if getPack != 2 {
|
|
t.Fatalf("oversized store getPack calls = %d, want 2", getPack)
|
|
}
|
|
}
|
|
|
|
type countingLangPackStore struct {
|
|
store.LangPackStore
|
|
mu sync.Mutex
|
|
delay time.Duration
|
|
getPack int
|
|
getStrings int
|
|
listLanguages int
|
|
}
|
|
|
|
type metadataLangPackStore struct {
|
|
store.LangPackStore
|
|
languages []domain.LangPackLanguage
|
|
}
|
|
|
|
func (s *metadataLangPackStore) ListLanguages(context.Context, string) ([]domain.LangPackLanguage, error) {
|
|
return append([]domain.LangPackLanguage(nil), s.languages...), nil
|
|
}
|
|
|
|
func (s *countingLangPackStore) GetPack(ctx context.Context, langPack, langCode string, fromVersion int) (domain.LangPack, error) {
|
|
s.mu.Lock()
|
|
s.getPack++
|
|
delay := s.delay
|
|
s.mu.Unlock()
|
|
if delay > 0 {
|
|
time.Sleep(delay)
|
|
}
|
|
return s.LangPackStore.GetPack(ctx, langPack, langCode, fromVersion)
|
|
}
|
|
|
|
func (s *countingLangPackStore) GetStrings(ctx context.Context, langPack, langCode string, keys []string) (domain.LangPack, error) {
|
|
s.mu.Lock()
|
|
s.getStrings++
|
|
s.mu.Unlock()
|
|
return s.LangPackStore.GetStrings(ctx, langPack, langCode, keys)
|
|
}
|
|
|
|
func (s *countingLangPackStore) ListLanguages(ctx context.Context, langPack string) ([]domain.LangPackLanguage, error) {
|
|
s.mu.Lock()
|
|
s.listLanguages++
|
|
s.mu.Unlock()
|
|
return s.LangPackStore.ListLanguages(ctx, langPack)
|
|
}
|
|
|
|
func (s *countingLangPackStore) counts() (getPack, getStrings, listLanguages int) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
return s.getPack, s.getStrings, s.listLanguages
|
|
}
|
|
|
|
func findLanguage(languages []domain.LangPackLanguage, code string) *domain.LangPackLanguage {
|
|
for i := range languages {
|
|
if languages[i].LangCode == code {
|
|
return &languages[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func stringValue(strings []domain.LangPackString, key string) string {
|
|
for _, item := range strings {
|
|
if item.Key == key {
|
|
return item.Value
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func findLangPackString(strings []domain.LangPackString, key string) *domain.LangPackString {
|
|
for i := range strings {
|
|
if strings[i].Key == key {
|
|
return &strings[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|