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 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_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_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, setInterval, clearInterval, 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…", } @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