Base layout with top bar, role-based menu, profile menu and phone layout; theme, language and German form of address kept in cookies; German formal, German informal and English text; Fluent avatar bundled in the image by a Node build stage; placeholder pages for the Publisher and Admin groups. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N3t4WL7bd9bdHHNjEsxzqC
89 lines
3.3 KiB
Python
89 lines
3.3 KiB
Python
"""Per-device preferences: cookies in, validated values out (AD-15)."""
|
|
|
|
import pytest
|
|
from django.test import Client, RequestFactory
|
|
|
|
from portal.shell import preferences
|
|
from portal.shell.preferences import Preferences
|
|
|
|
|
|
def request_with(cookies=None, accept_language=""):
|
|
request = RequestFactory().get("/", HTTP_ACCEPT_LANGUAGE=accept_language)
|
|
request.COOKIES.update(cookies or {})
|
|
return request
|
|
|
|
|
|
def test_without_cookies_the_defaults_apply():
|
|
prefs = preferences.from_request(request_with())
|
|
assert prefs == Preferences(theme="system", language="en", form="sie")
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("header", "expected"),
|
|
[("de-DE,de;q=0.9,en;q=0.5", "de"), ("en-GB", "en"), ("fr-FR", "en"), ("", "en")],
|
|
)
|
|
def test_the_language_defaults_from_the_browser(header, expected):
|
|
assert preferences.from_request(request_with(accept_language=header)).language == expected
|
|
|
|
|
|
def test_valid_cookies_are_used_and_beat_the_browser():
|
|
cookies = {"pm_theme": "dark", "pm_lang": "en", "pm_form": "du"}
|
|
prefs = preferences.from_request(request_with(cookies, accept_language="de"))
|
|
assert prefs == Preferences(theme="dark", language="en", form="du")
|
|
|
|
|
|
@pytest.mark.parametrize("value", ["", "blue", "<script>", "DARK", "x" * 500])
|
|
def test_forged_cookie_values_fall_back_to_the_defaults(value):
|
|
cookies = {"pm_theme": value, "pm_lang": value, "pm_form": value}
|
|
prefs = preferences.from_request(request_with(cookies))
|
|
assert prefs == Preferences(theme="system", language="en", form="sie")
|
|
|
|
|
|
def test_the_informal_form_only_changes_german():
|
|
assert Preferences(language="de", form="du").language_code == "de-du"
|
|
assert Preferences(language="de", form="sie").language_code == "de"
|
|
assert Preferences(language="en", form="du").language_code == "en"
|
|
|
|
|
|
def test_only_valid_choices_are_taken_from_a_submission():
|
|
assert preferences.chosen({"theme": "dark", "language": "xx", "form": "du"}) == {
|
|
"pm_theme": "dark",
|
|
"pm_form": "du",
|
|
}
|
|
assert preferences.chosen({}) == {}
|
|
|
|
|
|
# -- the preferences address ------------------------------------------------------------------
|
|
|
|
|
|
def test_choosing_sets_the_cookies_and_returns_to_the_page():
|
|
response = Client().post(
|
|
"/preferences", {"theme": "dark", "language": "de", "form": "du", "next": "/x?y=1"}
|
|
)
|
|
assert response.status_code == 302 and response["Location"] == "/x?y=1"
|
|
for name, value in (("pm_theme", "dark"), ("pm_lang", "de"), ("pm_form", "du")):
|
|
cookie = response.cookies[name]
|
|
assert cookie.value == value
|
|
assert cookie["samesite"] == "Lax" and cookie["secure"] and not cookie["httponly"]
|
|
assert cookie["max-age"] == preferences.COOKIE_MAX_AGE
|
|
|
|
|
|
def test_an_invalid_value_sets_no_cookie():
|
|
response = Client().post("/preferences", {"theme": "neon", "language": "fr"})
|
|
assert not response.cookies
|
|
|
|
|
|
@pytest.mark.parametrize("target", ["https://evil.example.test/", "//evil.example.test", "x", ""])
|
|
def test_the_return_address_cannot_leave_the_site(target):
|
|
response = Client().post("/preferences", {"theme": "dark", "next": target})
|
|
assert response["Location"] == "/"
|
|
|
|
|
|
def test_choosing_needs_post():
|
|
assert Client().get("/preferences").status_code == 405
|
|
|
|
|
|
def test_choosing_needs_a_csrf_token():
|
|
strict = Client(enforce_csrf_checks=True)
|
|
assert strict.post("/preferences", {"theme": "dark"}).status_code == 403
|