fix(raleigh): validate statistics response shapes

This commit is contained in:
Magnus Hedemark
2026-07-26 18:33:14 -04:00
committed by username
parent b030f2607c
commit bf69aa1689
3 changed files with 24 additions and 4 deletions
+2 -2
View File
@@ -170,7 +170,7 @@ The following public service boundaries were exercised successfully on 2026-07-2
### Verification
- `PYTHONDONTWRITEBYTECODE=1 python3 -m unittest raleigh/tests/test_raleigh.py`: **354 tests passed**.
- `PYTHONDONTWRITEBYTECODE=1 python3 -m unittest raleigh/tests/test_raleigh.py`: **355 tests passed**.
- `python3 -m ruff check raleigh/scripts/raleighlib/core.py raleigh/scripts/raleighlib/public_safety_stats.py raleigh/scripts/raleighlib/cli.py`: passed.
- `python3 scripts/validate-evals.py raleigh`: **11 eval manifests validated**.
- `ruby scripts/validate-skills.rb`: **110 canonical skills validated**.
@@ -181,7 +181,7 @@ The following public service boundaries were exercised successfully on 2026-07-2
- Live `fire stats --year 2026 --json`: returned seven official published categories, including medical `7,882`, source revision/retrieval metadata, the annual document URL, and the aggregate-only privacy warning.
- Live `fire reports --year 2025 --quarter 1 --json`: returned the canonical official quarterly page.
- Live `fire reports --date 2026-07-24 --json`: exercised the unchanged ArcGIS incident-report path successfully.
- Three review passes found and then verified fixes for terminal-control handling, path traversal and URL components, empty tables, document availability, redirect targets, JSON:API relationship ambiguity, and eval coverage; the final pass reported no actionable findings.
- Review passes found and then verified fixes for terminal-control handling, path traversal and URL components, empty tables, document availability, redirect targets, malformed or ambiguous JSON:API relationships, and eval coverage.
### Remaining boundaries and follow-up triggers
@@ -151,17 +151,22 @@ def _fetch_page(agency: str) -> tuple[dict[str, Any], dict[str, str]]:
if not isinstance(data, dict) or not isinstance(included, list):
raise PublishedStatisticsError("published statistics source returned invalid JSON:API data")
attrs = data.get("attributes")
path = attrs.get("path") if isinstance(attrs, dict) else None
if (
data.get("type") != "node--service"
or data.get("id") != source["id"]
or not isinstance(attrs, dict)
or not isinstance(path, dict)
or attrs.get("status") is not True
or attrs.get("title") != source["title"]
or attrs.get("path", {}).get("alias") != urllib.parse.urlparse(source["page_url"]).path
or path.get("alias") != urllib.parse.urlparse(source["page_url"]).path
):
raise PublishedStatisticsError("published statistics page identity changed")
relationship = data.get("relationships", {}).get("field_content_primary", {})
relationships = data.get("relationships")
if not isinstance(relationships, dict):
raise PublishedStatisticsError("published statistics content relationships are invalid")
relationship = relationships.get("field_content_primary", {})
related = relationship.get("data") if isinstance(relationship, dict) else None
if not isinstance(related, list):
raise PublishedStatisticsError("published statistics content relationship is missing")
+15
View File
@@ -3071,6 +3071,21 @@ class PublishedPublicSafetyStatisticsTests(unittest.TestCase):
with self.assertRaisesRegex(public_safety_stats.PublishedStatisticsError, "no published"):
public_safety_stats.reports("police", 2026, 4)
def test_malformed_jsonapi_nested_objects_fail_visibly(self):
for field, value, message in (
("path", None, "page identity changed"),
("relationships", None, "relationships are invalid"),
):
with self.subTest(field=field):
fixture = self._fixture("police")
if field == "path":
fixture["data"]["attributes"][field] = value
else:
fixture["data"][field] = value
with patch("raleighlib.public_safety_stats.core.json_request", return_value=fixture):
with self.assertRaisesRegex(public_safety_stats.PublishedStatisticsError, message):
public_safety_stats.reports("police")
def test_report_link_outside_canonical_origins_fails_closed(self):
fixture = self._fixture("police")
html = fixture["included"][0]["attributes"]["field_stories_text_formatted"]["value"]