281 lines
11 KiB
Python
281 lines
11 KiB
Python
import gzip
|
|
import re
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
import httpx
|
|
import pytest
|
|
from starlette.requests import Request
|
|
|
|
from src import main
|
|
from src.frontend_bundle import build_frontend
|
|
from src.views import dashboard, feature_bundle, runtime_bundle, service_worker
|
|
|
|
|
|
FRONTEND = Path(__file__).resolve().parents[1] / "frontend"
|
|
PAGE_SCRIPT = re.compile(r'<script src="(static/[^"]+\.js)"></script>')
|
|
|
|
|
|
def test_page_runtime_is_one_deterministic_content_addressed_bundle(tmp_path):
|
|
first = build_frontend(FRONTEND)
|
|
second = build_frontend(FRONTEND)
|
|
|
|
assert first.runtime_name == second.runtime_name
|
|
assert first.runtime_bytes == second.runtime_bytes
|
|
assert first.runtime_gzip_bytes == second.runtime_gzip_bytes
|
|
assert gzip.decompress(first.runtime_gzip_bytes) == first.runtime_bytes
|
|
assert PAGE_SCRIPT.findall(first.dashboard_html) == []
|
|
assert first.dashboard_html.count("<script src=") == 2
|
|
assert f'<script src="{first.feature_bundles["today-timer"].runtime_name}"></script>' in first.dashboard_html
|
|
assert f'<script src="{first.runtime_name}"></script>' in first.dashboard_html
|
|
assert first.runtime_name.startswith("runtime-")
|
|
assert first.runtime_name.endswith(".js")
|
|
assert len(gzip.compress(first.runtime_bytes, mtime=0)) <= 100 * 1024
|
|
|
|
changed_frontend = tmp_path / "frontend"
|
|
shutil.copytree(FRONTEND, changed_frontend)
|
|
(changed_frontend / "widgets.js").write_text(
|
|
(changed_frontend / "widgets.js").read_text() + "\n// changed\n"
|
|
)
|
|
|
|
changed = build_frontend(changed_frontend)
|
|
assert changed.runtime_name != first.runtime_name
|
|
assert changed.runtime_bytes != first.runtime_bytes
|
|
|
|
|
|
def test_product_workflows_are_stable_lazy_feature_chunks(tmp_path):
|
|
first = build_frontend(FRONTEND)
|
|
|
|
assert set(first.feature_bundles) == {
|
|
"comment-actions", "issue-capture", "pull-workflow", "push-notifications", "device-setup",
|
|
"today-timer",
|
|
}
|
|
assert (
|
|
f'<script src="{first.feature_bundles["today-timer"].runtime_name}"></script>\n'
|
|
f'<script src="{first.runtime_name}"></script>'
|
|
) in first.dashboard_html
|
|
capture = first.feature_bundles["issue-capture"]
|
|
pull_workflow = first.feature_bundles["pull-workflow"]
|
|
assert b"function createIssueCapture" not in first.runtime_bytes
|
|
assert b"function createIssueCapture" in capture.runtime_bytes
|
|
assert b"function createPullSheet" not in first.runtime_bytes
|
|
assert b"function createReviewController" not in first.runtime_bytes
|
|
assert b"function createPullSheet" in pull_workflow.runtime_bytes
|
|
assert b"function createReviewController" in pull_workflow.runtime_bytes
|
|
assert len(first.runtime_gzip_bytes) <= 95 * 1024
|
|
assert f'name="stackchain-feature-issue-capture" content="{capture.runtime_name}"' in first.dashboard_html
|
|
assert f'name="stackchain-feature-pull-workflow" content="{pull_workflow.runtime_name}"' in first.dashboard_html
|
|
assert f"BASE + '{capture.runtime_name}'" in first.service_worker_source
|
|
assert f"BASE + '{pull_workflow.runtime_name}'" in first.service_worker_source
|
|
|
|
changed_frontend = tmp_path / "frontend"
|
|
shutil.copytree(FRONTEND, changed_frontend)
|
|
capture_source = changed_frontend / "create-issue-sheet.js"
|
|
capture_source.write_text(capture_source.read_text() + "\n// feature-only change\n")
|
|
|
|
changed = build_frontend(changed_frontend)
|
|
assert changed.runtime_name == first.runtime_name
|
|
assert changed.feature_bundles["issue-capture"].runtime_name != capture.runtime_name
|
|
|
|
pull_source = changed_frontend / "pull-sheet.js"
|
|
pull_source.write_text(pull_source.read_text() + "\n// pull-workflow-only change\n")
|
|
pull_changed = build_frontend(changed_frontend)
|
|
assert pull_changed.runtime_name == first.runtime_name
|
|
assert pull_changed.feature_bundles["pull-workflow"].runtime_name != pull_workflow.runtime_name
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_feature_chunk_is_immutable_and_rejects_unknown_revision():
|
|
build = build_frontend(FRONTEND)
|
|
capture = build.feature_bundles["issue-capture"]
|
|
|
|
request = Request(
|
|
{"type": "http", "method": "GET", "path": f"/{capture.runtime_name}",
|
|
"headers": [(b"accept-encoding", b"identity")]}
|
|
)
|
|
response = await feature_bundle("issue-capture", capture.runtime_digest, request)
|
|
|
|
assert response.body == capture.runtime_bytes
|
|
assert response.headers["cache-control"] == "public, max-age=31536000, immutable"
|
|
with pytest.raises(Exception) as rejected:
|
|
await feature_bundle("issue-capture", "missing", request)
|
|
assert rejected.value.status_code == 404
|
|
|
|
|
|
def test_css_only_release_gets_a_new_shell_generation(tmp_path):
|
|
first = build_frontend(FRONTEND)
|
|
changed_frontend = tmp_path / "frontend"
|
|
shutil.copytree(FRONTEND, changed_frontend)
|
|
(changed_frontend / "dashboard.css").write_text(
|
|
(changed_frontend / "dashboard.css").read_text() + "\n/* release change */\n"
|
|
)
|
|
|
|
changed = build_frontend(changed_frontend)
|
|
|
|
assert changed.runtime_name == first.runtime_name
|
|
assert changed.runtime_bytes == first.runtime_bytes
|
|
assert changed.shell_digest != first.shell_digest
|
|
assert f"const CACHE = 'stackchain-dashboard-shell-{changed.shell_digest}';" in (
|
|
changed.service_worker_source
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"asset",
|
|
[
|
|
"manifest.webmanifest",
|
|
"icons/stackchain-192.png",
|
|
"icons/stackchain-512.png",
|
|
"background-issue-sync.js",
|
|
"service-worker.js",
|
|
],
|
|
)
|
|
def test_every_offline_shell_asset_versions_the_shell_generation(tmp_path, asset):
|
|
first = build_frontend(FRONTEND)
|
|
changed_frontend = tmp_path / "frontend"
|
|
shutil.copytree(FRONTEND, changed_frontend)
|
|
path = changed_frontend / asset
|
|
path.write_bytes(path.read_bytes() + b"\nrelease-change")
|
|
|
|
changed = build_frontend(changed_frontend)
|
|
|
|
assert changed.shell_digest != first.shell_digest
|
|
|
|
|
|
def test_legacy_cache_marker_is_normalized_out_of_build_identity(tmp_path):
|
|
first = build_frontend(FRONTEND)
|
|
changed_frontend = tmp_path / "frontend"
|
|
shutil.copytree(FRONTEND, changed_frontend)
|
|
worker = changed_frontend / "service-worker.js"
|
|
worker.write_text(
|
|
worker.read_text().replace(
|
|
"const CACHE = 'stackchain-dashboard-shell-v88';",
|
|
"const CACHE = 'stackchain-dashboard-shell-v999';",
|
|
)
|
|
)
|
|
|
|
changed = build_frontend(changed_frontend)
|
|
|
|
assert changed.shell_digest == first.shell_digest
|
|
assert changed.service_worker_source == first.service_worker_source
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_runtime_is_transferred_as_gzip_when_the_client_accepts_it():
|
|
transport = httpx.ASGITransport(app=main.app)
|
|
|
|
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
|
response = await client.get(
|
|
f"/{main.FRONTEND_BUILD.runtime_name}",
|
|
headers={"Accept-Encoding": "gzip"},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.headers["content-encoding"] == "gzip"
|
|
assert response.headers["vary"] == "Accept-Encoding"
|
|
assert int(response.headers["content-length"]) == len(main.FRONTEND_BUILD.runtime_gzip_bytes)
|
|
assert response.content == main.FRONTEND_BUILD.runtime_bytes
|
|
assert response.headers["cache-control"] == "public, max-age=31536000, immutable"
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_runtime_route_returns_the_precomputed_gzip_representation():
|
|
build = build_frontend(FRONTEND)
|
|
request = Request(
|
|
{"type": "http", "method": "GET", "path": f"/{build.runtime_name}",
|
|
"headers": [(b"accept-encoding", b"gzip")]}
|
|
)
|
|
|
|
response = await runtime_bundle(build.runtime_digest, request)
|
|
|
|
assert response.body == build.runtime_gzip_bytes
|
|
assert response.headers["content-encoding"] == "gzip"
|
|
assert response.headers["content-length"] == str(len(build.runtime_gzip_bytes))
|
|
assert response.headers["vary"] == "Accept-Encoding"
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_runtime_requests_bypass_dynamic_gzip_compression(monkeypatch):
|
|
class UnexpectedDynamicCompression:
|
|
def __init__(self, *args, **kwargs):
|
|
raise AssertionError("immutable runtime entered dynamic compression")
|
|
|
|
monkeypatch.setattr("src.compression.GZipResponder", UnexpectedDynamicCompression)
|
|
transport = httpx.ASGITransport(app=main.app)
|
|
|
|
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
|
response = await client.get(
|
|
f"/{main.FRONTEND_BUILD.runtime_name}",
|
|
headers={"Accept-Encoding": "gzip"},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.headers["content-encoding"] == "gzip"
|
|
|
|
|
|
@pytest.mark.anyio
|
|
@pytest.mark.parametrize("accept_encoding", ["identity", "gzip;q=0, identity;q=1"])
|
|
async def test_runtime_stays_unencoded_when_the_client_declines_gzip(accept_encoding):
|
|
transport = httpx.ASGITransport(app=main.app)
|
|
|
|
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
|
response = await client.get(
|
|
f"/{main.FRONTEND_BUILD.runtime_name}",
|
|
headers={"Accept-Encoding": accept_encoding},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert "content-encoding" not in response.headers
|
|
assert int(response.headers["content-length"]) == len(main.FRONTEND_BUILD.runtime_bytes)
|
|
assert response.content == main.FRONTEND_BUILD.runtime_bytes
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_dashboard_shell_is_compressed_without_losing_response_policies():
|
|
transport = httpx.ASGITransport(app=main.app)
|
|
|
|
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
|
page = await client.get("/", headers={"Accept-Encoding": "gzip"})
|
|
stylesheet = await client.get(
|
|
"/static/dashboard.css", headers={"Accept-Encoding": "gzip"}
|
|
)
|
|
health = await client.get("/healthz", headers={"Accept-Encoding": "gzip"})
|
|
|
|
assert page.headers["content-encoding"] == "gzip"
|
|
assert page.headers["cache-control"] == "no-cache"
|
|
assert page.headers["content-security-policy"]
|
|
assert main.FRONTEND_BUILD.runtime_name in page.text
|
|
assert stylesheet.headers["content-encoding"] == "gzip"
|
|
assert "mobile-task-dock" in stylesheet.text
|
|
assert health.json() == {"service": "stackchain-dashboard", "status": "ok"}
|
|
assert "content-encoding" not in health.headers
|
|
|
|
|
|
def test_offline_shell_precaches_exact_runtime_without_superseded_page_modules():
|
|
build = build_frontend(FRONTEND)
|
|
|
|
assert f"BASE + '{build.runtime_name}'" in build.service_worker_source
|
|
for source in build.page_sources:
|
|
if source == "static/background-issue-sync.js":
|
|
continue
|
|
assert f"BASE + '{source}'" not in build.service_worker_source
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_runtime_asset_is_immutable_while_html_and_worker_revalidate():
|
|
build = build_frontend(FRONTEND)
|
|
request = Request(
|
|
{"type": "http", "method": "GET", "path": f"/{build.runtime_name}",
|
|
"headers": [(b"accept-encoding", b"identity")]}
|
|
)
|
|
|
|
html = await dashboard()
|
|
bundle = await runtime_bundle(build.runtime_digest, request)
|
|
worker = await service_worker()
|
|
|
|
assert build.runtime_name in html
|
|
assert bundle.body == build.runtime_bytes
|
|
assert bundle.headers["cache-control"] == "public, max-age=31536000, immutable"
|
|
assert worker.headers["cache-control"] == "no-cache"
|
|
assert build.runtime_name.encode() in worker.body
|