59 lines
2.2 KiB
Python
59 lines
2.2 KiB
Python
"""Crawls every internal link of a locally running Website — signed out, as a buyer, and as a seller — and reports the ones that fail.
|
|
|
|
swift run Website &
|
|
python3 Tests/Browser/links.py
|
|
|
|
Stdlib only. Exits non-zero when a link answers 4xx or 5xx.
|
|
"""
|
|
import html, re, sys, urllib.error, urllib.parse, urllib.request
|
|
|
|
BASE = "http://localhost:8080"
|
|
|
|
class NoRedirect(urllib.request.HTTPRedirectHandler):
|
|
def redirect_request(self, *args, **kwargs):
|
|
return None
|
|
|
|
opener = urllib.request.build_opener(NoRedirect)
|
|
|
|
def fetch(path, cookie=None, data=None):
|
|
request = urllib.request.Request(BASE + path, data=data, headers={"Cookie": cookie} if cookie else {})
|
|
try:
|
|
response = opener.open(request)
|
|
except urllib.error.HTTPError as error:
|
|
response = error
|
|
return response.status, response.headers, response.read().decode("utf-8", "replace")
|
|
|
|
def sign_in(email):
|
|
_, headers, _ = fetch("/sign-in", data=urllib.parse.urlencode({"email": email, "return": "/"}).encode())
|
|
return headers["Set-Cookie"].split(";")[0]
|
|
|
|
def crawl(name, cookie):
|
|
seen, queue, broken = set(), ["/"], []
|
|
while queue:
|
|
path = queue.pop()
|
|
if path in seen:
|
|
continue
|
|
seen.add(path)
|
|
status, headers, body = fetch(path, cookie)
|
|
if status >= 400:
|
|
broken.append((status, path))
|
|
continue
|
|
if 300 <= status < 400:
|
|
target = urllib.parse.urlsplit(headers["Location"])
|
|
queue.append(target.path + ("?" + target.query if target.query else ""))
|
|
continue
|
|
for href in re.findall(r'href="(/[^"#]*)', body):
|
|
href = html.unescape(href)
|
|
# Assets are covered by the app's own tests; the crawl is for screens.
|
|
if not re.match(r"/(css|js|font|uploads)/|/[^/]+\.(ico|svg|png|webmanifest|xml|txt)", href):
|
|
queue.append(href)
|
|
print(f"{name}: {len(seen)} links, {len(broken)} broken")
|
|
for status, path in sorted(broken):
|
|
print(f" {status} {path}")
|
|
return broken
|
|
|
|
broken = crawl("signed out", None)
|
|
broken += crawl("buyer", sign_in(""))
|
|
broken += crawl("seller", sign_in("joost@kestrelrow.example"))
|
|
sys.exit(1 if broken else 0)
|