// SPDX-License-Identifier: Apache-2.0 package server import ( "html" "net/http" "net/http/httptest" "net/url" "strings" "testing" "github.com/X4Applegate/caddyui/internal/auth" "github.com/X4Applegate/caddyui/internal/models" ) // Anonymous pages use the site or browser language, not an account selected // by an untrusted email field. Partial catalogs keep working through English. func TestI18nLoginAnonymousLocaleAndMessages(t *testing.T) { e := newSecEnv(t) for _, tc := range []struct{ name, site, browser, lang string }{ {"browser_french", "", "fr-FR,fr;q=0.9,en;q=0.8", "fr"}, {"site_over_browser", "fr", "en-US", "fr"}, {"english", "en", "fr-FR", "en"}, {"russian_fallback", "ru", "en-US", "ru"}, {"chinese_fallback", "zh-CN", "en-US", "zh-CN"}, {"unknown_browser", "", "xx-YY", "en"}, } { t.Run(tc.name, func(t *testing.T) { if err := models.SetSetting(e.db, settingDefaultLocale, tc.site); err != nil { t.Fatal(err) } for _, page := range []struct{ query, key string }{ {"", "login.heading"}, {"?reset=1", "login.password_updated"}, {"?invited=1", "login.account_activated"}, {"?error=sso_nouser", "login.error.sso_no_user"}, {"?error=sso_email", "login.error.sso_email"}, {"?error=sso_denied", "login.error.sso_cancelled"}, {"?error=sso_domain", "login.error.sso_domain"}, {"?error=sso_unknown", "login.error.sso_failed"}, } { req := httptest.NewRequest(http.MethodGet, "/login"+page.query, nil) req.Header.Set("Accept-Language", tc.browser) rec := httptest.NewRecorder() e.h.ServeHTTP(rec, req) body := rec.Body.String() if rec.Code != http.StatusOK { t.Fatalf("%s: HTTP %d", page.query, rec.Code) } if !strings.Contains(body, `login.") { t.Error("raw login key rendered") } } }) } } func TestI18nLoginErrorsAndSuccessfulSession(t *testing.T) { t.Setenv(envCaptchaDisable, "") e := newSecEnv(t) if err := models.SetSetting(e.db, settingDefaultLocale, "fr"); err != nil { t.Fatal(err) } for _, email := range []string{"admin@t", "unknown@t"} { rec := e.do(t, "", http.MethodPost, "/login", url.Values{"email": {email}, "password": {"wrong-password"}}) want := html.EscapeString(translations().T("fr", "login.error.invalid_credentials")) if rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), want) { t.Errorf("invalid login should show generic localized error, HTTP %d", rec.Code) } for _, cookie := range rec.Result().Cookies() { if cookie.Name == auth.SessionCookie && cookie.Value != "" { t.Error("failed login created a session") } } } rec := e.do(t, "", http.MethodPost, "/login", url.Values{"email": {"admin@t"}, "password": {"password-123"}}) if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/" { t.Fatalf("successful login changed: %d %s", rec.Code, rec.Header().Get("Location")) } hasSession := false for _, cookie := range rec.Result().Cookies() { if cookie.Name == auth.SessionCookie && cookie.Value != "" { hasSession = true } } if !hasSession { t.Error("successful login did not create a session") } if err := models.SetSetting(e.db, settingMaxLoginAttempts, "1"); err != nil { t.Fatal(err) } rec = e.do(t, "", http.MethodPost, "/login", url.Values{"email": {"admin@t"}, "password": {"wrong-password"}}) if !strings.Contains(rec.Body.String(), html.EscapeString(translations().T("fr", "login.error.login_limit"))) { t.Error("localized lockout message missing") } } func TestI18nLoginSSOOnlyTranslatesBuiltInLabel(t *testing.T) { e := newSecEnv(t) for key, value := range map[string]string{settingDefaultLocale: "fr", settingOIDCEnabled: "1", settingOIDCIssuer: "https://idp.example.test", settingOIDCClientID: "fixture-client", settingOIDCClientSecret: "fixture-secret", settingOIDCRedirectURL: "https://caddyui.example.test/auth/oidc/callback"} { if err := models.SetSetting(e.db, key, value); err != nil { t.Fatal(err) } } for _, label := range []string{"", "Sign in with SSO", ``} { if err := models.SetSetting(e.db, settingOIDCButtonLabel, label); err != nil { t.Fatal(err) } rec := e.do(t, "", http.MethodGet, "/login", nil) if rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), `href="/auth/oidc/login"`) { t.Fatalf("configured SSO button missing: HTTP %d", rec.Code) } want := label if label == "" { want = translations().T("fr", "login.sso_default") } if !strings.Contains(rec.Body.String(), html.EscapeString(want)) { t.Errorf("SSO label not preserved/translated: %q", want) } if strings.Contains(rec.Body.String(), ``) { t.Error("custom SSO label was not escaped") } } }