167 lines
5.8 KiB
Python
167 lines
5.8 KiB
Python
|
|
"""The checks that read a page's stylesheet and images from a live server."""
|
||
|
|
|
||
|
|
from pathlib import Path
|
||
|
|
import zlib
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from mews.fetch import Fetcher
|
||
|
|
from mews.lint import validate_bytes
|
||
|
|
|
||
|
|
CANONICAL = (Path(__file__).resolve().parent.parent / "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
|