From 4cc8052a88c5c7a648de357a5da41cf97f6be58b Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Wed, 23 Sep 2026 02:09:23 +0200 Subject: [PATCH] test: parse the page instead of matching tags with a regex The inline-script check matched ]*> and then filtered the matched text, so a > inside an attribute value split one tag into a fragment that had already lost the attribute the filter looks for. An html.parser subclass decides on the parsed attributes instead. The i18n test imported unittest twice, once plain and once as a from-import. Co-Authored-By: Claude Opus 5 (1M context) --- tests/integration/test_app_routes.py | 24 ++++++++++++++++++------ tests/unit/test_i18n.py | 4 ++-- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/tests/integration/test_app_routes.py b/tests/integration/test_app_routes.py index 18e0a73..d362ddb 100644 --- a/tests/integration/test_app_routes.py +++ b/tests/integration/test_app_routes.py @@ -6,6 +6,7 @@ import subprocess import sys import tempfile import unittest +from html.parser import HTMLParser from pathlib import Path from unittest.mock import Mock, patch @@ -112,18 +113,29 @@ class TestEscaping(AppRouteMixin, unittest.TestCase): self.assertIn("<script>alert('config')", body) +class InlineScriptCollector(HTMLParser): + def __init__(self): + super().__init__() + self.inline = [] + + def handle_starttag(self, tag, attrs): + if tag != "script": + return + attributes = dict(attrs) + if "src" in attributes or attributes.get("type") == "application/json": + return + self.inline.append(self.get_starttag_text()) + + class TestContentSecurityPolicy(AppRouteMixin, unittest.TestCase): def test_page_ships_no_executable_inline_script(self): body = self.client.get("/de/").get_data(as_text=True) - inline = [ - tag - for tag in re.findall(r"]*>", body) - if "src=" not in tag and 'type="application/json"' not in tag - ] + collector = InlineScriptCollector() + collector.feed(body) self.assertEqual( - inline, + collector.inline, [], "a host CSP can only hash an inline script whose content it knows, " "and this one changes with every language", diff --git a/tests/unit/test_i18n.py b/tests/unit/test_i18n.py index 5366c19..50ef208 100644 --- a/tests/unit/test_i18n.py +++ b/tests/unit/test_i18n.py @@ -2,8 +2,8 @@ import re import shutil import tempfile import unittest +import unittest.mock from pathlib import Path -from unittest import mock import yaml @@ -170,7 +170,7 @@ class TestCatalogMerge(unittest.TestCase): ) def test_an_unsupported_code_never_becomes_a_path(self): - with mock.patch.object(i18n, "read_catalog") as read: + with unittest.mock.patch.object(i18n, "read_catalog") as read: self.assertEqual(i18n.catalog("../content/de"), {}) read.assert_not_called()