import json import subprocess from pathlib import Path import pytest from src.views import login ROOT = Path(__file__).resolve().parents[1] LOGIN_JS = ROOT / "frontend" / "login.js" def test_expired_session_reason_explains_preserved_private_work(): harness = f""" const createLoginController = require({json.dumps(str(LOGIN_JS))}); const status = {{ textContent: '' }}; const controller = createLoginController({{ form: {{ reset: () => {{}} }}, status, button: {{ disabled: false }}, fetchImpl: async () => new Response('{{}}', {{ status: 200 }}), location: {{ replace: () => {{}} }}, }}); controller.showReason('session-expired'); const expired = status.textContent; controller.showReason('https://evil.example/redirect'); process.stdout.write(JSON.stringify({{ expired, ignored: status.textContent }})); """ result = subprocess.run( ["node", "-e", harness], text=True, capture_output=True, check=True ) state = json.loads(result.stdout) assert state["expired"] == ( "Your session expired. Private drafts remain on this device. " "Sign in to continue." ) assert state["ignored"] == state["expired"] def test_idle_session_reason_explains_that_saved_work_will_resume(): harness = f""" const createLoginController = require({json.dumps(str(LOGIN_JS))}); const status = {{ textContent: '' }}; const controller = createLoginController({{ form: {{ reset: () => {{}} }}, status, button: {{ disabled: false }}, fetchImpl: async () => new Response('{{}}', {{ status: 200 }}), location: {{ replace: () => {{}} }}, }}); controller.showReason('session-idle'); process.stdout.write(JSON.stringify({{ status: status.textContent }})); """ result = subprocess.run( ["node", "-e", harness], text=True, capture_output=True, check=True ) assert json.loads(result.stdout)["status"] == ( "Stackchain locked after inactivity. Your drafts and queued work are still " "on this device. Sign in to resume." ) def test_revoked_session_reason_clears_private_data_before_enabling_sign_in(): harness = f""" const createLoginController = require({json.dumps(str(LOGIN_JS))}); const state = {{cleared:false, snapshots:[]}}; const status = {{textContent:''}}; const button = {{disabled:false}}; const controller = createLoginController({{ form:{{reset:()=>{{}}}}, status, button, fetchImpl:async()=>new Response('{{}}',{{status:200}}), location:{{replace:()=>{{}}}}, clearPrivateDeviceData:async()=>{{ state.snapshots.push({{status:status.textContent,disabled:button.disabled}}); await new Promise(resolve=>setTimeout(resolve,10)); state.cleared=true; }}, }}); (async()=>{{ const pending=controller.showReason('session-revoked'); state.snapshots.push({{status:status.textContent,disabled:button.disabled}}); await pending; state.final={{status:status.textContent,disabled:button.disabled,cleared:state.cleared}}; process.stdout.write(JSON.stringify(state)); }})().catch(error=>{{console.error(error);process.exit(1);}}); """ result = subprocess.run( ["node", "-e", harness], text=True, capture_output=True, check=True ) state = json.loads(result.stdout) assert all(snapshot["disabled"] for snapshot in state["snapshots"]) assert state["final"] == { "status": "This device was remotely signed out. Stackchain private data was cleared. Sign in to use it again.", "disabled": False, "cleared": True, } @pytest.mark.anyio async def test_login_loads_private_data_purger_before_controller(): html = await login() assert '' in html assert '' in html assert html.index('static/private-data-registry.js') < html.index('static/private-device-data.js') assert html.index('static/private-device-data.js') < html.index('static/login.js') def test_revoked_session_does_not_claim_success_when_purger_is_unavailable(): harness = f""" const createLoginController = require({json.dumps(str(LOGIN_JS))}); const status={{textContent:''}}; const button={{disabled:false}}; const controller=createLoginController({{ form:{{reset:()=>{{}}}},status,button, fetchImpl:async()=>new Response('{{}}',{{status:200}}),location:{{replace:()=>{{}}}}, }}); (async()=>{{ await controller.showReason('session-revoked'); process.stdout.write(JSON.stringify({{status:status.textContent,disabled:button.disabled}})); }})(); """ result = subprocess.run( ["node", "-e", harness], text=True, capture_output=True, check=True ) assert json.loads(result.stdout) == { "status": "This device was remotely signed out, but private data could not be cleared. Close other Stackchain tabs and clear this site’s data before signing in.", "disabled": True, } def test_rate_limited_login_disables_submit_and_counts_down(): harness = f""" const createLoginController = require({json.dumps(str(LOGIN_JS))}); const state = {{ reset: 0, interval: null }}; const status = {{ textContent: '' }}; const button = {{ disabled: false }}; const form = {{ reset: () => state.reset += 1 }}; const controller = createLoginController({{ form, status, button, fetchImpl: async () => new Response(JSON.stringify({{ detail: 'Too many sign-in attempts' }}), {{ status: 429, headers: {{ 'Retry-After': '2' }} }}), location: {{ replace: () => {{}} }}, setIntervalImpl: callback => {{ state.interval = callback; return 1; }}, clearIntervalImpl: () => {{}}, }}); (async () => {{ await controller.submit('never-store-this-token'); state.initial = {{ disabled: button.disabled, status: status.textContent, reset: state.reset }}; state.interval(); state.afterTick = {{ disabled: button.disabled, status: status.textContent }}; state.interval(); state.finished = {{ disabled: button.disabled, status: status.textContent }}; process.stdout.write(JSON.stringify(state)); }})().catch(error => {{ console.error(error); process.exit(1); }}); """ result = subprocess.run( ["node", "-e", harness], text=True, capture_output=True, check=True ) state = json.loads(result.stdout) assert state["initial"] == { "disabled": True, "status": "Too many attempts. Try again in 2 seconds.", "reset": 1, } assert state["afterTick"]["disabled"] is True assert state["finished"] == { "disabled": False, "status": "You can try signing in again.", } def test_stalled_token_login_aborts_once_and_restores_both_sign_in_controls(): harness = f""" const createLoginController = require({json.dumps(str(LOGIN_JS))}); const state = {{ requests: 0, timeout: null, aborted: false }}; const status = {{ textContent: '' }}; const button = {{ disabled: false }}; const passkeyButton = {{ disabled: false }}; const controller = createLoginController({{ form: {{ reset: () => {{}} }}, status, button, passkeyButton, credentials: {{ get: async () => {{ throw new Error('unused'); }} }}, fetchImpl: (_url, options) => {{ state.requests += 1; options.signal.addEventListener('abort', () => state.aborted = true); return new Promise((_resolve, reject) => {{ options.signal.addEventListener('abort', () => reject(options.signal.reason)); }}); }}, location: {{ replace: () => {{}} }}, requestTimeoutMs: 25, setTimeoutImpl: callback => {{ state.timeout = callback; return 7; }}, clearTimeoutImpl: () => {{}}, }}); (async () => {{ const first = controller.submit('operator-token'); const duplicate = controller.submit('operator-token'); state.during = {{ button: button.disabled, passkey: passkeyButton.disabled }}; state.timeout(); await Promise.all([first, duplicate]); state.after = {{ button: button.disabled, passkey: passkeyButton.disabled, status: status.textContent, }}; process.stdout.write(JSON.stringify(state)); }})().catch(error => {{ console.error(error); process.exit(1); }}); """ result = subprocess.run( ["node", "-e", harness], text=True, capture_output=True, check=True ) state = json.loads(result.stdout) assert state["requests"] == 1 assert state["during"] == {"button": True, "passkey": True} assert state["aborted"] is True assert state["after"] == { "button": False, "passkey": False, "status": "Sign-in timed out. Check your connection and try again.", } def test_successful_login_resumes_valid_shared_capture_continuation(): harness = f""" const createLoginController = require({json.dumps(str(LOGIN_JS))}); const state = {{ replaced: null }}; const controller = createLoginController({{ form: {{ reset: () => {{}} }}, status: {{ textContent: '' }}, button: {{ disabled: false }}, fetchImpl: async () => new Response('{{}}', {{ status: 200 }}), location: {{ replace: value => state.replaced = value }}, continuation: './?title=Production+crash&url=https%3A%2F%2Fexample.com', }}); (async () => {{ await controller.submit('operator-token'); process.stdout.write(JSON.stringify(state)); }})().catch(error => {{ console.error(error); process.exit(1); }}); """ result = subprocess.run( ["node", "-e", harness], text=True, capture_output=True, check=True ) assert json.loads(result.stdout)["replaced"] == ( "./?title=Production+crash&url=https%3A%2F%2Fexample.com" ) def test_successful_login_resumes_only_valid_search_preview_continuation(): harness = f""" const createLoginController = require({json.dumps(str(LOGIN_JS))}); async function destination(continuation) {{ let replaced = null; const controller = createLoginController({{ form: {{ reset: () => {{}} }}, status: {{ textContent: '' }}, button: {{ disabled: false }}, fetchImpl: async () => new Response('{{}}', {{ status: 200 }}), location: {{ replace: value => replaced = value }}, continuation, }}); await controller.submit('operator-token'); return replaced; }} (async () => process.stdout.write(JSON.stringify({{ valid:await destination('./?search=release+blocker&preview=pull%3Astackchain%2Fapi%3A9&search_kind=pull&search_state=open&search_repository=stackchain%2Fapi'), malformed:await destination('./?search=release&preview=issue%3Astackchain%2Fapi%3A0'), unknown:await destination('./?search=release&next=https%3A%2F%2Fevil.example'), }})))().catch(error => {{ console.error(error); process.exit(1); }}); """ result = subprocess.run(["node", "-e", harness], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "valid": "./?search=release+blocker&preview=pull%3Astackchain%2Fapi%3A9&search_kind=pull&search_state=open&search_repository=stackchain%2Fapi", "malformed": "./", "unknown": "./", } def test_search_preview_login_explains_the_resumable_handoff(): harness = f""" const createLoginController = require({json.dumps(str(LOGIN_JS))}); const status = {{ textContent: '' }}; createLoginController({{ form: {{ reset: () => {{}} }}, status, button: {{ disabled: false }}, fetchImpl: async () => new Response('{{}}', {{ status: 200 }}), location: {{ replace: () => {{}} }}, continuation: './?search=release&preview=issue%3Astackchain%2Fapi%3A42', }}); process.stdout.write(status.textContent); """ result = subprocess.run(["node", "-e", harness], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert result.stdout == "Sign in to open the shared Search result." def test_passkey_login_uses_web_authentication_without_sending_the_operator_token(): harness = f""" const createLoginController = require({json.dumps(str(LOGIN_JS))}); const requests = []; const state = {{ replaced: null }}; const credential = {{ id:'credential-id', type:'public-key', rawId:Uint8Array.from([1,2,3]).buffer, response:{{ authenticatorData:Uint8Array.from([4]).buffer, clientDataJSON:Uint8Array.from([5]).buffer, signature:Uint8Array.from([6]).buffer, userHandle:null, }}, }}; const controller = createLoginController({{ form:{{reset:()=>{{}}}}, status:{{textContent:''}}, button:{{disabled:false}}, passkeyButton:{{disabled:false}}, credentials:{{get:async options=>{{ state.publicKey=options.publicKey; return credential; }}}}, fetchImpl:async (url, options={{}})=>{{ requests.push({{url, body:options.body ? JSON.parse(options.body) : null}}); if (url.endsWith('/options')) return new Response(JSON.stringify({{ challenge:'AQID', rpId:'forge.example', userVerification:'required', allowCredentials:[{{type:'public-key',id:'BAUG'}}], }}),{{status:200,headers:{{'Content-Type':'application/json'}}}}); return new Response('{{}}',{{status:200}}); }}, location:{{replace:value=>state.replaced=value}}, }}); (async()=>{{ await controller.signInWithPasskey('Timmy’s Pixel'); process.stdout.write(JSON.stringify({{requests,state}})); }})().catch(error=>{{console.error(error);process.exit(1);}}); """ result = subprocess.run( ["node", "-e", harness], text=True, capture_output=True, check=True ) output = json.loads(result.stdout) assert output["state"]["publicKey"]["challenge"] == {"0": 1, "1": 2, "2": 3} assert output["state"]["replaced"] == "./" assert output["requests"][1]["url"] == "api/v1/passkeys/authentication/verify" assert output["requests"][1]["body"] == { "challenge": "AQID", "credential": { "id": "credential-id", "type": "public-key", "rawId": "AQID", "response": { "authenticatorData": "BA", "clientDataJSON": "BQ", "signature": "Bg", "userHandle": None, }, }, "device_label": "Timmy’s Pixel", "action": "sign_in", "target": "dashboard", } assert "access_token" not in json.dumps(output) def test_stalled_passkey_network_phases_time_out_without_timing_out_biometrics(): harness = f""" const createLoginController = require({json.dumps(str(LOGIN_JS))}); async function scenario(stalledPhase) {{ const state = {{ requests: [], timers: [], credentialCalls: 0 }}; const status = {{textContent:''}}; const button = {{disabled:false}}; const passkeyButton = {{disabled:false}}; const credential = {{ id:'credential-id', type:'public-key', rawId:Uint8Array.from([1]).buffer, response:{{authenticatorData:Uint8Array.from([2]).buffer, clientDataJSON:Uint8Array.from([3]).buffer, signature:Uint8Array.from([4]).buffer,userHandle:null}}, }}; const controller=createLoginController({{ form:{{reset:()=>{{}}}},status,button,passkeyButton, credentials:{{get:async()=>{{state.credentialCalls+=1;return credential;}}}}, fetchImpl:(url,options)=>{{ state.requests.push(url); const phase=url.endsWith('/options') ? 'options' : 'verify'; if (phase === stalledPhase) return new Promise((_resolve,reject)=>{{ options.signal.addEventListener('abort',()=>reject(options.signal.reason)); }}); return Promise.resolve(new Response(JSON.stringify({{ challenge:'AQID',allowCredentials:[] }}),{{status:200,headers:{{'Content-Type':'application/json'}}}})); }}, location:{{replace:()=>{{}}}},requestTimeoutMs:25, setTimeoutImpl:callback=>{{state.timers.push(callback);return state.timers.length;}}, clearTimeoutImpl:()=>{{}}, }}); const pending=controller.signInWithPasskey('Phone'); await new Promise(resolve=>setImmediate(resolve)); if (stalledPhase === 'verify') await new Promise(resolve=>setImmediate(resolve)); state.timers.at(-1)(); state.result=await pending; state.final={{status:status.textContent,button:button.disabled,passkey:passkeyButton.disabled}}; return state; }} (async()=>{{ process.stdout.write(JSON.stringify({{ options:await scenario('options'),verify:await scenario('verify'), }})); }})().catch(error=>{{console.error(error);process.exit(1);}}); """ result = subprocess.run( ["node", "-e", harness], text=True, capture_output=True, check=True ) state = json.loads(result.stdout) assert state["options"]["credentialCalls"] == 0 assert state["verify"]["credentialCalls"] == 1 assert state["verify"]["timers"] == [None, None] for phase in ("options", "verify"): assert state[phase]["result"] is False assert state[phase]["final"] == { "status": "Passkey sign-in timed out. Check your connection and try again.", "button": False, "passkey": False, } def test_login_sends_a_bounded_device_label_with_the_access_token(): harness = f""" const createLoginController = require({json.dumps(str(LOGIN_JS))}); let request = null; const controller = createLoginController({{ form: {{ reset: () => {{}} }}, status: {{ textContent: '' }}, button: {{ disabled: false }}, fetchImpl: async (url, options) => {{ request = {{url, body: JSON.parse(options.body)}}; return new Response('{{}}', {{ status: 200 }}); }}, location: {{ replace: () => {{}} }}, }}); (async () => {{ await controller.submit('operator-token', 'Timmy’s Pixel'); process.stdout.write(JSON.stringify(request)); }})().catch(error => {{ console.error(error); process.exit(1); }}); """ result = subprocess.run( ["node", "-e", harness], text=True, capture_output=True, check=True ) assert json.loads(result.stdout) == { "url": "api/v1/session", "body": {"access_token": "operator-token", "device_label": "Timmy’s Pixel"}, } def test_shared_capture_login_explains_why_sign_in_is_required(): harness = f""" const createLoginController = require({json.dumps(str(LOGIN_JS))}); const status = {{ textContent: '' }}; createLoginController({{ form: {{ reset: () => {{}} }}, status, button: {{ disabled: false }}, fetchImpl: async () => new Response('{{}}', {{ status: 200 }}), location: {{ replace: () => {{}} }}, continuation: './?title=Shared', }}); process.stdout.write(status.textContent); """ result = subprocess.run( ["node", "-e", harness], text=True, capture_output=True, check=True ) assert result.stdout == "Sign in to continue your shared capture." def test_login_rejects_untrusted_or_non_share_continuations(): harness = f""" const createLoginController = require({json.dumps(str(LOGIN_JS))}); async function destination(continuation) {{ let replaced = null; const controller = createLoginController({{ form: {{ reset: () => {{}} }}, status: {{ textContent: '' }}, button: {{ disabled: false }}, fetchImpl: async () => new Response('{{}}', {{ status: 200 }}), location: {{ replace: value => replaced = value }}, continuation, }}); await controller.submit('operator-token'); return replaced; }} (async () => {{ process.stdout.write(JSON.stringify({{ crossOrigin: await destination('https://evil.example/steal'), otherRoute: await destination('./settings?title=Shared'), extraField: await destination('./?title=Shared&next=https%3A%2F%2Fevil.example'), prototypeField: await destination('./?toString=Shared'), oversized: await destination('./?title=' + 'x'.repeat(201)), }})); }})().catch(error => {{ console.error(error); process.exit(1); }}); """ result = subprocess.run( ["node", "-e", harness], text=True, capture_output=True, check=True ) assert json.loads(result.stdout) == { "crossOrigin": "./", "otherRoute": "./", "extraField": "./", "prototypeField": "./", "oversized": "./", } def test_login_bootstrap_continues_shared_capture_after_submit(): harness = f""" const fs = require('fs'); const vm = require('vm'); const state = {{ replaced: null, submit: null }}; const elements = {{ 'sign-in': {{ reset: () => {{}}, addEventListener: (_name, handler) => state.submit = handler }}, 'status': {{ textContent: '' }}, 'submit-sign-in': {{ disabled: false }}, }}; const context = {{ URLSearchParams, Response, AbortController, setInterval, clearInterval, setTimeout, clearTimeout, FormData: function () {{ return {{ get: () => 'operator-token' }}; }}, fetch: async () => new Response('{{}}', {{ status: 200 }}), document: {{ getElementById: id => elements[id] }}, window: {{ location: {{ search: '?continue=.%2F%3Ftitle%3DShared%2Blink%26url%3Dhttps%253A%252F%252Fexample.com', replace: value => state.replaced = value, }} }}, }}; context.fetch.bind = Function.prototype.bind; vm.createContext(context); vm.runInContext(fs.readFileSync({json.dumps(str(LOGIN_JS))}, 'utf8'), context); state.submit({{ preventDefault: () => {{}} }}); setTimeout(() => process.stdout.write(JSON.stringify({{ replaced: state.replaced, status: elements.status.textContent, }})), 0); """ result = subprocess.run( ["node", "-e", harness], text=True, capture_output=True, check=True ) assert json.loads(result.stdout) == { "replaced": "./?title=Shared+link&url=https%3A%2F%2Fexample.com", "status": "Signing in…", } def test_login_continues_only_bounded_shared_screenshot_capture(): harness = f""" const createLoginController = require({json.dumps(str(LOGIN_JS))}); async function destination(continuation) {{ let replaced = null; const controller = createLoginController({{ form: {{ reset: () => {{}} }}, status: {{ textContent: '' }}, button: {{ disabled: false }}, fetchImpl: async () => new Response('{{}}', {{ status: 200 }}), location: {{ replace: value => replaced = value }}, continuation, }}); await controller.submit('operator-token'); return replaced; }} (async () => {{ process.stdout.write(JSON.stringify({{ valid: await destination('./?title=Broken&launch=new&shared=image'), wrongLaunch: await destination('./?launch=agenda&shared=image'), missingLaunch: await destination('./?shared=image'), wrongMarker: await destination('./?launch=new&shared=document'), duplicateMarker: await destination('./?launch=new&shared=image&shared=image'), launchWithoutMarker: await destination('./?launch=new'), }})); }})().catch(error => {{ console.error(error); process.exit(1); }}); """ result = subprocess.run( ["node", "-e", harness], text=True, capture_output=True, check=True ) assert json.loads(result.stdout) == { "valid": "./?title=Broken&launch=new&shared=image", "wrongLaunch": "./", "missingLaunch": "./", "wrongMarker": "./", "duplicateMarker": "./", "launchWithoutMarker": "./?launch=new", } @pytest.mark.anyio async def test_login_page_loads_rate_limit_controller(): html = await login() assert '' in html assert "main{box-sizing:border-box" in html assert '

' in html assert 'name="device_label"' in html assert 'maxlength="64"' in html