mirror of
https://github.com/Dasharo/docs.git
synced 2026-06-13 10:16:57 -07:00
tests/: add tos-gate test and CI workflow
29 tests across four classes: - TestTosGatedDownloads (9) - macro unit tests: URL absent from HTML, base64/JSON roundtrip, group format encoding, checkbox attributes, prose_section_id, tos_url override - TestTosCheckbox (4) - legacy macro: element presence, section ID, URL defaults/override - TestTosGateJs (7) - JS file: function defined, noreferrer absent from link template but warning comment present, data-prose-group selector present, group branch present, prose hidden on uncheck - TestStaticFiles (9) - no plain dl.3mdeb.com links, correct macro usage in variants/ files, tos-gate.js in mkdocs.yml, permalink: true, referrer meta in overrides/main.html Signed-off-by: Artur Raglis <artur.raglis@3mdeb.com>
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: ToS gate tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
paths:
|
||||
- main.py
|
||||
- docs/javascripts/tos-gate.js
|
||||
- overrides/main.html
|
||||
- mkdocs.yml
|
||||
- docs/**
|
||||
- tests/test_tos_gate.py
|
||||
- requirements.txt
|
||||
pull_request:
|
||||
paths:
|
||||
- main.py
|
||||
- docs/javascripts/tos-gate.js
|
||||
- overrides/main.html
|
||||
- mkdocs.yml
|
||||
- docs/**
|
||||
- tests/test_tos_gate.py
|
||||
- requirements.txt
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- name: Install dependencies
|
||||
run: pip install -r requirements.txt
|
||||
- name: Run ToS gate tests
|
||||
run: pytest tests/test_tos_gate.py -v
|
||||
@@ -36,27 +36,6 @@ def define_env(env):
|
||||
'</div>'
|
||||
)
|
||||
|
||||
@env.macro
|
||||
def tos_checkbox(section_id, tos_url="https://www.dasharo.com/pages/terms/"):
|
||||
"""Render a ToS acceptance checkbox that reveals the section with the given id."""
|
||||
checkbox_id = f"tos-cb-{section_id}"
|
||||
onchange = (
|
||||
f"document.getElementById('{section_id}').style.display="
|
||||
f"this.checked?'':'none'"
|
||||
)
|
||||
return (
|
||||
'<div class="tos-gate">'
|
||||
f'<label class="tos-gate__label" for="{checkbox_id}">'
|
||||
f'<input type="checkbox" id="{checkbox_id}" '
|
||||
f'class="tos-gate__checkbox" onchange="{onchange}"> '
|
||||
'I confirm that I have read and agree to the '
|
||||
f'<a href="{tos_url}" target="_blank" rel="noopener noreferrer">'
|
||||
'Dasharo Terms of Service</a> and all applicable open-source '
|
||||
'licensing terms governing this firmware release.'
|
||||
'</label>'
|
||||
'</div>'
|
||||
)
|
||||
|
||||
@env.macro
|
||||
def subscribe_form(list_id, btn_text):
|
||||
return (
|
||||
|
||||
@@ -46,3 +46,4 @@ watchdog==4.0.2
|
||||
markdown-grid-tables==0.4.0
|
||||
mkdocs-macros-plugin==1.3.7
|
||||
mkdocs-include-markdown-plugin==7.1.5
|
||||
pytest==8.3.2
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
"""
|
||||
Red/green tests for the ToS gate feature (Tasks 0 & 1).
|
||||
|
||||
Covers:
|
||||
- tos_gated_downloads macro unit tests
|
||||
- tos-gate.js static assertions
|
||||
- Markdown source files: no plain dl.3mdeb.com links in gated sections
|
||||
- mkdocs.yml / overrides configuration
|
||||
"""
|
||||
import base64
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO_ROOT = Path(__file__).parent.parent
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _load_macros():
|
||||
"""Collect macro functions from main.py by simulating define_env."""
|
||||
sys.path.insert(0, str(REPO_ROOT))
|
||||
import main # noqa: PLC0415
|
||||
|
||||
macros: dict = {}
|
||||
|
||||
class _Env:
|
||||
def macro(self, fn):
|
||||
macros[fn.__name__] = fn
|
||||
return fn
|
||||
|
||||
main.define_env(_Env())
|
||||
return macros
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def macros():
|
||||
return _load_macros()
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def tos_gate_js():
|
||||
return (REPO_ROOT / "docs/javascripts/tos-gate.js").read_text()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# tos_gated_downloads – unit tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestTosGatedDownloads:
|
||||
def test_url_not_in_html_source(self, macros):
|
||||
"""URLs must be absent from rendered HTML — only encoded in data-payload."""
|
||||
url = "https://dl.3mdeb.com/secret/firmware.zip"
|
||||
html = macros["tos_gated_downloads"]("s", [{"label": "f", "url": url}])
|
||||
assert url not in html
|
||||
|
||||
def test_payload_is_valid_base64_json(self, macros):
|
||||
files = [{"label": "sha256", "url": "https://dl.3mdeb.com/file.sha256"}]
|
||||
html = macros["tos_gated_downloads"]("s", files)
|
||||
m = re.search(r'data-payload="([^"]+)"', html)
|
||||
assert m, "data-payload attribute missing from output"
|
||||
decoded = json.loads(base64.b64decode(m.group(1)))
|
||||
assert decoded == files
|
||||
|
||||
def test_group_format_encodes_correctly(self, macros):
|
||||
files = [{"group": "Raw image", "note": "(fw.rom)", "items": [
|
||||
{"label": "sha256", "url": "https://dl.3mdeb.com/fw.sha256"},
|
||||
]}]
|
||||
html = macros["tos_gated_downloads"]("s", files)
|
||||
assert "dl.3mdeb.com" not in html
|
||||
m = re.search(r'data-payload="([^"]+)"', html)
|
||||
decoded = json.loads(base64.b64decode(m.group(1)))
|
||||
assert decoded[0]["group"] == "Raw image"
|
||||
assert decoded[0]["note"] == "(fw.rom)"
|
||||
assert decoded[0]["items"][0]["url"] == "https://dl.3mdeb.com/fw.sha256"
|
||||
|
||||
def test_checkbox_required_attributes(self, macros):
|
||||
html = macros["tos_gated_downloads"]("my-section", [])
|
||||
assert 'data-section="my-section"' in html
|
||||
assert 'onchange="revealGated(this)"' in html
|
||||
assert 'class="tos-gate__checkbox"' in html
|
||||
|
||||
def test_section_content_div_present(self, macros):
|
||||
html = macros["tos_gated_downloads"]("my-section", [])
|
||||
assert 'id="my-section"' in html
|
||||
assert "tos-gate-content" in html
|
||||
|
||||
def test_prose_section_id_emits_data_attribute(self, macros):
|
||||
html = macros["tos_gated_downloads"]("s", [], prose_section_id="my-prose")
|
||||
assert 'data-prose-section="my-prose"' in html
|
||||
|
||||
def test_no_prose_section_id_omits_data_attribute(self, macros):
|
||||
html = macros["tos_gated_downloads"]("s", [])
|
||||
assert "data-prose-section" not in html
|
||||
|
||||
def test_default_tos_url(self, macros):
|
||||
html = macros["tos_gated_downloads"]("s", [])
|
||||
assert 'href="https://www.dasharo.com/pages/terms/"' in html
|
||||
|
||||
def test_custom_tos_url_override(self, macros):
|
||||
html = macros["tos_gated_downloads"]("s", [], tos_url="https://example.com/tos")
|
||||
assert 'href="https://example.com/tos"' in html
|
||||
assert "dasharo.com/pages/terms" not in html
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# tos-gate.js – static assertions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestTosGateJs:
|
||||
def test_file_exists(self):
|
||||
assert (REPO_ROOT / "docs/javascripts/tos-gate.js").exists()
|
||||
|
||||
def test_reveal_gated_function_defined(self, tos_gate_js):
|
||||
assert "function revealGated" in tos_gate_js
|
||||
|
||||
def test_noreferrer_absent_from_link_template(self, tos_gate_js):
|
||||
"""noreferrer on injected download links would strip the Referer header,
|
||||
breaking the MinIO bucket policy gate."""
|
||||
for line in tos_gate_js.splitlines():
|
||||
if "md-button" in line:
|
||||
assert "noreferrer" not in line, (
|
||||
f"noreferrer must not appear in the injected link template: {line!r}"
|
||||
)
|
||||
|
||||
def test_noreferrer_warning_comment_present(self, tos_gate_js):
|
||||
"""The intentional absence of noreferrer should be documented."""
|
||||
assert "noreferrer" in tos_gate_js
|
||||
|
||||
def test_prose_group_queryselector_present(self, tos_gate_js):
|
||||
"""Multiple prose divs must be toggled via data-prose-group selector."""
|
||||
assert "data-prose-group" in tos_gate_js
|
||||
|
||||
def test_group_rendering_branch_present(self, tos_gate_js):
|
||||
assert "f.group" in tos_gate_js
|
||||
|
||||
def test_prose_elements_toggled_on_uncheck(self, tos_gate_js):
|
||||
"""Unchecking must hide prose sections, not just clear download buttons."""
|
||||
assert 'display = "none"' in tos_gate_js or "display='none'" in tos_gate_js or \
|
||||
'style.display = "none"' in tos_gate_js
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Static file tests – variant markdown files (parameterized, opt-in)
|
||||
#
|
||||
# Tests run against every *.md under docs/variants/.
|
||||
# Files that do not use tos_gated_downloads are skipped automatically,
|
||||
# so adding the gate to a new variant is covered without touching this file.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_VARIANT_MD_FILES = sorted((REPO_ROOT / "docs/variants").rglob("*.md"))
|
||||
_VARIANT_IDS = [str(f.relative_to(REPO_ROOT)) for f in _VARIANT_MD_FILES]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("md_file", _VARIANT_MD_FILES, ids=_VARIANT_IDS)
|
||||
def test_no_plain_dl_links_in_gated_file(md_file):
|
||||
"""Any file using tos_gated_downloads must not expose dl.3mdeb.com URLs
|
||||
as plain markdown links (inline or reference-style)."""
|
||||
content = md_file.read_text()
|
||||
if "tos_gated_downloads" not in content:
|
||||
pytest.skip("file does not use tos_gated_downloads")
|
||||
assert not re.search(r"^\[.*\]:\s+https://dl\.3mdeb\.com/", content, re.MULTILINE), \
|
||||
f"Reference-style link to dl.3mdeb.com found in {md_file.name}"
|
||||
assert not re.search(r"\]\(https://dl\.3mdeb\.com/", content), \
|
||||
f"Inline markdown link to dl.3mdeb.com found in {md_file.name}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("md_file", _VARIANT_MD_FILES, ids=_VARIANT_IDS)
|
||||
def test_prose_section_id_has_matching_div(md_file):
|
||||
"""Any file using prose_section_id must have at least one data-prose-group div."""
|
||||
content = md_file.read_text()
|
||||
if "prose_section_id" not in content:
|
||||
pytest.skip("file does not use prose_section_id")
|
||||
assert "data-prose-group" in content, \
|
||||
f"prose_section_id used but no data-prose-group div found in {md_file.name}"
|
||||
|
||||
|
||||
def test_at_least_one_variant_uses_tos_gated_downloads():
|
||||
"""Sanity check: ensure the feature is actually in use and hasn't been
|
||||
accidentally removed from all variant files."""
|
||||
gated = [f for f in _VARIANT_MD_FILES if "tos_gated_downloads" in f.read_text()]
|
||||
assert gated, "No variant file uses tos_gated_downloads — feature may have been removed"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Static file tests – infrastructure configuration
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestInfrastructure:
|
||||
def test_mkdocs_yml_includes_tos_gate_js(self):
|
||||
content = (REPO_ROOT / "mkdocs.yml").read_text()
|
||||
assert "javascripts/tos-gate.js" in content
|
||||
|
||||
def test_mkdocs_yml_permalink_enabled(self):
|
||||
content = (REPO_ROOT / "mkdocs.yml").read_text()
|
||||
assert "permalink: true" in content
|
||||
|
||||
def test_overrides_main_html_referrer_meta(self):
|
||||
content = (REPO_ROOT / "overrides/main.html").read_text()
|
||||
assert 'name="referrer"' in content
|
||||
assert "strict-origin-when-cross-origin" in content
|
||||
Reference in New Issue
Block a user