fix(web): render <strong> in /me/activity hero subtitle instead of escaping it (#312)
The subtitle was built by ~-concatenating a Markup operand
(user.email | e) with HTML string literals. Under autoescaping,
Jinja2's markup_join escapes every non-Markup part once it hits a
Markup operand — so the literal <strong> tags became <strong>
and the page showed literal "<strong>...</strong>" text around the
email. The | safe in _page_hero.html was too late to undo it.
Switch to {% set %}...{% endset %} block capture: the literal
<strong> stays HTML while {{ user.email }} is still autoescaped.
Regression test asserts the tags render and a hostile email stays
escaped.
This commit is contained in:
parent
a1c7849b3e
commit
8b5b0f8ef5
3 changed files with 26 additions and 1 deletions
|
|
@ -10,6 +10,15 @@ CalVer image tags (`stable-YYYY.MM.N`, `dev-YYYY.MM.N`) are produced for every C
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- `/me/activity` hero subtitle showed literal `<strong>…</strong>` tags
|
||||||
|
around the user's email instead of rendering them bold. The subtitle
|
||||||
|
was built by `~`-concatenating a `Markup` operand (`user.email | e`)
|
||||||
|
with HTML string literals, which made Jinja2's `markup_join` escape
|
||||||
|
the literal tags too. Switched to `{% set %}…{% endset %}` block
|
||||||
|
capture so the literal `<strong>` stays HTML while the email is still
|
||||||
|
autoescaped.
|
||||||
|
|
||||||
### Internal
|
### Internal
|
||||||
- CI test suite sharded for speed. The `test` job in `.github/workflows/ci.yml` is now a `test-shard` matrix — 4 parallel jobs via `pytest-split`, balanced by a committed `.test_durations` file — aggregated into a single `test` status check so branch protection needs no change. The duplicate full-suite `test` job in `release.yml` is removed (it re-ran the same ~10 min suite a second time on every push to main/feature branches); `release.yml` is now image-build only, with the advisory ruff/mypy steps moved to a lean `lint` job in `ci.yml`. Net: ~10 min → ~3 min wall-clock per push, and the suite runs once instead of twice. Adds `pytest-split` to the `dev` extra.
|
- CI test suite sharded for speed. The `test` job in `.github/workflows/ci.yml` is now a `test-shard` matrix — 4 parallel jobs via `pytest-split`, balanced by a committed `.test_durations` file — aggregated into a single `test` status check so branch protection needs no change. The duplicate full-suite `test` job in `release.yml` is removed (it re-ran the same ~10 min suite a second time on every push to main/feature branches); `release.yml` is now image-build only, with the advisory ruff/mypy steps moved to a lean `lint` job in `ci.yml`. Net: ~10 min → ~3 min wall-clock per push, and the suite runs once instead of twice. Adds `pytest-split` to the `dev` extra.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,10 @@
|
||||||
<div class="activity-page">
|
<div class="activity-page">
|
||||||
{% set page_hero_eyebrow = "Profile" %}
|
{% set page_hero_eyebrow = "Profile" %}
|
||||||
{% set page_hero_title = "My activity" %}
|
{% set page_hero_title = "My activity" %}
|
||||||
{% set page_hero_subtitle = "Sessions, token usage, data access, and sync activity for <strong>" ~ (user.email | e) ~ "</strong>." %}
|
{# Block-capture so the literal <strong> stays HTML while {{ user.email }}
|
||||||
|
is still autoescaped. `~`-concatenating a Markup operand (user.email | e)
|
||||||
|
made Jinja2's markup_join escape the literal tags too. #}
|
||||||
|
{% set page_hero_subtitle %}Sessions, token usage, data access, and sync activity for <strong>{{ user.email }}</strong>.{% endset %}
|
||||||
{% include "_page_hero.html" %}
|
{% include "_page_hero.html" %}
|
||||||
|
|
||||||
<nav class="tab-strip" role="tablist" aria-label="Activity sections">
|
<nav class="tab-strip" role="tablist" aria-label="Activity sections">
|
||||||
|
|
|
||||||
|
|
@ -422,6 +422,19 @@ class TestAdminRoleGuards:
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
assert b"My activity" in r.content
|
assert b"My activity" in r.content
|
||||||
|
|
||||||
|
def test_me_activity_hero_renders_strong_email_unescaped(self, web_client, analyst_cookie):
|
||||||
|
"""Regression: the /me/activity hero subtitle embeds the user's email
|
||||||
|
in <strong> tags. Building it via `~` concatenation with a Markup
|
||||||
|
operand (`user.email | e`) made Jinja2's markup_join escape the
|
||||||
|
literal tags too, so the page showed literal "<strong>...</strong>"
|
||||||
|
text. The subtitle must render real <strong> HTML while still
|
||||||
|
escaping the email itself."""
|
||||||
|
r = web_client.get("/me/activity", cookies=analyst_cookie, follow_redirects=False)
|
||||||
|
assert r.status_code == 200
|
||||||
|
body = r.text
|
||||||
|
assert "activity for <strong>analyst@test.com</strong>." in body
|
||||||
|
assert "activity for <strong>" not in body
|
||||||
|
|
||||||
def test_profile_session_download_returns_file_for_owner(self, web_client, analyst_cookie, tmp_path, monkeypatch):
|
def test_profile_session_download_returns_file_for_owner(self, web_client, analyst_cookie, tmp_path, monkeypatch):
|
||||||
"""Authenticated owner can fetch their own jsonl with proper Content-Disposition."""
|
"""Authenticated owner can fetch their own jsonl with proper Content-Disposition."""
|
||||||
# The seeded analyst is "analyst1" (per conftest.seeded_app).
|
# The seeded analyst is "analyst1" (per conftest.seeded_app).
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue