123 lines
6.0 KiB
Python
123 lines
6.0 KiB
Python
"""Unit checks for the release smoke harness; no Docker or network required."""
|
|
|
|
from email.message import Message
|
|
from email.parser import BytesParser
|
|
from email.policy import default
|
|
import importlib.util
|
|
from pathlib import Path
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
|
|
HELPER_PATH = Path(__file__).resolve().parents[2] / "scripts" / "container_smoke.py"
|
|
SPEC = importlib.util.spec_from_file_location("magent_container_smoke", HELPER_PATH)
|
|
smoke = importlib.util.module_from_spec(SPEC)
|
|
SPEC.loader.exec_module(smoke)
|
|
|
|
|
|
def response_headers(**changes):
|
|
headers = Message()
|
|
for key, value in {
|
|
"Content-Type": "text/html; charset=utf-8",
|
|
"Content-Security-Policy": "default-src 'self'; script-src 'self' 'nonce-test-nonce' 'strict-dynamic'",
|
|
"X-Content-Type-Options": "nosniff",
|
|
"X-Frame-Options": "DENY",
|
|
**changes,
|
|
}.items():
|
|
headers[key] = value
|
|
return headers
|
|
|
|
|
|
class ContainerPackagingHarnessTests(unittest.TestCase):
|
|
def test_backup_multipart_preserves_binary_content_and_required_fields(self):
|
|
content = b"MAGENT-BACKUP\x00\x01\xff\r\n\x00encrypted"
|
|
body, content_type = smoke.backup_restore_upload(content, "synthetic backup passphrase")
|
|
parsed = BytesParser(policy=default).parsebytes(
|
|
f"Content-Type: {content_type}\r\nMIME-Version: 1.0\r\n\r\n".encode() + body,
|
|
)
|
|
fields = {part.get_param("name", header="content-disposition"): part
|
|
for part in parsed.iter_parts()}
|
|
self.assertEqual(set(fields), {"passphrase", "confirmation", "file"})
|
|
self.assertEqual(fields["passphrase"].get_payload(decode=True), b"synthetic backup passphrase")
|
|
self.assertEqual(fields["confirmation"].get_payload(decode=True), b"RESTORE")
|
|
self.assertEqual(fields["file"].get_payload(decode=True), content)
|
|
self.assertEqual(fields["file"].get_filename(), "smoke.magent-backup")
|
|
|
|
def test_http_rejects_conflicting_body_encodings_without_network(self):
|
|
with patch.object(smoke.request, "urlopen") as urlopen:
|
|
with self.assertRaisesRegex(AssertionError, "only one encoding"):
|
|
smoke.http("/test", payload={}, raw=b"binary")
|
|
urlopen.assert_not_called()
|
|
|
|
def page(self, *, nonce="test-nonce", source="/_next/static/app.js", extra=""):
|
|
return (
|
|
f'<script nonce="{nonce}" src="{source}"></script>'
|
|
f'<script nonce="{nonce}">self.__next_f.push([])</script>'
|
|
'<link rel="stylesheet" href="/_next/static/app.css">'
|
|
f"{extra}"
|
|
).encode()
|
|
|
|
def test_static_assets_and_every_bootstrap_script_are_validated(self):
|
|
seen = []
|
|
|
|
def fake_http(path):
|
|
seen.append(path)
|
|
if path == "/login":
|
|
return self.page(), response_headers()
|
|
return b"static content", response_headers(**{"Content-Type": "application/javascript"})
|
|
|
|
with patch.object(smoke, "http", side_effect=fake_http):
|
|
assets = set()
|
|
self.assertEqual(smoke.check_page("/login", assets), "test-nonce")
|
|
self.assertEqual(assets, {"/_next/static/app.js", "/_next/static/app.css"})
|
|
self.assertEqual(seen, ["/login", "/_next/static/app.css", "/_next/static/app.js"])
|
|
smoke.check_page("/login", assets)
|
|
self.assertEqual(seen[-1], "/login")
|
|
self.assertEqual(len(seen), 4)
|
|
|
|
def test_nonce_mismatch_fails_before_fetching_assets(self):
|
|
with patch.object(smoke, "http", return_value=(self.page(nonce="wrong"), response_headers())):
|
|
with self.assertRaisesRegex(AssertionError, "script blocked by its CSP nonce"):
|
|
smoke.check_page("/login", set())
|
|
|
|
def test_missing_nonce_policy_is_rejected(self):
|
|
headers = response_headers(**{"Content-Security-Policy": "script-src 'self'"})
|
|
with patch.object(smoke, "http", return_value=(self.page(), headers)):
|
|
with self.assertRaisesRegex(AssertionError, "missing script nonce policy"):
|
|
smoke.check_page("/login", set())
|
|
|
|
def test_development_eval_policy_is_rejected(self):
|
|
headers = response_headers(**{
|
|
"Content-Security-Policy": "script-src 'nonce-test-nonce' 'strict-dynamic' 'unsafe-eval'",
|
|
})
|
|
with patch.object(smoke, "http", return_value=(self.page(), headers)):
|
|
with self.assertRaisesRegex(AssertionError, "development eval"):
|
|
smoke.check_page("/login", set())
|
|
|
|
def test_html_fallback_for_static_asset_is_rejected(self):
|
|
with patch.object(smoke, "http", return_value=(self.page(), response_headers())):
|
|
with self.assertRaisesRegex(AssertionError, "Asset returned HTML"):
|
|
smoke.check_page("/login", set())
|
|
|
|
def test_missing_executable_script_nonce_is_rejected(self):
|
|
page = self.page(extra='<script src="/_next/static/missing-nonce.js"></script>')
|
|
with patch.object(smoke, "http", return_value=(page, response_headers())):
|
|
with self.assertRaisesRegex(AssertionError, "script blocked by its CSP nonce"):
|
|
smoke.check_page("/login", set())
|
|
|
|
def test_inert_json_scripts_do_not_require_executable_nonce(self):
|
|
page = self.page(extra='<script type="application/ld+json">{"name":"Magent"}</script>')
|
|
with patch.object(smoke, "http", return_value=(page, response_headers())):
|
|
cache = {"/_next/static/app.js", "/_next/static/app.css"}
|
|
self.assertEqual(smoke.check_page("/login", cache), "test-nonce")
|
|
|
|
def test_external_scripts_are_not_followed_by_smoke_harness(self):
|
|
page = self.page(extra='<script nonce="test-nonce" src="https://external.invalid/app.js"></script>')
|
|
with patch.object(smoke, "http", return_value=(page, response_headers())):
|
|
with self.assertRaisesRegex(AssertionError, "Unexpected external executable asset"):
|
|
smoke.check_page("/login", {"/_next/static/app.js", "/_next/static/app.css"})
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|