validator/ holds mewslint (lint, css, fetch, DTD/CSS data); directory/ holds the mews.page service (app, db, check, pages, cli) and depends on validator/; quickstart/ holds mews-quickstart and md2mews.py for third parties who want to create Mews content without cloning the repo; spec/ consolidates doc/ and dtd/ with the canonical mews-0.1.css; site/ becomes a Pelican project with the forward-facing pages as Markdown, the check form kept as a raw HTML snippet. Root pyproject.toml is now a virtual uv workspace over validator, directory and quickstart, with cross-cutting tests that span more than one of them staying at the repo root. Makefile and flake.nix updated to match.
166 lines
5.8 KiB
Python
166 lines
5.8 KiB
Python
"""The checks that read a page's stylesheet and images from a live server."""
|
|
|
|
from importlib import resources
|
|
import zlib
|
|
|
|
import pytest
|
|
|
|
from mews.fetch import Fetcher
|
|
from mews.lint import validate_bytes
|
|
|
|
CANONICAL = resources.files("mews.data").joinpath("mews-0.1.css").read_bytes()
|
|
FONT_FACE = b"""@font-face {
|
|
font-family: "Atkinson Hyperlegible";
|
|
src: url(/fonts/atkinson.woff2) format("woff2");
|
|
}
|
|
|
|
"""
|
|
PNG = (
|
|
b"\x89PNG\r\n\x1a\n"
|
|
b"\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00"
|
|
b"\x1f\x15\xc4\x89"
|
|
b"\x00\x00\x00\x00IEND\xaeB`\x82"
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def check(site):
|
|
"""Validate a page served by the local server, reading its sub-resources."""
|
|
|
|
def run(page: bytes, path: str = "/index.html"):
|
|
url = site.add(path, page, headers={"ETag": '"v1"'})
|
|
with Fetcher(allow_loopback=True) as fetcher:
|
|
return validate_bytes(page, url=url, fetcher=fetcher)
|
|
|
|
return run
|
|
|
|
|
|
def codes(report):
|
|
return [finding.code for finding in report.findings]
|
|
|
|
|
|
def test_an_unmodified_stylesheet_copy_passes(site, check, conforming):
|
|
site.add("/mews-0.1.css", CANONICAL)
|
|
report = check(conforming())
|
|
assert "stylesheet-modified" not in codes(report)
|
|
|
|
|
|
def test_an_added_font_face_passes(site, check, conforming):
|
|
site.add("/mews-0.1.css", FONT_FACE + CANONICAL)
|
|
site.add("/fonts/atkinson.woff2", b"font")
|
|
report = check(conforming())
|
|
assert "stylesheet-modified" not in codes(report)
|
|
assert "font-offsite" not in codes(report)
|
|
|
|
|
|
def test_a_changed_stylesheet_fails(site, check, conforming):
|
|
site.add("/mews-0.1.css", CANONICAL.replace(b"#faf8f3", b"#ffffff"))
|
|
report = check(conforming())
|
|
failure = [f for f in report.findings if f.code == "stylesheet-modified"]
|
|
assert failure and failure[0].section == "5.2"
|
|
assert not report.conforms
|
|
|
|
|
|
def test_a_font_from_another_site_fails(site, check, conforming):
|
|
off = FONT_FACE.replace(b"/fonts/atkinson.woff2", b"https://fonts.example.net/a")
|
|
site.add("/mews-0.1.css", off + CANONICAL)
|
|
report = check(conforming())
|
|
assert "font-offsite" in codes(report)
|
|
assert not report.conforms
|
|
|
|
|
|
def test_an_import_in_the_stylesheet_fails(site, check, conforming):
|
|
site.add("/mews-0.1.css", b'@import url("x.css");\n' + CANONICAL)
|
|
report = check(conforming())
|
|
assert "stylesheet-import" in codes(report)
|
|
|
|
|
|
def test_a_missing_stylesheet_only_warns(site, check, conforming):
|
|
report = check(conforming())
|
|
warning = [f for f in report.findings if f.code == "stylesheet-unreachable"]
|
|
assert warning and warning[0].level == "should"
|
|
|
|
|
|
def test_a_stylesheet_on_another_site_fails(site, check, conforming):
|
|
site.add("/mews-0.1.css", CANONICAL)
|
|
report = check(conforming(stylesheet="https://cdn.example.net/mews-0.1.css"))
|
|
assert "stylesheet-offsite" in codes(report)
|
|
assert not report.conforms
|
|
|
|
|
|
def test_the_central_stylesheet_only_warns(site, check, conforming):
|
|
"""Linking mews.page's own copy is discouraged, not a failure."""
|
|
report = check(conforming(stylesheet="https://mews.page/mews-0.1.css"))
|
|
assert "stylesheet-offsite" not in codes(report)
|
|
canonical = [f for f in report.findings if f.code == "stylesheet-canonical"]
|
|
assert canonical and canonical[0].level == "should"
|
|
|
|
|
|
def test_a_cookie_on_the_stylesheet_fails(site, check, conforming):
|
|
site.add("/mews-0.1.css", CANONICAL, headers={"Set-Cookie": "a=1"})
|
|
report = check(conforming())
|
|
assert "cookie" in codes(report)
|
|
assert not report.conforms
|
|
|
|
|
|
# --- images ------------------------------------------------------------
|
|
|
|
|
|
def test_a_small_png_passes(site, check, conforming):
|
|
site.add("/mews-0.1.css", CANONICAL)
|
|
site.add("/a.png", PNG, headers={"Content-Type": "image/png"})
|
|
report = check(conforming(body='<p><img src="a.png" alt="A dot" /></p>'))
|
|
assert report.conforms
|
|
assert not report.warnings
|
|
|
|
|
|
def test_an_image_that_is_not_an_image_warns(site, check, conforming):
|
|
site.add("/mews-0.1.css", CANONICAL)
|
|
site.add("/a.png", b"<svg/>")
|
|
report = check(conforming(body='<p><img src="a.png" alt="x" /></p>'))
|
|
warning = [f for f in report.findings if f.code == "image-format"]
|
|
assert warning and warning[0].level == "should"
|
|
assert report.conforms
|
|
|
|
|
|
def test_a_large_image_warns(site, check, conforming):
|
|
site.add("/mews-0.1.css", CANONICAL)
|
|
site.add("/a.png", PNG + b"\x00" * 60_000)
|
|
report = check(conforming(body='<p><img src="a.png" alt="x" /></p>'))
|
|
assert "image-size" in codes(report)
|
|
|
|
|
|
def test_camera_metadata_warns(site, check, conforming):
|
|
site.add("/mews-0.1.css", CANONICAL)
|
|
site.add("/a.jpg", b"\xff\xd8\xff\xe1\x00\x16Exif\x00\x00" + b"\x00" * 100)
|
|
report = check(conforming(body='<p><img src="a.jpg" alt="x" /></p>'))
|
|
assert "image-metadata" in codes(report)
|
|
|
|
|
|
def test_a_missing_image_warns(site, check, conforming):
|
|
site.add("/mews-0.1.css", CANONICAL)
|
|
report = check(conforming(body='<p><img src="gone.png" alt="x" /></p>'))
|
|
assert "image-unreachable" in codes(report)
|
|
|
|
|
|
def test_only_the_first_twenty_images_are_read(site, check, conforming):
|
|
site.add("/mews-0.1.css", CANONICAL)
|
|
site.add("/a.png", PNG)
|
|
body = "".join(f'<p><img src="a.png?{i}" alt="x" /></p>' for i in range(25))
|
|
report = check(conforming(body=body))
|
|
assert "images-not-checked" in codes(report)
|
|
assert len([r for r in site.requests if r[0].startswith("/a.png")]) == 20
|
|
|
|
|
|
def test_a_compression_bomb_is_refused(site, conforming):
|
|
"""A short download that expands enormously is not a page."""
|
|
body = zlib.compress(b"x" * 10_000_000)
|
|
url = site.add(
|
|
"/big.html",
|
|
body,
|
|
headers={"Content-Encoding": "deflate", "Content-Type": "text/html"},
|
|
declare_length=False,
|
|
)
|
|
with Fetcher(allow_loopback=True) as fetcher:
|
|
response = fetcher.get(url)
|
|
assert response.truncated
|