"""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)