ixg_platform/apps/core/tests.py
ixgadmin e2a6784505 Assert rendered pages carry no unquoted multi-token attributes
The ops templates shipped with 346 of them: an unquoted attribute value ends at
the first whitespace, so the styles were never applied and status badges rendered
invisible. Assert it on the rendered HTML rather than the template source, because
a template may hold {{ }} inside an unquoted attribute that renders harmlessly —
the source is ambiguous, the response is not.
2026-09-17 14:00:10 +00:00

297 lines
12 KiB
Python

"""Cross-cutting checks: every portal page renders, map tiles are served
from our own origin, and the training data the walkthrough depends on is
coherent.
None of these were covered before. The page sweep exists because a view can lose
a line — a `template_name`, say — and 500 in front of a user while the rest of the
suite stays green; it fetches each read-only page with an empty database, so it
covers the empty-state branches too. The tile tests cover the map's only external
dependency, which used to be fetched by the viewer's browser. The seed tests cover
the exception branch, which used to depend on a record that happened to exist
already, with a stored status that contradicted its own values.
"""
import re
import shutil
import tempfile
import urllib.error
from pathlib import Path
from unittest.mock import MagicMock, patch
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.management import call_command
from django.test import Client, TestCase, override_settings
from apps.batches.models import ProcessingBatch
from apps.capa.models import CAPARecord
from apps.compliance.models import ProductSpecification
from apps.qc.models import QCResult
from .tiles import browser_tile_url, upstream_url
User = get_user_model()
OPS_ROUTES = [
"/ops/",
"/ops/intakes/",
"/ops/intakes/new/",
"/ops/batches/",
"/ops/qc/",
"/ops/qc/verify/",
"/ops/coa/",
"/ops/shipments/",
"/ops/capa/",
"/ops/documents/",
"/ops/esg/",
"/ops/eudr/",
"/ops/help/",
]
PORTAL_ROUTES = [
"/portal/",
"/portal/batches/",
"/portal/documents/",
"/portal/compliance/",
"/portal/subscription/",
"/portal/help/",
]
class PortalPagesRenderTests(TestCase):
def make_user(self, email, role):
return User.objects.create_user(
email=email, password="pass1234", role=role,
full_name=email.split("@")[0], is_staff=True,
)
def sweep(self, user, routes, host):
client = Client(SERVER_NAME=host)
client.force_login(user)
for route in routes:
with self.subTest(route=route):
response = client.get(route)
body = response.content
self.assertEqual(response.status_code, 200, f"{route} did not render")
for marker in (b"Traceback", b"TemplateDoesNotExist", b"Django Version"):
self.assertNotIn(marker, body, f"{route} rendered an error page")
def test_ops_pages_render(self):
self.sweep(self.make_user("ops-sweep@ixg.local", "ops"), OPS_ROUTES, "ops.ixgltd.com")
def test_buyer_pages_render(self):
self.sweep(self.make_user("buyer-sweep@ixg.local", "buyer"), PORTAL_ROUTES, "portal.ixgltd.com")
class TileProxyTests(TestCase):
"""Tiles come from our origin, so the map does not depend on the viewer's
network being able to reach a third-party tile server."""
def setUp(self):
self.cache = Path(tempfile.mkdtemp(prefix="ixg-tile-cache-"))
self.addCleanup(shutil.rmtree, self.cache, True)
override = override_settings(LEAFLET_TILE_CACHE_DIR=str(self.cache))
override.enable()
self.addCleanup(override.disable)
self.user = User.objects.create_user(
email="tile-viewer@ixg.local", password="pass1234", role="admin",
full_name="Tile Viewer", is_staff=True,
)
self.anon = Client(SERVER_NAME="app.ixgltd.com")
self.viewer = Client(SERVER_NAME="app.ixgltd.com")
self.viewer.force_login(self.user)
@staticmethod
def fake_upstream(payload):
"""Stand in for urllib.request.urlopen returning a tile."""
response = MagicMock()
response.read.return_value = payload
context = MagicMock()
context.__enter__.return_value = response
return lambda *a, **k: context
def test_a_viewer_must_be_signed_in(self):
self.assertEqual(self.anon.get("/tiles/13/4085/3448.png").status_code, 403)
def test_tiles_outside_the_zoom_range_are_refused(self):
self.assertEqual(self.viewer.get("/tiles/20/1/1.png").status_code, 404)
# x beyond the span of that zoom level
self.assertEqual(self.viewer.get("/tiles/1/5/1.png").status_code, 404)
def test_a_tile_is_fetched_from_the_upstream_and_served(self):
payload = b"\x89PNG\r\n\x1a\n fake tile"
with patch("apps.core.tiles.urllib.request.urlopen", self.fake_upstream(payload)):
r = self.viewer.get("/tiles/13/4085/3448.png")
self.assertEqual(r.status_code, 200)
self.assertEqual(r["Content-Type"], "image/png")
self.assertEqual(r.content, payload)
self.assertTrue((self.cache / "13" / "4085" / "3448.png").is_file())
def test_a_second_request_is_served_from_the_cache(self):
payload = b"\x89PNG\r\n\x1a\n cached tile"
first = MagicMock(side_effect=self.fake_upstream(payload))
with patch("apps.core.tiles.urllib.request.urlopen", first):
self.viewer.get("/tiles/13/4085/3448.png")
self.assertEqual(first.call_count, 1)
with patch("apps.core.tiles.urllib.request.urlopen") as second:
r = self.viewer.get("/tiles/13/4085/3448.png")
self.assertEqual(second.call_count, 0, "cache should have been used")
self.assertEqual(r.status_code, 200)
self.assertEqual(r.content, payload)
def test_an_unreachable_upstream_answers_an_empty_tile_not_an_error_page(self):
with patch(
"apps.core.tiles.urllib.request.urlopen",
side_effect=urllib.error.HTTPError("u", 403, "Forbidden", None, None),
):
r = self.viewer.get("/tiles/13/4085/3448.png")
self.assertEqual(r.status_code, 204)
self.assertEqual(r.content, b"")
def test_browser_tile_url_is_same_origin_when_the_proxy_is_on(self):
self.assertEqual(browser_tile_url(), "/tiles/{z}/{x}/{y}.png")
def test_browser_tile_url_is_the_upstream_when_the_proxy_is_off(self):
with override_settings(LEAFLET_TILE_PROXY_ENABLED=False):
self.assertEqual(browser_tile_url(), settings.LEAFLET_TILE_URL)
def test_the_upstream_url_gets_the_coordinates_substituted(self):
with override_settings(
LEAFLET_TILE_URL="https://example.invalid/{s}/{z}/{x}/{y}.png"
):
self.assertEqual(
upstream_url(13, 4085, 3448),
"https://example.invalid/a/13/4085/3448.png",
)
class SeedWalkthroughTests(TestCase):
"""The walkthrough's exception branch is training data, and it has to hold
together: a failure the platform's own evaluator would recognise, a batch that
follows it, and one CAPA — reproducible on an empty database rather than
depending on a record that happened to exist already."""
def setUp(self):
self.media = Path(tempfile.mkdtemp(prefix="ixg-seed-media-"))
self.addCleanup(shutil.rmtree, self.media, True)
override = override_settings(MEDIA_ROOT=self.media)
override.enable()
self.addCleanup(override.disable)
# The seed uses the accounts that already exist rather than inventing staff.
User.objects.create_user(
email="test@ixg.local", password="pass1234", role="ops",
full_name="Test User", is_staff=True,
)
User.objects.create_user(
email="dbg@ixg.local", password="pass1234", role="ops",
full_name="Debug User", is_staff=True,
)
call_command("seed_walkthrough", verbosity=0)
def test_the_exception_branch_is_reproducible(self):
batch = ProcessingBatch.objects.get(batch_reference="IXG-TEST-001")
self.assertEqual(batch.status, ProcessingBatch.Status.QC_FAIL)
self.assertFalse(batch.intake_lineage.exists(), "the example has no lineage")
qc = QCResult.objects.get(batch=batch)
self.assertFalse(qc.overall_pass)
self.assertEqual(qc.result_status, QCResult.ResultStatus.OUTSIDE_SPEC)
self.assertTrue(qc.is_quarantine, "a failing result quarantines the batch")
self.assertEqual(qc.verification_status, QCResult.VerificationStatus.DRAFT)
self.assertIsNone(qc.verified_by, "it must still need a second pair of eyes")
def test_the_stored_status_cannot_contradict_the_stored_values(self):
qc = QCResult.objects.get(batch__batch_reference="IXG-TEST-001")
limit = ProductSpecification.current("Unrefined Shea Butter").parameters.get(
parameter="moisture"
).max_value
self.assertGreater(
qc.moisture_percentage, limit,
"the failure must be a genuine one, judged by the real evaluator",
)
def test_the_passing_batches_stay_within_specification(self):
for reference in ("IXG-BATCH-2026-014", "IXG-BATCH-2026-005"):
with self.subTest(batch=reference):
qc = QCResult.objects.get(batch__batch_reference=reference)
self.assertTrue(qc.overall_pass)
self.assertEqual(qc.result_status, QCResult.ResultStatus.WITHIN_SPEC)
self.assertFalse(qc.is_quarantine)
def test_one_capa_per_demo_batch(self):
capas = CAPARecord.objects.filter(batch__batch_reference="IXG-TEST-001")
self.assertEqual(capas.count(), 1)
self.assertEqual(
capas.first().issue_description, "QC failure on batch IXG-TEST-001"
)
def test_re_running_the_seed_does_not_duplicate_records(self):
call_command("seed_walkthrough", verbosity=0)
self.assertEqual(
QCResult.objects.filter(batch__batch_reference="IXG-TEST-001").count(), 1
)
self.assertEqual(CAPARecord.objects.count(), 1)
self.assertEqual(ProcessingBatch.objects.filter(batch_reference="IXG-TEST-001").count(), 1)
class TemplateAttributeHygieneTests(TestCase):
"""No ops or buyer page may render an unquoted attribute that holds more than
one token.
An unquoted attribute value ends at the first whitespace, so
``class=badge bg-danger`` reaches the browser as ``class="badge"`` plus two
meaningless attributes: the badge renders white-on-white and the failure is
invisible. Every ops template shipped that way for months, so this asserts it
on the *rendered* pages — the only place the question has an unambiguous
answer, since a template may contain ``{{ }}`` in an unquoted attribute that
renders harmlessly.
"""
DETECTOR = re.compile(r'\s([\w-]+)=([^\s"\'=<>`]+(?:\s+[^\s"\'=<>`]+)+)(?=[\s>])')
TAG = re.compile(r"<[a-zA-Z/][^>]*>")
@classmethod
def offenders(cls, html):
found = []
for tag in cls.TAG.findall(html):
for match in cls.DETECTOR.finditer(tag):
found.append(match.group(0).strip())
return found
def test_the_detector_catches_the_real_defect(self):
bad = '<span class=badge bg-danger>Fail</span>'
self.assertTrue(self.DETECTOR.search(self.TAG.findall(bad)[0]))
good = '<span class="badge bg-danger">Fail</span>'
self.assertFalse(self.DETECTOR.search(self.TAG.findall(good)[0]))
def check_pages(self, user, routes, host):
client = Client(SERVER_NAME=host)
client.force_login(user)
for route in routes:
with self.subTest(route=route):
html = client.get(route).content.decode()
self.assertEqual(
self.offenders(html), [],
f"{route} renders unquoted multi-token attributes",
)
def test_ops_pages_have_no_unquoted_multi_token_attributes(self):
self.check_pages(
User.objects.create_user(
email="attr-ops@ixg.local", password="pass1234", role="ops",
full_name="Attr Ops", is_staff=True,
),
OPS_ROUTES, "ops.ixgltd.com",
)
def test_portal_pages_have_no_unquoted_multi_token_attributes(self):
self.check_pages(
User.objects.create_user(
email="attr-buyer@ixg.local", password="pass1234", role="buyer",
full_name="Attr Buyer", is_staff=True,
),
PORTAL_ROUTES, "portal.ixgltd.com",
)