mirror of
https://github.com/Sea-Haven-Industries/procurement-ingest.git
synced 2026-09-30 09:33:15 +00:00
143 lines
5.2 KiB
Python
143 lines
5.2 KiB
Python
|
|
"""web_ui handler coverage for BOTH pipelines (constraint 5c) -- 0% before
|
||
|
|
Phase 8. Root placement is deliberate: web_ui_auth is cross-pipeline and PO
|
||
|
|
web_ui lacks __init__.py, so the modules are reached ONLY through the shared
|
||
|
|
loader (``load_lambda_module("po"|"wo", "web_ui/handler")``), never package
|
||
|
|
imports. The loader's web_ui_auth sibling entry binds each handler's bare
|
||
|
|
``from web_ui_auth import is_authenticated``.
|
||
|
|
|
||
|
|
Pins: wrong/absent token -> 401; 401 WITHOUT any table access (auth is the first
|
||
|
|
line of handler()); the authenticated render path; and a hostile-field escaping
|
||
|
|
regression lock (every rendered field carrying <script>/quotes/ampersands must
|
||
|
|
appear only as escaped entities).
|
||
|
|
"""
|
||
|
|
|
||
|
|
from types import SimpleNamespace
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from tests.support import load_lambda_module
|
||
|
|
|
||
|
|
_HOSTILE = "<script>alert(1)</script>&\"'"
|
||
|
|
|
||
|
|
|
||
|
|
class _WebFakeTable:
|
||
|
|
def __init__(self, items):
|
||
|
|
self._items = items
|
||
|
|
|
||
|
|
def scan(self, **kwargs):
|
||
|
|
return {"Items": list(self._items)}
|
||
|
|
|
||
|
|
def get_item(self, Key): # noqa: N803 (boto3 kwarg name)
|
||
|
|
pk, val = next(iter(Key.items()))
|
||
|
|
for item in self._items:
|
||
|
|
if item.get(pk) == val:
|
||
|
|
return {"Item": item}
|
||
|
|
return {}
|
||
|
|
|
||
|
|
def query(self, **kwargs):
|
||
|
|
return {"Items": []}
|
||
|
|
|
||
|
|
|
||
|
|
class _WebFakeDynamo:
|
||
|
|
def __init__(self, tables):
|
||
|
|
self._tables = tables
|
||
|
|
|
||
|
|
def Table(self, name): # noqa: N802 (boto3 method name)
|
||
|
|
return _WebFakeTable(self._tables.get(name, []))
|
||
|
|
|
||
|
|
|
||
|
|
class _ExplodingDynamo:
|
||
|
|
"""Any table access is a contract violation: auth must run first."""
|
||
|
|
|
||
|
|
def Table(self, name): # noqa: N802
|
||
|
|
raise AssertionError(f"auth must precede any table access ({name})")
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture(params=["po", "wo"])
|
||
|
|
def web_ui(request):
|
||
|
|
mod = load_lambda_module(request.param, "web_ui/handler")
|
||
|
|
if request.param == "po":
|
||
|
|
table = mod.PO_TABLE
|
||
|
|
hostile_item = {
|
||
|
|
"po_number": _HOSTILE,
|
||
|
|
"po_status": _HOSTILE,
|
||
|
|
"email_type": _HOSTILE,
|
||
|
|
"supplier": {"name": _HOSTILE},
|
||
|
|
"site_code": _HOSTILE,
|
||
|
|
"trade": _HOSTILE,
|
||
|
|
"total_amount": _HOSTILE,
|
||
|
|
"processed_at": _HOSTILE,
|
||
|
|
}
|
||
|
|
else:
|
||
|
|
table = mod.WORK_ORDERS_TABLE
|
||
|
|
hostile_item = {
|
||
|
|
"work_order_id": _HOSTILE,
|
||
|
|
"description": _HOSTILE,
|
||
|
|
"site_code": _HOSTILE,
|
||
|
|
"wo_status": _HOSTILE,
|
||
|
|
"record_type": _HOSTILE,
|
||
|
|
"due_date": _HOSTILE,
|
||
|
|
"updated_at": _HOSTILE,
|
||
|
|
}
|
||
|
|
return SimpleNamespace(mod=mod, table=table, hostile_item=hostile_item)
|
||
|
|
|
||
|
|
|
||
|
|
def test_wrong_token_returns_401(web_ui, monkeypatch):
|
||
|
|
monkeypatch.setattr(web_ui.mod, "is_authenticated", lambda event: False)
|
||
|
|
result = web_ui.mod.handler({"headers": {"x-auth-token": "wrong"}}, None)
|
||
|
|
assert result["statusCode"] == 401
|
||
|
|
|
||
|
|
|
||
|
|
def test_absent_token_returns_401(web_ui, monkeypatch):
|
||
|
|
monkeypatch.setattr(web_ui.mod, "is_authenticated", lambda event: False)
|
||
|
|
result = web_ui.mod.handler({}, None)
|
||
|
|
assert result["statusCode"] == 401
|
||
|
|
|
||
|
|
|
||
|
|
def test_401_does_not_touch_the_table(web_ui, monkeypatch):
|
||
|
|
"""Auth is the first line of handler(): a rejected request must return 401
|
||
|
|
WITHOUT scanning (or otherwise touching) the DynamoDB table."""
|
||
|
|
monkeypatch.setattr(web_ui.mod, "is_authenticated", lambda event: False)
|
||
|
|
monkeypatch.setattr(web_ui.mod, "dynamodb", _ExplodingDynamo())
|
||
|
|
result = web_ui.mod.handler({}, None) # _ExplodingDynamo raises if touched
|
||
|
|
assert result["statusCode"] == 401
|
||
|
|
|
||
|
|
|
||
|
|
def test_authenticated_render_path(web_ui, monkeypatch):
|
||
|
|
monkeypatch.setattr(web_ui.mod, "is_authenticated", lambda event: True)
|
||
|
|
monkeypatch.setattr(
|
||
|
|
web_ui.mod, "dynamodb", _WebFakeDynamo({web_ui.table: [web_ui.hostile_item]})
|
||
|
|
)
|
||
|
|
result = web_ui.mod.handler({}, None)
|
||
|
|
assert result["statusCode"] == 200
|
||
|
|
assert result["headers"]["Content-Type"] == "text/html"
|
||
|
|
assert "<html" in result["body"].lower()
|
||
|
|
|
||
|
|
|
||
|
|
def test_hostile_fields_are_escaped_not_reflected_raw(web_ui, monkeypatch):
|
||
|
|
"""Regression lock: hostile field values must reach the HTML only as escaped
|
||
|
|
entities -- no raw <script> in element context, and the payload's quotes
|
||
|
|
must be entity-escaped in attribute context (the onclick row-link sink), or
|
||
|
|
a " breaks out of the attribute value and injects an event handler."""
|
||
|
|
monkeypatch.setattr(web_ui.mod, "is_authenticated", lambda event: True)
|
||
|
|
monkeypatch.setattr(
|
||
|
|
web_ui.mod, "dynamodb", _WebFakeDynamo({web_ui.table: [web_ui.hostile_item]})
|
||
|
|
)
|
||
|
|
body = web_ui.mod.handler({}, None)["body"]
|
||
|
|
|
||
|
|
# element context: angle brackets escaped
|
||
|
|
assert "<script>alert(1)</script>" not in body # never reflected raw
|
||
|
|
assert "<script>alert(1)</script>" in body # escaped form present
|
||
|
|
|
||
|
|
# attribute context: the id flows through json.dumps into
|
||
|
|
# onclick="window.location={esc(..., quote=True)}", so the JSON string's
|
||
|
|
# opening quote must render as " -- a raw " right after the = means
|
||
|
|
# quote-escaping regressed and the attribute is breakable
|
||
|
|
assert "window.location="" in body
|
||
|
|
assert 'window.location="' not in body
|
||
|
|
|
||
|
|
# the payload's own quote characters appear only entity-escaped
|
||
|
|
assert """ in body
|
||
|
|
assert "'" in body
|
||
|
|
assert _HOSTILE not in body
|