Ctrl K
ComunidadGuías…

internal/server/settings_security_i18n_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// SPDX-License-Identifier: Apache-2.0

package server

import (
	"html"
	"net/http"
	"net/url"
	"strings"
	"testing"

	"github.com/X4Applegate/caddyui/internal/models"
)

// A partial catalog must retain usable security controls through English
// fallback. Changing display language must never translate submitted values.
func TestI18nSecuritySettingsFallbackAndControls(t *testing.T) {
	e := newSecEnv(t)
	for _, lang := range []string{"en", "fr", "ru", "zh-CN"} {
		t.Run(lang, func(t *testing.T) {
			e.do(t, "admin", http.MethodPost, "/profile", url.Values{"action": {"update_locale"}, "locale": {lang}})
			rec := e.do(t, "admin", http.MethodGet, "/settings/security", nil)
			if rec.Code != http.StatusOK {
				t.Fatalf("security settings: HTTP %d", rec.Code)
			}
			body := rec.Body.String()
			for _, key := range []string{"settings.security.captcha.title", "settings.security.allowlist.label", "settings.security.oidc.label", "settings.security.totp.require"} {
				want := html.EscapeString(translations().T(lang, key))
				if !strings.Contains(body, want) {
					t.Errorf("missing localized security text %q", want)
				}
			}
			for _, contract := range []string{`name="captcha_provider" value="off"`, `name="captcha_provider" value="turnstile"`, `name="captcha_provider" value="recaptcha"`, `data-captcha-panel="turnstile"`, `data-captcha-panel="recaptcha"`, `name="admin_allowlist"`, `name="global_ip_blocklist"`, `name="oidc_default_role"`, `value="view"`, `value="user"`, `value="admin"`, `name="session_duration_days"`, `name="recaptcha_min_score"`, `CADDYUI_ADMIN_ALLOWLIST=off`, `group|role`} {
				if !strings.Contains(body, contract) {
					t.Errorf("security form contract lost: %s", contract)
				}
			}
			if strings.Contains(body, ">settings.security.") || strings.Contains(body, `title="settings.security.`) {
				t.Error("raw translation key rendered")
			}
			if lang == "ru" || lang == "zh-CN" {
				if !strings.Contains(body, "CAPTCHA protection") || !strings.Contains(body, "Security policy") {
					t.Error("partial catalog did not fall back to English")
				}
			}
		})
	}
}

func TestI18nSecuritySettingsEscapesStoredValues(t *testing.T) {
	e := newSecEnv(t)
	const unsafe = `<img src=x onerror=alert(1)>`
	if err := models.SetSetting(e.db, "oidc_issuer", unsafe); err != nil {
		t.Fatal(err)
	}
	body := e.do(t, "admin", http.MethodGet, "/settings/security", nil).Body.String()
	if strings.Contains(body, unsafe) || !strings.Contains(body, html.EscapeString(unsafe)) {
		t.Error("stored issuer must remain escaped in the translated form")
	}
}

// These messages must remain whole so translators can move technical values
// and the profile-menu label to the position their language requires.
func TestI18nSecuritySettingsInterpolatesCompleteHelp(t *testing.T) {
	e := newSecEnv(t)
	for _, lang := range []string{"en", "fr", "ru", "zh-CN"} {
		t.Run(lang, func(t *testing.T) {
			e.do(t, "admin", http.MethodPost, "/profile", url.Values{"action": {"update_locale"}, "locale": {lang}})
			rec := e.do(t, "admin", http.MethodGet, "/settings/security", nil)
			if rec.Code != http.StatusOK {
				t.Fatalf("security settings: HTTP %d", rec.Code)
			}
			body := rec.Body.String()
			for _, message := range []struct {
				key  string
				args []any
			}{
				{"settings.security.oidc.redirect_help", []any{"suffix", "/callback"}},
				{"settings.security.oidc.groups_help", []any{"claim", "groups"}},
				{"settings.security.totp.help", []any{"profile", translations().T(lang, "settings.security.totp.profile_link")}},
			} {
				want := html.EscapeString(translations().T(lang, message.key, message.args...))
				if !strings.Contains(body, want) {
					t.Errorf("complete help missing for %s: %q", message.key, want)
				}
			}
			for _, placeholder := range []string{"{suffix}", "{claim}", "{profile}"} {
				if strings.Contains(body, placeholder) {
					t.Errorf("unresolved help placeholder: %s", placeholder)
				}
			}
			if !strings.Contains(body, "https://") || !strings.Contains(body, "/auth/oidc/callback") {
				t.Error("technical callback example lost")
			}
		})
	}
}

// Exercise the conditional labels too: a default empty form cannot cover
// configured providers or the environment override, and secrets must stay private.
func TestI18nSecuritySettingsCaptchaStates(t *testing.T) {
	t.Setenv(envCaptchaDisable, "")
	e := newSecEnv(t)
	for _, tc := range []struct {
		name, provider, site, secret, override, key string
	}{
		{"off", "off", "", "", "", "status.disabled"},
		{"turnstile_missing", "turnstile", "", "", "", "settings.security.captcha.turnstile_missing"},
		{"turnstile_active", "turnstile", "fixture-site", "fixture-secret-not-for-html", "", "settings.security.captcha.turnstile_active"},
		{"recaptcha_missing", "recaptcha", "", "", "", "settings.security.captcha.recaptcha_missing"},
		{"recaptcha_active", "recaptcha", "fixture-site", "fixture-secret-not-for-html", "", "settings.security.captcha.recaptcha_active"},
		{"override", "turnstile", "fixture-site", "fixture-secret-not-for-html", "1", "settings.security.captcha.overridden"},
	} {
		t.Run(tc.name, func(t *testing.T) {
			t.Setenv(envCaptchaDisable, tc.override)
			for key, value := range map[string]string{
				settingCaptchaProvider:    tc.provider,
				settingTurnstileSiteKey:   tc.site,
				settingTurnstileSecretKey: tc.secret,
				settingRecaptchaSiteKey:   tc.site,
				settingRecaptchaSecretKey: tc.secret,
			} {
				if err := models.SetSetting(e.db, key, value); err != nil {
					t.Fatal(err)
				}
			}
			for _, lang := range []string{"en", "fr", "ru", "zh-CN"} {
				e.do(t, "admin", http.MethodPost, "/profile", url.Values{"action": {"update_locale"}, "locale": {lang}})
				rec := e.do(t, "admin", http.MethodGet, "/settings/security", nil)
				if rec.Code != http.StatusOK {
					t.Fatalf("%s: security settings HTTP %d", lang, rec.Code)
				}
				body := rec.Body.String()
				want := html.EscapeString(translations().T(lang, tc.key))
				if !strings.Contains(body, want) {
					t.Errorf("%s: missing CAPTCHA state %q", lang, want)
				}
				if tc.override != "" && !strings.Contains(body, "CADDYUI_CAPTCHA_DISABLE") {
					t.Errorf("%s: missing override recovery variable", lang)
				}
				if strings.Contains(body, "fixture-secret-not-for-html") {
					t.Errorf("%s: CAPTCHA secret leaked into HTML", lang)
				}
			}
		})
	}
}

func TestI18nOIDCAllowedDomainsHelp(t *testing.T) {
	e := newSecEnv(t)
	for _, lang := range []string{"en", "fr", "ru", "zh-CN"} {
		t.Run(lang, func(t *testing.T) {
			e.do(t, "admin", http.MethodPost, "/profile", url.Values{"action": {"update_locale"}, "locale": {lang}})
			body := e.do(t, "admin", http.MethodGet, "/settings/security", nil).Body.String()
			for _, key := range []string{"domains_label", "domains_warning", "domains_public_help"} {
				want := html.EscapeString(translations().T(lang, "settings.security.oidc."+key))
				if !strings.Contains(body, want) {
					t.Errorf("missing %s", key)
				}
			}
			help := translations().T(lang, "settings.security.oidc.domains_help", "example", "mail.example.com")
			if !strings.Contains(body, html.EscapeString(help)) || strings.Contains(body, "{example}") {
				t.Error("domain help interpolation failed")
			}
			if !strings.Contains(body, `name="oidc_allowed_domains"`) {
				t.Error("allowed-domain field lost")
			}
			warning := html.EscapeString(translations().T(lang, "settings.security.oidc.domains_warning"))
			if !strings.Contains(body, `<strong class="text-amber-700">`+warning+`</strong>`) {
				t.Error("security warning emphasis lost")
			}
		})
	}
}