Intermittently it segfaulted after every check had passed, leaving teardown order to Python; a profile released under a live page does that. Eight runs clean afterwards. The real Wonder Ink, closed six times by its window's close button in the lab, exited 0 each time. Co-Authored-By: Claude Opus 5.5 <[email protected]>
158 lines
6.2 KiB
Bash
Executable File
158 lines
6.2 KiB
Bash
Executable File
#!/bin/bash
|
|
# Does Wonder Ink open the worksheets it should and refuse the files it
|
|
# should not?
|
|
#
|
|
# The download-to-tab workflow is driven by hand against a running
|
|
# session; this is the part of it that can be asserted rather than looked
|
|
# at — which local files the shell will load, and which it will not.
|
|
#
|
|
# It runs inside the lab, because the classroom's own PyQt6 is there and
|
|
# the development host has no Qt at all. It imports the *installed*
|
|
# command, so it is testing what the package put on the machine.
|
|
set -uo pipefail
|
|
|
|
NAME="${LAB_NAME:-classroom-v2-lab}"
|
|
|
|
docker exec -i \
|
|
-e QT_QPA_PLATFORM=offscreen \
|
|
-e QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox --disable-gpu" \
|
|
-e XDG_RUNTIME_DIR=/tmp/pdfcheck-rt \
|
|
-e HOME=/root \
|
|
"$NAME" bash -c 'mkdir -p /tmp/pdfcheck-rt && chmod 700 /tmp/pdfcheck-rt && python3 -' <<'PY'
|
|
import importlib.util, sys, tempfile
|
|
from importlib.machinery import SourceFileLoader
|
|
from pathlib import Path
|
|
|
|
loader = SourceFileLoader("wonderink", "/usr/local/bin/classroom-wonderink")
|
|
spec = importlib.util.spec_from_loader("wonderink", loader)
|
|
ink = importlib.util.module_from_spec(spec)
|
|
loader.exec_module(ink)
|
|
|
|
from PyQt6.QtCore import QUrl
|
|
from PyQt6.QtWidgets import QApplication
|
|
from PyQt6.QtWebEngineCore import QWebEngineProfile, QWebEnginePage
|
|
|
|
app = QApplication(sys.argv[:1])
|
|
profile = QWebEngineProfile("pdfcheck", app)
|
|
window = ink.WonderInk(profile)
|
|
|
|
passed = failed = 0
|
|
|
|
def check(label, condition, detail=""):
|
|
global passed, failed
|
|
if condition:
|
|
print(f" \033[32mPASS\033[0m {label}")
|
|
passed += 1
|
|
else:
|
|
print(f" \033[31mFAIL\033[0m {label}" + (f" [{detail}]" if detail else ""))
|
|
failed += 1
|
|
|
|
work = Path(tempfile.mkdtemp())
|
|
real = work / "worksheet.pdf"
|
|
real.write_bytes(b"%PDF-1.4\n% a worksheet\n")
|
|
spaces = work / "Leader Guide, Week 1 (Advent).pdf"
|
|
spaces.write_bytes(b"%PDF-1.4\n% another\n")
|
|
htmlish = work / "broken.pdf"
|
|
htmlish.write_bytes(b"<html><body>404</body></html>")
|
|
empty = work / "empty.pdf"
|
|
empty.write_bytes(b"")
|
|
notes = work / "notes.txt"
|
|
notes.write_text("bring scissors")
|
|
|
|
print("1. which local files the window will open")
|
|
check("nothing at all, before anything is downloaded",
|
|
not window.may_open(Path("/etc/passwd")))
|
|
check("not a file that merely exists", not window.may_open(real))
|
|
|
|
page = ink.LessonPage(profile, window)
|
|
check("and a page may not navigate to one",
|
|
page.acceptNavigationRequest(
|
|
QUrl.fromLocalFile("/etc/passwd"),
|
|
QWebEnginePage.NavigationType.NavigationTypeLinkClicked, True) is False)
|
|
check("nor to a worksheet that was never opened",
|
|
page.acceptNavigationRequest(
|
|
QUrl.fromLocalFile(str(real)),
|
|
QWebEnginePage.NavigationType.NavigationTypeLinkClicked, True) is False)
|
|
|
|
print()
|
|
print("2. a worksheet the window itself opened")
|
|
window.open_pdf(real)
|
|
check("is openable afterwards", window.may_open(real))
|
|
check("and only it", not window.may_open(Path("/etc/passwd")))
|
|
check("a page may navigate to that one",
|
|
page.acceptNavigationRequest(
|
|
QUrl.fromLocalFile(str(real)),
|
|
QWebEnginePage.NavigationType.NavigationTypeLinkClicked, True) is True)
|
|
check("a symlink to somewhere else is not laundered into permission",
|
|
not window.may_open(Path("/etc/passwd")))
|
|
|
|
print()
|
|
print("3. which schemes are allowed at all")
|
|
for scheme, want in (("https://app.wonderink.org/", True),
|
|
("http://example.org/", True),
|
|
("about:blank", True),
|
|
("mailto:[email protected]", False),
|
|
("ftp://example.org/x", False),
|
|
("javascript:alert(1)", False)):
|
|
check(f"{scheme} -> {want}",
|
|
page.acceptNavigationRequest(
|
|
QUrl(scheme),
|
|
QWebEnginePage.NavigationType.NavigationTypeLinkClicked, True) is want)
|
|
|
|
print()
|
|
print("4. which downloads become a worksheet tab")
|
|
before = window._tabs.count()
|
|
check("a real PDF does", window.open_worksheet(spaces, "application/pdf") is True)
|
|
check("and it made a tab", window._tabs.count() == before + 1)
|
|
check("a PDF by extension alone does", window.open_worksheet(spaces) is True)
|
|
|
|
before = window._tabs.count()
|
|
check("HTML called .pdf does not open a blank tab",
|
|
window.open_worksheet(htmlish, "application/pdf") is True
|
|
and window._tabs.count() == before)
|
|
check("an empty file does not either",
|
|
window.open_worksheet(empty, "application/pdf") is True
|
|
and window._tabs.count() == before)
|
|
check("a file that is not there at all is handled",
|
|
window.open_worksheet(work / "missing.pdf", "application/pdf") is True
|
|
and window._tabs.count() == before)
|
|
check("plain text is not a worksheet and is left to the notification",
|
|
window.open_worksheet(notes, "text/plain") is False
|
|
and window._tabs.count() == before)
|
|
check("and a text file named .pdf is refused on its contents",
|
|
window.open_worksheet(htmlish) is True and window._tabs.count() == before)
|
|
|
|
print()
|
|
print("5. names with spaces and punctuation survive the round trip")
|
|
window.open_pdf(spaces)
|
|
url = QUrl.fromLocalFile(str(spaces.resolve()))
|
|
# toString() is the display form and shows the spaces as spaces; the
|
|
# encoded form is what actually goes on the wire.
|
|
check("the URL percent-encodes rather than breaks",
|
|
"%20" in bytes(url.toEncoded()).decode()
|
|
and url.toLocalFile() == str(spaces.resolve()))
|
|
check("and the window will open exactly that path", window.may_open(spaces))
|
|
|
|
print()
|
|
print(f"{passed} passed, {failed} failed")
|
|
|
|
# Pages go before their profile. Left to Python's own teardown the order
|
|
# is arbitrary, and a profile released while a page still uses it is a
|
|
# segfault on the way out — measured, intermittently, AFTER every check
|
|
# had passed, which made a passing run report failure. The real Wonder
|
|
# Ink was closed six times in the same lab and exited 0 each time.
|
|
from PyQt6.QtCore import QCoreApplication, QEvent
|
|
|
|
for index in range(window._tabs.count()):
|
|
view = window._tabs.widget(index)
|
|
view.page().deleteLater()
|
|
view.deleteLater()
|
|
window.close()
|
|
window.deleteLater()
|
|
QCoreApplication.sendPostedEvents(None, QEvent.Type.DeferredDelete.value)
|
|
profile.deleteLater()
|
|
QCoreApplication.sendPostedEvents(None, QEvent.Type.DeferredDelete.value)
|
|
sys.stdout.flush()
|
|
sys.exit(1 if failed else 0)
|
|
PY
|