Files
3x-ui/internal/web/locale/locale_test.go
T
MHSanaei f1e1b0fb0c fix(i18n): publish the translation bundle only once it is complete
InitLocalizer filled the shared bundle in place, and the Telegram bot
runs it again while the panel is serving, so a request could read the
bundle's maps while they were being written and the process died on a
concurrent map access. Build the bundle privately and publish it, and
the bot and web localizers, through atomic pointers; the lazy load in
the middleware publishes only if nothing else did first.
2026-10-31 09:11:54 +00:00

49 lines
1.3 KiB
Go

package locale
import (
"net/http"
"net/http/httptest"
"sync"
"testing"
"testing/fstest"
"github.com/gin-gonic/gin"
)
type fixedLang string
func (l fixedLang) GetTgLang() (string, error) { return string(l), nil }
// The Telegram bot re-runs InitLocalizer while the panel serves requests; a request must
// never read the bundle while it is being filled (a concurrent map access kills the process).
func TestInitLocalizerWhileServingRequests(t *testing.T) {
gin.SetMode(gin.TestMode)
translations := fstest.MapFS{"translation/en_US.json": {Data: []byte(`{"hello": "Hello"}`)}}
engine := gin.New()
engine.Use(LocalizerMiddleware())
engine.GET("/", func(c *gin.Context) { c.String(http.StatusOK, I18nForLang("en-US", "hello")) })
var wg sync.WaitGroup
wg.Go(func() {
for range 200 {
if err := InitLocalizer(translations, fixedLang("en-US")); err != nil {
t.Errorf("InitLocalizer: %v", err)
return
}
}
})
for range 4 {
wg.Go(func() {
for range 200 {
engine.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/", nil))
}
})
}
wg.Wait()
rec := httptest.NewRecorder()
engine.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil))
if rec.Body.String() != "Hello" {
t.Fatalf("after re-initialisation the bundle answers %q, want Hello", rec.Body.String())
}
}