Implemented phase 5 for the POC prototype in the Website service.
This commit is contained in:
1 parent
89a7a7b8e5
commit
31d2d98d49
46 files changed
+4039
-162
No files matched your search
@@ -0,0 +1,58 @@
|
||||
"""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)
|
||||
@@ -44,9 +44,12 @@ def check(name, ok, detail=""):
|
||||
# 1. An unknown email registers a passkey and creates the account, then returns to the intent.
|
||||
open_sign_in("/search")
|
||||
press("sam.okafor@example.com")
|
||||
welcomed = chrome.wait_for("location.pathname === '/welcome'")
|
||||
check("join: registers a passkey and meets the first-sign-in screen", welcomed, status() if not welcomed else "")
|
||||
chrome.evaluate("document.querySelector('#name').value = 'Sam Okafor'; document.querySelector('form').submit(); true")
|
||||
landed = chrome.wait_for("location.pathname === '/search'")
|
||||
check("join: registers a passkey and lands on the intent", landed, status() if not landed else chrome.evaluate("location.pathname"))
|
||||
check("join: the page is rendered for the new account", "Account of sam.okafor" in chrome.evaluate("document.documentElement.outerHTML"))
|
||||
check("join: a display name later, lands on the intent", landed, chrome.evaluate("location.pathname"))
|
||||
check("join: the page is rendered for the new account", "Account of Sam Okafor" in chrome.evaluate("document.documentElement.outerHTML"))
|
||||
held = credentials()
|
||||
check("join: the authenticator holds one discoverable credential", len(held) == 1 and held[0]["isResidentCredential"], f"{len(held)} held")
|
||||
|
||||
@@ -56,7 +59,7 @@ open_sign_in("/grading-guide")
|
||||
press("")
|
||||
landed = chrome.wait_for("location.pathname === '/grading-guide'")
|
||||
check("sign-in: a discoverable passkey signs in without an email", landed, status() if not landed else "")
|
||||
check("sign-in: as the same account", "Account of sam.okafor" in chrome.evaluate("document.documentElement.outerHTML"))
|
||||
check("sign-in: as the same account", "Account of Sam Okafor" in chrome.evaluate("document.documentElement.outerHTML"))
|
||||
check("sign-in: the signature counter moved on", credentials()[0]["signCount"] >= 2, credentials()[0]["signCount"])
|
||||
|
||||
# 3. By email, the ceremony names the account's credential.
|
||||
@@ -65,12 +68,27 @@ open_sign_in("/")
|
||||
press("Sam.Okafor@Example.com")
|
||||
check("sign-in: by email, normalized", chrome.wait_for("location.pathname === '/'"), status())
|
||||
|
||||
# 4. A demo account has no passkey, and none can be added to it.
|
||||
# 3b. Signed in, the account adds a second passkey, then revokes one.
|
||||
chrome.send("Page.navigate", url=f"{BASE}/account")
|
||||
assert chrome.wait_for("document.querySelector('[data-passkey=add]') && !document.querySelector('[data-passkey=add]').hidden")
|
||||
chrome.evaluate("document.querySelector('[data-passkey=add]').click(); true")
|
||||
added = chrome.wait_for("document.querySelectorAll('.rows__row--passkey').length === 2")
|
||||
check("account: adds a second passkey from its own session", added and len(credentials()) == 2, status())
|
||||
chrome.evaluate("document.querySelector('.rows__row--passkey form').submit(); true")
|
||||
check("account: revokes one, at once", chrome.wait_for("document.querySelectorAll('.rows__row--passkey').length === 1"))
|
||||
chrome.send("WebAuthn.clearCredentials", authenticatorId=authenticator)
|
||||
chrome.evaluate("fetch('/sign-out', {method: 'POST'}).then(() => true)")
|
||||
chrome.send("Page.navigate", url=f"{BASE}/")
|
||||
chrome.wait_for("document.readyState === 'complete'")
|
||||
anonymous = chrome.evaluate("fetch('/sign-in/passkey/options', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: '{\"add\": true}'}).then(r => r.status)")
|
||||
check("refusal: nobody adds a passkey signed out", anonymous == 401, anonymous)
|
||||
held_before = 0
|
||||
|
||||
# 4. A demo account has no passkey, and none can be added to it.
|
||||
open_sign_in("/")
|
||||
press("mara.v@example.com")
|
||||
refused = chrome.wait_for("(document.querySelector('[data-passkey-status]')?.textContent ?? '').includes('no passkey')")
|
||||
check("refusal: an existing account is never handed a new passkey", refused and len(credentials()) == 1, status())
|
||||
check("refusal: an existing account is never handed a new passkey", refused and len(credentials()) == held_before, status())
|
||||
check("refusal: nobody was signed in", chrome.evaluate("location.pathname") == "/sign-in")
|
||||
|
||||
sys.exit(0 if all(results) else 1)
|
||||
Reference in new issue
Block a user