import json import subprocess from pathlib import Path import pytest from tests.dashboard_bundle import dashboard ROOT = Path(__file__).resolve().parents[1] SESSION_JS = ROOT / "frontend" / "session.js" SECURITY_CENTER_JS = ROOT / "frontend" / "security-center.js" def run_session_scenario(scenario: str) -> dict: harness = f""" const createSessionBoundary = require({json.dumps(str(SESSION_JS))}); const state = {{ requests: [], removed: [], deletedDatabases: [], deletionCompleted: false, deletedCaches: [], assigned: '', replaced: [], assignedAfterDeletion: false, replacedAfterDeletion: false, expiredAfterDeletion: false, workerMessages: [], confirmations: [], prompts: [], clearErrors: [], activityListeners: {{}}, timers: [], now: 100000, responseStatus: 200, responsePayload: {{}}, responses: [] }}; const storage = {{ values: new Map([['stackchain.private', 'secret'], ['gitea.preference', 'keep']]), get length() {{ return this.values.size; }}, key(index) {{ return Array.from(this.values.keys())[index] || null; }}, getItem(key) {{ return this.values.get(key) || null; }}, setItem(key, value) {{ this.values.set(key, String(value)); }}, removeItem(key) {{ state.removed.push(key); this.values.delete(key); }}, }}; const boundary = createSessionBoundary({{ cookie: () => 'other=x; stackchain_csrf=csrf-proof; theme=dark', origin: 'https://forge.example', base: '/dashboard/', fetchImpl: async (url, options = {{}}) => {{ state.requests.push({{ url: String(url), method: options.method || 'GET', headers: Object.fromEntries(new Headers(options.headers || {{}})), body: options.body || null }}); if (state.failFetch) throw new Error('offline'); const configured = state.responses.length ? state.responses.shift() : {{ status: state.responseStatus, payload: state.responsePayload }}; return new Response(JSON.stringify(configured.payload), {{ status: configured.status, headers: {{ 'Content-Type': 'application/json' }} }}); }}, localStorage: storage, sessionStorage: storage, indexedDB: {{ deleteDatabase: name => {{ state.deletedDatabases.push(name); const request = {{ onsuccess: null, onerror: null, onblocked: null, error: null }}; setTimeout(() => {{ if (state.failDeletion) {{ request.error = new Error('database blocked'); request.onerror?.(); }} else {{ state.deletionCompleted = true; request.onsuccess?.(); }} }}, 20); return request; }} }}, caches: {{ keys: async () => ['stackchain-dashboard-shell-v15', 'gitea-assets'], delete: async key => {{ state.deletedCaches.push(key); }} }}, serviceWorker: {{ ready: Promise.resolve({{ active: {{ postMessage: (message, ports = []) => {{ state.workerMessages.push(message); ports[0]?.postMessage({{ok:true}}); }} }} }}) }}, MessageChannel: class {{ constructor() {{ const first = {{ onmessage: null, postMessage: data => queueMicrotask(() => second.onmessage?.({{data}})) }}; const second = {{ onmessage: null, postMessage: data => queueMicrotask(() => first.onmessage?.({{data}})) }}; this.port1 = first; this.port2 = second; }} }}, location: {{ assign: value => {{ state.assigned = value; state.assignedAfterDeletion = state.deletionCompleted; }}, replace: value => state.replaced.push(value), }}, onClearError: error => state.clearErrors.push(error.message), onExpired: () => {{ state.expiredAfterDeletion = state.deletionCompleted; }}, addActivityListener: (type, listener) => {{ state.activityListeners[type] = listener; }}, now: () => state.now, setTimer: (callback, delay) => {{ state.leaseDelay = delay; state.timers.push({{callback, delay}}); return state.timers.length; }}, confirmAction: message => {{ state.confirmations.push(message); return true; }}, promptAuthorization: details => {{ state.prompts.push(details); return 'correct horse battery staple'; }}, }}); (async () => {{ {scenario} }})().catch(error => {{ console.error(error); process.exit(1); }}); """ result = subprocess.run(["node", "-e", harness], text=True, capture_output=True, check=True) return json.loads(result.stdout) def test_same_origin_api_fetch_times_out_even_when_fetch_ignores_abort(): script = f""" const createSessionBoundary = require({json.dumps(str(SESSION_JS))}); let aborted = false; const boundary = createSessionBoundary({{ cookie: () => '', origin: 'https://forge.example', base: '/dashboard/', requestTimeoutMs: 10, fetchImpl: (_url, options = {{}}) => new Promise(resolve => {{ options.signal?.addEventListener('abort', () => {{ aborted = true; }}); setTimeout(() => resolve(new Response('{{}}', {{status:200}})), 80); }}), location: {{ replace: () => {{}} }}, }}); (async () => {{ const started = Date.now(); try {{ await boundary.fetch('/dashboard/api/v1/context'); }} catch (error) {{ process.stdout.write(JSON.stringify({{ name:error.name, message:error.message, method:error.method, outcome:error.outcome, aborted, elapsed:Date.now()-started, }})); }} }})().catch(error => {{ console.error(error); process.exit(1); }}); """ result = subprocess.run( ["node", "-e", script], text=True, capture_output=True, check=True, timeout=2 ) output = json.loads(result.stdout) assert output["name"] == "TimeoutError" assert output["message"] == "Request timed out. Try again." assert output["method"] == "GET" assert output["outcome"] == "unknown" assert output["aborted"] is True assert output["elapsed"] < 70 def test_caller_abort_cancels_same_origin_api_fetch_without_waiting_for_deadline(): script = f""" const createSessionBoundary = require({json.dumps(str(SESSION_JS))}); const caller = new AbortController(); let requestAborted = false; const boundary = createSessionBoundary({{ cookie: () => '', origin:'https://forge.example', base:'/dashboard/', requestTimeoutMs:1000, fetchImpl: (_url, options = {{}}) => new Promise(() => {{ options.signal.addEventListener('abort', () => {{ requestAborted = true; }}); }}), location: {{ replace: () => {{}} }}, }}); (async () => {{ const started = Date.now(); const pending = boundary.fetch('/dashboard/api/v1/context', {{signal:caller.signal}}); setTimeout(() => caller.abort(new DOMException('Superseded', 'AbortError')), 10); try {{ await pending; }} catch (error) {{ process.stdout.write(JSON.stringify({{name:error.name, message:error.message, source:error.source, requestAborted, elapsed:Date.now()-started}})); }} }})().catch(error => {{ console.error(error); process.exit(1); }}); """ result = subprocess.run( ["node", "-e", script], text=True, capture_output=True, check=True, timeout=2 ) output = json.loads(result.stdout) assert output["name"] == "AbortError" assert output["message"] == "Superseded" assert output["source"] == "caller" assert output["requestAborted"] is True assert output["elapsed"] < 100 def test_request_object_abort_signal_is_composed_with_the_api_deadline(): script = f""" const createSessionBoundary = require({json.dumps(str(SESSION_JS))}); const caller = new AbortController(); const boundary = createSessionBoundary({{ cookie: () => '', origin:'https://forge.example', base:'/dashboard/', requestTimeoutMs:1000, fetchImpl: () => new Promise(() => {{}}), location: {{replace:() => {{}}}}, }}); (async () => {{ const request = new Request('https://forge.example/dashboard/api/v1/context', {{signal:caller.signal}}); const pending = boundary.fetch(request); setTimeout(() => caller.abort(new DOMException('Request superseded', 'AbortError')), 5); try {{ await pending; }} catch (error) {{ process.stdout.write(JSON.stringify({{name:error.name, message:error.message, source:error.source}})); }} }})().catch(error => {{ console.error(error); process.exit(1); }}); """ result = subprocess.run( ["node", "-e", script], text=True, capture_output=True, check=True, timeout=2 ) assert json.loads(result.stdout) == { "name": "AbortError", "message": "Request superseded", "source": "caller", } def test_mutation_timeout_warns_that_the_server_outcome_may_be_ambiguous(): script = f""" const createSessionBoundary = require({json.dumps(str(SESSION_JS))}); const boundary = createSessionBoundary({{ cookie: () => 'stackchain_csrf=proof', origin:'https://forge.example', base:'/dashboard/', requestTimeoutMs:5, fetchImpl: () => new Promise(() => {{}}), location: {{replace:() => {{}}}}, }}); (async () => {{ try {{ await boundary.fetch('/dashboard/api/v1/repos/stackchain/app/issues/9', {{method:'PATCH'}}); }} catch (error) {{ process.stdout.write(JSON.stringify({{name:error.name, message:error.message, method:error.method, safeToRetry:error.safeToRetry}})); }} }})().catch(error => {{ console.error(error); process.exit(1); }}); """ result = subprocess.run( ["node", "-e", script], text=True, capture_output=True, check=True, timeout=2 ) output = json.loads(result.stdout) assert output == { "name": "TimeoutError", "message": "Request timed out. Refresh to verify the outcome before retrying.", "method": "PATCH", "safeToRetry": False, } def test_fresh_authorization_request_uses_the_same_deadline_boundary(): script = f""" const createSessionBoundary = require({json.dumps(str(SESSION_JS))}); let calls = 0; const boundary = createSessionBoundary({{ cookie: () => 'stackchain_csrf=proof', origin:'https://forge.example', base:'/dashboard/', requestTimeoutMs:5, fetchImpl: () => {{ calls += 1; if (calls === 1) return Promise.resolve(new Response(JSON.stringify({{detail:{{code:'step_up_required', action:'merge_pull', target:'stackchain/app#9'}}}}), {{status:428, headers:{{'Content-Type':'application/json'}}}})); return new Promise(() => {{}}); }}, promptAuthorization: async () => 'operator-token', location: {{replace:() => {{}}}}, }}); (async () => {{ try {{ await boundary.fetch('/dashboard/api/v1/repos/stackchain/app/pulls/9/merge', {{method:'POST'}}); }} catch (error) {{ process.stdout.write(JSON.stringify({{name:error.name, phase:error.phase, calls}})); }} }})().catch(error => {{ console.error(error); process.exit(1); }}); """ result = subprocess.run( ["node", "-e", script], text=True, capture_output=True, check=True, timeout=2 ) assert json.loads(result.stdout) == { "name": "TimeoutError", "phase": "fresh-authorization", "calls": 2, } def test_mutating_same_origin_fetch_receives_csrf_proof(): result = run_session_scenario( """ await boundary.fetch('/dashboard/api/v1/notifications/7/read', { method: 'PATCH' }); process.stdout.write(JSON.stringify(state)); """ ) assert result["requests"][0]["headers"]["x-csrf-token"] == "csrf-proof" def test_user_activity_heartbeat_is_throttled_across_pointer_keyboard_and_touch_events(): result = run_session_scenario( """ boundary.startActivityHeartbeat(); await state.activityListeners.pointerdown(); await state.activityListeners.keydown(); state.now += 60000; await state.activityListeners.touchstart(); process.stdout.write(JSON.stringify(state)); """ ) assert [request["url"] for request in result["requests"]] == [ "/dashboard/api/v1/session/activity", "/dashboard/api/v1/session/activity", ] assert all(request["method"] == "POST" for request in result["requests"]) assert all( request["headers"]["x-csrf-token"] == "csrf-proof" for request in result["requests"] ) def test_authenticated_session_status_persists_offline_lease_for_worker_and_expiry_timer(): result = run_session_scenario( """ state.responsePayload = {authenticated:true, csrf_token:'csrf-proof', expires_at:4102444800, idle_expires_at:4102441200}; const valid = await boundary.refreshOfflineLease(); state.valid = valid; state.savedExpiry = storage.getItem('stackchain.session-expires-at'); state.savedIdleExpiry = storage.getItem('stackchain.session-idle-expires-at'); process.stdout.write(JSON.stringify(state)); """ ) assert result["valid"] is True assert result["savedExpiry"] == "4102444800" assert result["savedIdleExpiry"] == "4102441200" assert result["workerMessages"] == [ { "type": "stackchain-session-lease", "expiresAt": 4102444800, "idleExpiresAt": 4102441200, } ] assert result["leaseDelay"] > 0 def test_expired_idle_lease_locks_offline_without_purging_private_work(): result = run_session_scenario( """ storage.setItem('stackchain.session-expires-at', '200'); storage.setItem('stackchain.session-idle-expires-at', '99'); state.failFetch = true; const valid = await boundary.refreshOfflineLease(); state.valid = valid; state.remaining = Array.from(storage.values.keys()); process.stdout.write(JSON.stringify(state)); """ ) assert result["valid"] is False assert result["remaining"] == [ "stackchain.private", "gitea.preference", "stackchain.session-expires-at", "stackchain.session-idle-expires-at", ] assert result["deletedDatabases"] == [] assert result["deletedCaches"] == [] assert result["replaced"] == ["/dashboard/login?reason=session-idle"] def test_successful_activity_heartbeat_republishes_only_confirmed_idle_deadline(): result = run_session_scenario( """ storage.setItem('stackchain.session-expires-at', '4102444800'); state.responsePayload = {active:true, idle_expires_at:4102441300}; const recorded = await boundary.recordActivity(); state.recorded = recorded; state.savedIdleExpiry = storage.getItem('stackchain.session-idle-expires-at'); process.stdout.write(JSON.stringify(state)); """ ) assert result["recorded"] is True assert result["savedIdleExpiry"] == "4102441300" assert result["workerMessages"] == [ { "type": "stackchain-session-lease", "expiresAt": 4102444800, "idleExpiresAt": 4102441300, } ] def test_absolute_expiry_still_purges_after_an_idle_lock(): result = run_session_scenario( """ state.responsePayload = {authenticated:true, expires_at:200, idle_expires_at:150}; await boundary.refreshOfflineLease(); state.now = 150000; await state.timers[1].callback(); state.deletedAtIdle = [...state.deletedDatabases]; state.now = 200000; await state.timers[0].callback(); state.remaining = Array.from(storage.values.keys()); process.stdout.write(JSON.stringify(state)); """ ) assert result["deletedAtIdle"] == [] assert result["deletedDatabases"] == [ "stackchain-background-outbox-v1", "stackchain-offline-work-v2", "stackchain-unfiled-captures-v1", ] assert result["remaining"] == ["gitea.preference"] assert result["replaced"] == [ "/dashboard/login?reason=session-idle", "/dashboard/login?reason=session-expired", ] def test_expired_stored_lease_purges_private_work_when_session_status_is_offline(): result = run_session_scenario( """ storage.setItem('stackchain.session-expires-at', '1'); state.failFetch = true; const valid = await boundary.refreshOfflineLease(); state.valid = valid; state.remaining = Array.from(storage.values.keys()); state.replacedAfterDeletion = state.deletionCompleted && state.replaced.length === 1; process.stdout.write(JSON.stringify(state)); """ ) assert result["valid"] is False assert result["remaining"] == ["gitea.preference"] assert result["deletedDatabases"] == [ "stackchain-background-outbox-v1", "stackchain-offline-work-v2", "stackchain-unfiled-captures-v1", ] assert result["deletedCaches"] == ["stackchain-dashboard-shell-v15"] assert result["replaced"] == ["/dashboard/login?reason=session-expired"] assert result["replacedAfterDeletion"] is True def test_enroll_passkey_bootstraps_with_fresh_authorization_and_web_authentication(): script = f""" const createSessionBoundary=require({json.dumps(str(SESSION_JS))}); const requests=[]; const responses=[ {{status:428,payload:{{detail:{{code:'step_up_required',action:'enroll_passkey',target:'current_device'}}}}}}, {{status:201,payload:{{grant:'bootstrap-grant',expires_in:90}}}}, {{status:201,payload:{{challenge:'AQID',rp:{{id:'forge.example',name:'Stackchain Dashboard'}},user:{{id:'BAUG',name:'stackchain-operator',displayName:'Stackchain operator'}},pubKeyCredParams:[],excludeCredentials:[],authenticatorSelection:{{userVerification:'required'}}}}}}, {{status:201,payload:{{enrolled:true}}}}, ]; const credential={{id:'new-passkey',type:'public-key',rawId:Uint8Array.from([1]).buffer,response:{{attestationObject:Uint8Array.from([2]).buffer,clientDataJSON:Uint8Array.from([3]).buffer,getTransports:()=>['internal']}}}}; const boundary=createSessionBoundary({{ cookie:()=> 'stackchain_csrf=proof',origin:'https://forge.example',base:'/dashboard/', credentials:{{create:async options=>{{globalThis.creation=options.publicKey;return credential;}}}}, promptAuthorization:async()=> 'recovery-token', fetchImpl:async(url,options={{}})=>{{requests.push({{url:String(url),body:options.body||null,grant:new Headers(options.headers||{{}}).get('X-Step-Up-Grant')}});const item=responses.shift();return new Response(JSON.stringify(item.payload),{{status:item.status,headers:{{'Content-Type':'application/json'}}}});}}, location:{{replace:()=>{{}}}}, }}); (async()=>{{const enrolled=await boundary.enrollPasskey();process.stdout.write(JSON.stringify({{enrolled,requests,creation:globalThis.creation}}));}})().catch(error=>{{console.error(error);process.exit(1);}}); """ result = subprocess.run( ["node", "-e", script], text=True, capture_output=True, check=True ) output = json.loads(result.stdout) assert output["enrolled"] is True assert output["creation"]["challenge"] == {"0": 1, "1": 2, "2": 3} assert output["creation"]["user"]["id"] == {"0": 4, "1": 5, "2": 6} assert output["requests"][2]["grant"] == "bootstrap-grant" verification = json.loads(output["requests"][3]["body"]) assert verification["challenge"] == "AQID" assert verification["credential"]["response"]["transports"] == ["internal"] def test_high_impact_fetch_uses_passkey_before_access_token_fallback(): script = f""" const createSessionBoundary = require({json.dumps(str(SESSION_JS))}); const requests=[]; const responses=[ {{status:428,payload:{{detail:{{code:'step_up_required',action:'close_issue',target:'stackchain/api#7'}}}}}}, {{status:200,payload:{{challenge:'AQID',rpId:'forge.example',userVerification:'required',allowCredentials:[{{type:'public-key',id:'BAUG'}}]}}}}, {{status:201,payload:{{grant:'passkey-grant',expires_in:90}}}}, {{status:200,payload:{{closed:true}}}}, ]; let prompted=false; const credential={{id:'credential',type:'public-key',rawId:Uint8Array.from([4,5,6]).buffer,response:{{authenticatorData:Uint8Array.from([7]).buffer,clientDataJSON:Uint8Array.from([8]).buffer,signature:Uint8Array.from([9]).buffer,userHandle:null}}}}; const boundary=createSessionBoundary({{ cookie:()=> 'stackchain_csrf=proof',origin:'https://forge.example',base:'/dashboard/', credentials:{{get:async()=>credential}}, promptAuthorization:async()=>{{prompted=true;return 'must-not-be-requested';}}, fetchImpl:async(url,options={{}})=>{{requests.push({{url:String(url),body:options.body||null,grant:new Headers(options.headers||{{}}).get('X-Step-Up-Grant')}});const item=responses.shift();return new Response(JSON.stringify(item.payload),{{status:item.status,headers:{{'Content-Type':'application/json'}}}});}}, location:{{replace:()=>{{}}}}, }}); (async()=>{{const response=await boundary.fetch('/dashboard/api/v1/repos/stackchain/api/issues/7/close',{{method:'PATCH'}});process.stdout.write(JSON.stringify({{status:response.status,requests,prompted}}));}})().catch(error=>{{console.error(error);process.exit(1);}}); """ result = subprocess.run( ["node", "-e", script], text=True, capture_output=True, check=True ) output = json.loads(result.stdout) assert output["status"] == 200 assert output["prompted"] is False assert output["requests"][1]["url"].endswith("/passkeys/authorization/options") assert json.loads(output["requests"][1]["body"]) == { "action": "close_issue", "target": "stackchain/api#7", } assert output["requests"][2]["url"].endswith("/passkeys/authorization/verify") assert output["requests"][3]["grant"] == "passkey-grant" def test_high_impact_fetch_prompts_once_and_retries_original_request_with_grant(): result = run_session_scenario( """ state.responses = [ {status:428, payload:{detail:{code:'step_up_required', action:'merge_pull', target:'stackchain/api#7'}}}, {status:201, payload:{grant:'one-time-grant', expires_in:90}}, {status:200, payload:{merged:true}}, ]; const original = { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({expected_head_sha:'abc123'}) }; const response = await boundary.fetch('/dashboard/api/v1/repos/stackchain/api/pulls/7/merge', original); state.finalStatus = response.status; process.stdout.write(JSON.stringify(state)); """ ) assert result["finalStatus"] == 200 assert result["prompts"] == [ {"action": "merge_pull", "target": "stackchain/api#7"} ] assert [request["url"] for request in result["requests"]] == [ "/dashboard/api/v1/repos/stackchain/api/pulls/7/merge", "/dashboard/api/v1/fresh-authorization", "/dashboard/api/v1/repos/stackchain/api/pulls/7/merge", ] assert json.loads(result["requests"][1]["body"]) == { "access_token": "correct horse battery staple", "action": "merge_pull", "target": "stackchain/api#7", } assert result["requests"][2]["headers"]["x-step-up-grant"] == "one-time-grant" assert result["requests"][2]["body"] == result["requests"][0]["body"] def test_same_origin_unauthorized_response_purges_private_work_before_replacing_dashboard_once(): result = run_session_scenario( """ state.responseStatus = 401; await Promise.all([ boundary.fetch('/dashboard/api/v1/live'), boundary.fetch('https://forge.example/dashboard/api/v1/context'), ]); state.remaining = Array.from(storage.values.keys()); state.replacedAfterDeletion = state.deletionCompleted && state.replaced.length === 1; process.stdout.write(JSON.stringify(state)); """ ) assert result["replaced"] == [ "/dashboard/login?reason=session-expired" ] assert result["remaining"] == ["gitea.preference"] assert result["deletedDatabases"] == [ "stackchain-background-outbox-v1", "stackchain-offline-work-v2", "stackchain-unfiled-captures-v1", ] assert result["deletedCaches"] == ["stackchain-dashboard-shell-v15"] assert result["workerMessages"] == [{"type": "stackchain-purge-outbox"}] assert result["expiredAfterDeletion"] is True assert result["replacedAfterDeletion"] is True def test_remotely_revoked_response_clears_private_work_before_replacing_dashboard(): result = run_session_scenario( """ state.responseStatus = 401; state.responsePayload = {detail:'Authentication required', code:'session_revoked'}; await boundary.fetch('/dashboard/api/v1/live'); state.remaining = Array.from(storage.values.keys()); state.replacedAfterDeletion = state.deletionCompleted && state.replaced.length === 1; process.stdout.write(JSON.stringify(state)); """ ) assert result["remaining"] == ["gitea.preference"] assert result["deletedDatabases"] == [ "stackchain-background-outbox-v1", "stackchain-offline-work-v2", "stackchain-unfiled-captures-v1", ] assert result["deletedCaches"] == ["stackchain-dashboard-shell-v15"] assert result["workerMessages"] == [{"type": "stackchain-purge-outbox"}] assert result["replaced"] == ["/dashboard/login?reason=session-revoked"] assert result["replacedAfterDeletion"] is True def test_idle_response_locks_without_clearing_private_queued_work(): result = run_session_scenario( """ state.responseStatus = 401; state.responsePayload = {detail:'Authentication required', code:'session_idle'}; await boundary.fetch('/dashboard/api/v1/live'); state.remaining = Array.from(storage.values.keys()); process.stdout.write(JSON.stringify(state)); """ ) assert result["remaining"] == ["stackchain.private", "gitea.preference"] assert result["deletedDatabases"] == [] assert result["deletedCaches"] == [] assert result["workerMessages"] == [] assert result["replaced"] == ["/dashboard/login?reason=session-idle"] def test_worker_revocation_message_clears_window_storage_before_login(): result = run_session_scenario( """ await boundary.handleServiceWorkerMessage({data:{type:'stackchain-session-revoked'}}); state.remaining = Array.from(storage.values.keys()); process.stdout.write(JSON.stringify(state)); """ ) assert result["remaining"] == ["gitea.preference"] assert result["replaced"] == ["/dashboard/login?reason=session-revoked"] def test_worker_idle_message_locks_without_clearing_window_storage(): result = run_session_scenario( """ await boundary.handleServiceWorkerMessage({data:{type:'stackchain-session-idle'}}); state.remaining = Array.from(storage.values.keys()); process.stdout.write(JSON.stringify(state)); """ ) assert result["remaining"] == ["stackchain.private", "gitea.preference"] assert result["replaced"] == ["/dashboard/login?reason=session-idle"] def test_worker_expiry_message_clears_window_storage_before_expired_login(): result = run_session_scenario( """ await boundary.handleServiceWorkerMessage({data:{type:'stackchain-session-expired'}}); state.remaining = Array.from(storage.values.keys()); state.replacedAfterDeletion = state.deletionCompleted && state.replaced.length === 1; process.stdout.write(JSON.stringify(state)); """ ) assert result["remaining"] == ["gitea.preference"] assert result["replaced"] == ["/dashboard/login?reason=session-expired"] assert result["replacedAfterDeletion"] is True def test_cross_origin_unauthorized_response_does_not_expire_dashboard_session(): result = run_session_scenario( """ state.responseStatus = 401; await boundary.fetch('https://untrusted.example/api/private'); process.stdout.write(JSON.stringify(state)); """ ) assert result["replaced"] == [] def test_sign_out_clears_only_dashboard_private_device_state(): result = run_session_scenario( """ await boundary.signOut(); state.remaining = Array.from(storage.values.keys()); process.stdout.write(JSON.stringify(state)); """ ) request = result["requests"][0] assert request["url"] == "/dashboard/api/v1/session" assert request["method"] == "DELETE" assert request["headers"]["x-csrf-token"] == "csrf-proof" assert "stackchain.private" in result["removed"] assert result["remaining"] == ["gitea.preference"] assert result["deletedDatabases"] == [ "stackchain-background-outbox-v1", "stackchain-offline-work-v2", "stackchain-unfiled-captures-v1", ] assert result["deletedCaches"] == ["stackchain-dashboard-shell-v15"] assert result["assigned"] == "/dashboard/login" assert result["assignedAfterDeletion"] is True def test_sign_out_stays_on_page_and_reports_failed_private_outbox_deletion(): result = run_session_scenario( """ state.failDeletion = true; try { await boundary.signOut(); } catch (error) { state.signOutError = error.message; } process.stdout.write(JSON.stringify(state)); """ ) assert result["assigned"] == "" assert result["clearErrors"] == ["Could not clear private queued work from this device."] assert result["signOutError"] == "Could not clear private queued work from this device." def test_sign_out_all_devices_requires_confirmation_and_uses_global_endpoint(): result = run_session_scenario( """ await boundary.signOutAllDevices(); state.remaining = Array.from(storage.values.keys()); process.stdout.write(JSON.stringify(state)); """ ) assert result["confirmations"] == [ "Sign out every device? You will need to sign in again everywhere." ] request = result["requests"][0] assert request["url"] == "/dashboard/api/v1/sessions" assert request["method"] == "DELETE" assert request["headers"]["x-csrf-token"] == "csrf-proof" assert result["remaining"] == ["gitea.preference"] assert result["assigned"] == "/dashboard/login" def test_active_devices_can_be_loaded_and_one_remote_device_revoked(): result = run_session_scenario( """ state.responsePayload = {devices:[ {management_id:'remote-id-123456789', device_label:'Pixel ' in html assert html.index('static/session.js') < html.index('static/markdown.js') assert '' in html assert '' in html assert '' in html assert 'id="active-devices-sheet"' in html assert 'aria-label="Active devices and passkeys"' in html assert 'aria-labelledby="enrolled-passkeys-title"' in html assert 'id="enrolled-passkeys-list"' in html assert 'A recovery token will be required after sign-out if you remove this device’s passkey.' in html assert 'aria-labelledby="security-activity-title"' in html assert 'id="security-activity-list"' in html assert '>Load older activity' in html assert '#sign-out-all { min-height:44px; }' in html def test_security_activity_names_confirmed_gitea_time_logging(): source = SECURITY_CENTER_JS.read_text() assert "gitea_time_logged: 'Gitea time logged'" in source