diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1cc1ca6..003f7eb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -77,7 +77,7 @@ jobs: import os, re, sys sys.path.insert(0, os.getcwd()) import html_generator - page = html_generator.get_html_template("ci", {"items": []}, "1") + page = html_generator.get_html_template("ci", {"items": []}, "1", inline_assets=True) bodies = re.findall(r"]*\bsrc=)[^>]*>(.*?)", page, re.S) assert bodies, "no inline " + else: + css_tag = f'' + js_tag = f'' + # 3. Inject Data wrapped_json = f"/*__JSON_START__*/\nlet fullManifest = {json_str};\n/*__JSON_END__*/" - # 4. Final Replacements - final_html = html_template \ - .replace("__TITLE__", str(title)) \ - .replace("__NODE_ID__", str(node_id)) \ - .replace("__STANDALONE_FLAG__", "true" if standalone else "false") \ - .replace("let fullManifest = __JSON_DATA__;", wrapped_json) \ - .replace("__CSS_CONTENT__", css_content) \ - .replace("__JS_CONTENT__", js_content) + # 4. Final Replacements (asset tags LAST so resource text is never placeholder-substituted) + final_html = html_template .replace("__TITLE__", str(title)) .replace("__NODE_ID__", str(node_id)) .replace("__STANDALONE_FLAG__", "true" if standalone else "false") .replace("let fullManifest = __JSON_DATA__;", wrapped_json) .replace("__CSS_TAG__", css_tag) .replace("__JS_TAG__", js_tag) - return final_html \ No newline at end of file + return final_html diff --git a/resources/template.html b/resources/template.html index 8afff8e..d8ffac8 100644 --- a/resources/template.html +++ b/resources/template.html @@ -4,10 +4,7 @@ USCG - __TITLE__ - + __CSS_TAG__ @@ -930,16 +927,13 @@ + __JS_TAG__ " in page + + +def test_shell_references_bundles_with_hash(): + page = _shell(standalone=True) + jh = hg.get_dashboard_bundle("js").hash + ch = hg.get_dashboard_bundle("css").hash + tag = f'' + assert page.count(tag) == 1 + assert f'' in page + assert "/* --- logic_" not in page + size = len(page.encode("utf-8")) + print(f"measured shell size: {size} bytes") + assert size < 100 * 1024 + assert page.index("const TARGET_NODE_ID") < page.index(tag) < page.index('id="upscale-modal-overlay"') + for tok in TOKENS: + assert tok not in page + + +def test_inline_manifest_implies_inline_assets_and_overrides(): + m = {"items": []} + implied = hg.get_html_template("t", m, "1", inline_manifest=True) + assert "/* INJECTED LOGIC */" in implied and "dashboard_bundle_js" not in implied + assert "/* INJECTED LOGIC */" in hg.get_html_template("t", m, "1", inline_assets=True) + off = hg.get_html_template("t", m, "1", inline_manifest=True, inline_assets=False) + assert "/* INJECTED LOGIC */" not in off and "dashboard_bundle_js" in off + + +def test_env_forces_inline(monkeypatch): + monkeypatch.setenv("USCG_INLINE_DASHBOARD_ASSETS", "1") + page = _shell() + assert "/* INJECTED LOGIC */" in page and "dashboard_bundle_js" not in page + + +def test_hash_follows_resource_changes(tmp_path, monkeypatch): + dst = tmp_path / "resources" + shutil.copytree(hg.RESOURCE_DIR, dst) + monkeypatch.setattr(hg, "RESOURCE_DIR", str(dst)) + hg._bundle_cache.clear() + try: + b1 = hg.get_dashboard_bundle("js") + assert hg.get_dashboard_bundle("js") is b1 # unchanged signature: cached object + url1 = hg.bundle_url("js") + init = dst / "logic_init.js" + st = init.stat() + os.utime(init, ns=(st.st_atime_ns, st.st_mtime_ns + 5_000_000_000)) # mtime only + b2 = hg.get_dashboard_bundle("js") + assert b2 is not b1 and b2.hash == b1.hash + with open(init, "ab") as f: + f.write(b"\n// hot reload edit\n") + b3 = hg.get_dashboard_bundle("js") + assert b3.hash != b1.hash + assert hg.bundle_url("js") != url1 and b3.hash in hg.bundle_url("js") + finally: + hg._bundle_cache.clear() + + +def test_gzip_round_trip_and_deterministic(): + for kind in ("js", "css"): + b = hg.get_dashboard_bundle(kind) + assert gzip.decompress(b.gzip) == b.body + assert b.gzip == gzip.compress(b.body, 9, mtime=0) + assert b.body == b.text.encode("utf-8") + assert len(b.gzip) < len(b.body) + + +def test_unknown_kind_raises(): + with pytest.raises(KeyError): + hg.get_dashboard_bundle("../x") + + +def test_bundle_response_headers(): + b = hg.get_dashboard_bundle("js") + st, body, h = run_api.bundle_response(b, b.hash, None, "gzip, br") + assert st == 200 and body == b.gzip + assert h["Content-Encoding"] == "gzip" + assert h["Cache-Control"] == "public, max-age=31536000, immutable" + assert h["ETag"] == f'W/"{b.hash}"' + assert h["Vary"] == "Accept-Encoding" and h["X-Content-Type-Options"] == "nosniff" + st, body, h = run_api.bundle_response(b, b.hash, None, "") + assert st == 200 and body == b.body and "Content-Encoding" not in h + assert h["Vary"] == "Accept-Encoding" and h["X-Content-Type-Options"] == "nosniff" + for v in ("bogus", "", None): + _, _, h = run_api.bundle_response(b, v, None, "gzip") + assert h["Cache-Control"] == "no-cache" + for inm in (f'W/"{b.hash}"', f'"{b.hash}"', f'"zzz", W/"{b.hash}"'): + st, body, h = run_api.bundle_response(b, b.hash, inm, "gzip") + assert st == 304 and body is None and h["ETag"] == f'W/"{b.hash}"' + st, _, _ = run_api.bundle_response(b, b.hash, 'W/"other"', "gzip") + assert st == 200 + + +@pytest.mark.parametrize("v", ["../../etc/passwd", "logic_ui.js", "..\\..\\windows\\win.ini"]) +def test_v_is_never_a_path(v): + b = hg.get_dashboard_bundle("js") + st, body, h = run_api.bundle_response(b, v, None, "") + assert st == 200 and body == b.body and h["Cache-Control"] == "no-cache" + + +def test_routes_are_fixed_paths(): + with open(os.path.join(_ROOT, "__init__.py"), encoding="utf-8") as f: + src = f.read() + assert '"/config_tester/dashboard_bundle_js"' in src + assert '"/config_tester/dashboard_bundle_css"' in src + for path in re.findall(r'routes\.(?:get|post|put|delete)\(\s*"([^"]+)"', src): + assert "{" not in path, path + m = re.search(r"def _bundle_http\(.*?(?=\n@server)", src, re.S) + assert m + handlers = [m.group(0)] + for fn in ("dashboard_bundle_js", "dashboard_bundle_css"): + handlers.append(re.search(r"async def %s\(.*?(?=\n\n\n|\Z)" % fn, src, re.S).group(0)) + for body in handlers: + for banned in ("match_info", "open(", "FileResponse", "enable_compression"): + assert banned not in body, banned + + +@pytest.mark.skipif(NODE is None, reason="node not installed") +def test_bundle_parses_and_composes_with_globals(): + page = _shell(standalone=True) + globals_js = re.search(r"", page).group(1) + js = hg.get_dashboard_bundle("js").text + with tempfile.TemporaryDirectory() as d: + for name, code in (("bundle.js", js), ("all.js", globals_js + "\n" + js)): + path = os.path.join(d, name) + with open(path, "w", encoding="utf-8") as f: + f.write(code) + out = subprocess.run([NODE, "--check", path], capture_output=True, timeout=60) + assert out.returncode == 0, out.stderr.decode() diff --git a/tests/test_docs_consistency.py b/tests/test_docs_consistency.py index e1230a3..dd82fda 100644 --- a/tests/test_docs_consistency.py +++ b/tests/test_docs_consistency.py @@ -165,7 +165,7 @@ def test_events_documented(): def test_js_files_order(): src = _read("html_generator.py") - m = re.search(r"js_files\s*=\s*\[(.*?)\n\s*\]", src, re.S) + m = re.search(r"(?:JS_FILES|js_files)\s*=\s*[\[(](.*?)\n\s*[\])]", src, re.S) assert m files = re.findall(r'^\s*"([\w\-]+\.js)"', m.group(1), re.M) assert files and files[-1] == "logic_standalone.js" diff --git a/tests/test_standalone_html.py b/tests/test_standalone_html.py index a3c7e8d..f66b923 100644 --- a/tests/test_standalone_html.py +++ b/tests/test_standalone_html.py @@ -75,9 +75,24 @@ def test_escaped_manifest_still_parses_with_dashboard_js_extraction(): @pytest.mark.parametrize("standalone", [True, False]) def test_concatenated_script_parses_as_one_script(standalone): page = _page(standalone=standalone) - scripts = re.findall(r"", page) + scripts = re.findall(r"]*\bsrc=)[^>]*>([\s\S]*?)", page) assert scripts, "no inline script found" - js = max(scripts, key=len) + js = "\n;\n".join(scripts) + with tempfile.TemporaryDirectory() as d: + path = os.path.join(d, "page.js") + with open(path, "w", encoding="utf-8") as f: + f.write(js) + out = subprocess.run([NODE, "--check", path], capture_output=True, timeout=60) + assert out.returncode == 0, out.stderr.decode() + + +@pytest.mark.skipif(NODE is None, reason="node not installed") +def test_shell_script_parses_as_one_script(): + """The shell's globals script + the bundle route's JS share one global lexical scope.""" + page = _shell(standalone=True) + scripts = re.findall(r"]*\bsrc=)[^>]*>([\s\S]*?)", page) + assert scripts + js = "\n;\n".join(scripts) + "\n;\n" + html_generator.get_dashboard_bundle("js").text with tempfile.TemporaryDirectory() as d: path = os.path.join(d, "page.js") with open(path, "w", encoding="utf-8") as f: @@ -106,7 +121,7 @@ def test_shell_inlines_no_item_data(): def test_shell_size_does_not_grow_with_the_session(): big = [{"id": i, "note": "x" * 2500, "positive": "p" * 300} for i in range(1000)] - assert len(_shell(big).encode("utf-8")) < 560 * 1024 + assert len(_shell(big).encode("utf-8")) < 100 * 1024 assert len(_shell(big)) == len(_shell([])) @@ -115,7 +130,8 @@ def test_shell_placeholders_all_replaced(): assert 'const TARGET_NODE_ID = "42";' in page assert "const USCG_STANDALONE = true" in page assert 'id="session-input" class="session-name-input" type="text" value="my-sess"' in page - for token in ("__TITLE__", "__NODE_ID__", "__STANDALONE_FLAG__", "__CSS_CONTENT__", "__JS_CONTENT__"): + for token in ("__TITLE__", "__NODE_ID__", "__STANDALONE_FLAG__", "__CSS_CONTENT__", "__JS_CONTENT__", + "__CSS_TAG__", "__JS_TAG__"): assert token not in page @@ -133,18 +149,6 @@ def test_shell_parses_with_dashboard_js_extraction_as_lazy(): assert out.stdout.decode() == '[true,0,"abc"]' -@pytest.mark.skipif(NODE is None, reason="node not installed") -def test_shell_script_parses_as_one_script(): - scripts = re.findall(r"", _shell(standalone=True)) - js = max(scripts, key=len) - with tempfile.TemporaryDirectory() as d: - path = os.path.join(d, "page.js") - with open(path, "w", encoding="utf-8") as f: - f.write(js) - out = subprocess.run([NODE, "--check", path], capture_output=True, timeout=60) - assert out.returncode == 0, out.stderr.decode() - - def test_inline_manifest_flag_keeps_the_old_behaviour(): page = _page() assert "__uscg_lazy__" not in page.split("/*__JSON_START__*/")[1].split("/*__JSON_END__*/")[0]