#!/usr/bin/env python3 """Is the app's state addressable? — S3 / T4.2. X1 is a blocking dependency: CR-011 and CR-014 both promise an email carrying a direct link to a work package, and before this there was no pushState anywhere in the suite, so no work package had an address. This checks the promise those emails will rest on. 1. a URL identifying a work package opens that work package 2. the same URL works for a SIGNED-OUT user, via the real Okta sign-in round trip (T10.7's fake provider stands in for Okta itself — see server/okta_fake.py — but the app's own login.html, the redirect to /api/auth/okta/login, the state round trip through SessionMiddleware, and okta_callback()'s handling of ?next= are all real, unmodified code). Landing on the requested target, not the home page, doubles as setup for scenarios 3-6 below. 3. refresh preserves project, package, tab and view 4. Back and Forward move through states without a reload or a broken view 5. the URL survives being copied to a second browsing context 6. pushState is actually used; the count is recorded Self-contained: throwaway SQLite, its own uvicorn, headless Edge or Chrome. Exit 0 all passed, 1 a failure, 2 could not run. """ import json import os import subprocess import sys import tempfile import time sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import cdp # noqa: E402 from browser_check import seed, start_server, chk, _PASS, _FAIL, _c # noqa: E402 def settle(page, seconds=1.4): time.sleep(seconds) def main(): exe = cdp.find_browser() if not exe: print("no headless-capable browser found; set WP_BROWSER.") return 2 tmpdir = tempfile.mkdtemp(prefix="wpsuite-urlstate-") db_path = os.path.join(tmpdir, "check.db") server = None try: tok = seed(db_path) port = cdp.free_port() base = "http://127.0.0.1:%d" % port server = start_server(port, db_path) if server is None: print("the test server would not start.") return 2 print("\nAddressable state — S3 / T4.2\nTarget: %s" % base) browser = cdp.Browser(exe) page = browser.page() try: print("\n0. the module is present and does not need a hash") page.clear_cookies() page.set_cookie("wp_session", tok["root"]) page.goto(base + "/wp-creation-index.html?project=projA") settle(page) chk("WPUrl is loaded", page.eval("typeof WPUrl") == "object") chk("it merges rather than clobbers", page.eval("WPUrl.href({wp:'wpA1'})").find("project=projA") != -1 and page.eval("WPUrl.href({wp:'wpA1'})").find("wp=wpA1") != -1, page.eval("WPUrl.href({wp:'wpA1'})")) chk("clearing a key does not drop the others", "project=projA" in page.eval("WPUrl.href({wp:''})") and "wp=" not in page.eval("WPUrl.href({wp:''})"), page.eval("WPUrl.href({wp:''})")) chk("it produces an absolute link for emails (X1)", page.eval("WPUrl.absolute({wp:'wpA1'})").startswith("http"), page.eval("WPUrl.absolute({wp:'wpA1'})")) print("\n1. a URL identifying a work package opens it") page.goto(base + "/wp-creation-index.html?project=projA&wp=wpA1") for _ in range(30): if page.eval("!!window.wpCreatorReady"): break time.sleep(0.3) settle(page, 1.0) subject = page.eval("(document.getElementById('wp_subject')||{}).value||''") chk("the deep-linked package is loaded into the form", "horn/strobe" in subject, "subject field read %r" % subject) print("\n6. pushState is used") before = page.eval("history.length") page.eval("typeof showDashboard==='function' && showDashboard()") settle(page, 1.0) after = page.eval("history.length") chk("opening the dashboard adds a history entry", after > before, "history.length %s -> %s" % (before, after)) chk("...and says so in the URL", "view=dashboard" in page.eval("location.search"), page.eval("location.search")) print("\n4. Back and Forward move through states") page.eval("history.back()") settle(page, 1.2) chk("Back leaves the dashboard", "view=dashboard" not in page.eval("location.search"), page.eval("location.search")) chk("...and returns to the package, not to a blank page", page.eval("location.search").find("wp=wpA1") != -1, page.eval("location.search")) chk("...without a full reload (the app is still initialised)", page.eval("!!window.wpCreatorReady")) page.eval("history.forward()") settle(page, 1.2) chk("Forward returns to the dashboard", "view=dashboard" in page.eval("location.search"), page.eval("location.search")) chk("...and the dashboard is actually rendered, not just the URL", page.eval("(document.getElementById('dashboard-view')||{}).style.display") != "none") print("\n3. refresh preserves the state") page.goto(base + "/wp-creation-index.html?project=projA&wp=wpA2") for _ in range(30): if page.eval("!!window.wpCreatorReady"): break time.sleep(0.3) settle(page, 1.0) page.eval("location.reload()") for _ in range(30): if page.eval("!!window.wpCreatorReady"): break time.sleep(0.3) settle(page, 1.0) subject = page.eval("(document.getElementById('wp_subject')||{}).value||''") chk("a refresh lands on the same package", "wire pull" in subject, "subject read %r" % subject) print("\n3b. the SOP wizard's tab and step are addressable") page.goto(base + "/work-package-suite.html?project=projA&tab=sop&step=3") settle(page, 1.6) chk("the wizard restores the deep-linked step", page.eval("typeof currentStep!=='undefined' && currentStep") == 3, page.eval("typeof currentStep!=='undefined' && currentStep")) hlen = page.eval("history.length") page.eval("typeof goToStep==='function' && goToStep(5)") settle(page, 0.8) chk("moving a step records it", "step=5" in page.eval("location.search"), page.eval("location.search")) chk("...as a history entry", page.eval("history.length") > hlen) page.eval("history.back()") settle(page, 1.0) chk("Back returns to the previous step", page.eval("typeof currentStep!=='undefined' && currentStep") == 3, page.eval("location.search")) print("\n5. the URL reaches the same view in a second context") deep = base + "/wp-creation-index.html?project=projA&wp=wpA1" page2 = browser.page() try: page2.clear_cookies() page2.set_cookie("wp_session", tok["pat"]) page2.goto(deep) for _ in range(30): if page2.eval("!!window.wpCreatorReady"): break time.sleep(0.3) settle(page2, 1.0) s2 = page2.eval("(document.getElementById('wp_subject')||{}).value||''") chk("a different user opening the same URL sees the same package", "horn/strobe" in s2, "subject read %r" % s2) finally: page2.close() print("\n2. the same URL works for a signed-out user, via the real Okta round trip") page.clear_cookies() page.goto(deep) settle(page, 1.6) chk("a signed-out visitor is sent to login", "login.html" in page.eval("location.href"), page.eval("location.href")) nxt = page.eval("new URLSearchParams(location.search).get('next')||''") chk("...carrying the requested target, package id and all", "wp-creation-index.html" in nxt and "wp=wpA1" in nxt, "next=%r" % nxt) # Drive the actual button, not a shortcut to it — its href already # carries ?next= (login.js's safeNext()); this is the same click a # person makes. signin_href = page.eval( "(document.getElementById('okta-signin')||{}).getAttribute('href')||''") chk("the sign-in link itself carries ?next=", "next=" in signin_href, signin_href) page.goto(base + signin_href) for _ in range(30): if "_fake_provider" in page.eval("location.href"): break time.sleep(0.3) chk("the app hands off to the (fake) Okta provider", "_fake_provider" in page.eval("location.href"), page.eval("location.href")) # The fake provider's own picker page — a real page, not a shortcut. # See server/okta_fake.py: only the network-touching Authlib calls are # faked, not app.py's own login/callback/JIT/guard code. identity_href = page.eval( "(document.getElementById('okta-fake-identity-root')||{}).getAttribute('href')||''") chk("the fake provider offers the seeded 'root' identity", bool(identity_href), page.eval("document.body.innerHTML")) page.goto(base + identity_href) for _ in range(30): href = page.eval("location.href") if "login.html" not in href and "_fake_provider" not in href: break time.sleep(0.3) settle(page, 1.0) # NOT `"wp-creation-index.html" in location.href` alone — that string # is in the ?next= parameter too, so this would pass while still # sitting on login.html with the sign-in rejected (the same mistake # D13/T10.7's LDAP predecessor caught and fixed here). Assert we # actually LEFT the login page. href = page.eval("location.href") chk("signing in continues to the requested page, not the home page", "login.html" not in href and "wp-creation-index.html" in href and "wp=wpA1" in href, href) for _ in range(30): if page.eval("!!window.wpCreatorReady"): break time.sleep(0.3) settle(page, 1.0) s3 = page.eval("(document.getElementById('wp_subject')||{}).value||''") chk("...and lands on the work package itself, not a dashboard", "horn/strobe" in s3, "subject read %r" % s3) print("\n7. nothing secret rides in the URL") qs = page.eval("location.search").lower() leaked = [w for w in ("token", "session", "password", "secret", "auth") if w in qs] chk("no credential-shaped parameter", not leaked, "found %s in %r" % (leaked, qs)) finally: page.close() browser.close() finally: if server: server.kill() try: server.wait(timeout=10) except subprocess.TimeoutExpired: pass try: from server.db import engine engine.dispose() except Exception: pass import shutil for _ in range(10): shutil.rmtree(tmpdir, ignore_errors=True) if not os.path.exists(tmpdir): break time.sleep(0.3) total = len(_PASS) + len(_FAIL) print("\n%s\n%d/%d checks passed." % ("-" * 54, len(_PASS), total)) if _FAIL: for f in _FAIL: print(" - " + f) return 1 print("\nResult: " + _c("ALL PASS — the app's state has an address.", "32") + "\n") return 0 if __name__ == "__main__": sys.exit(main())