fix: recover stalled mobile sign-in (Closes #767)
CI / lint (pull_request) Successful in 1m32s
CI / build-release (pull_request) Successful in 5s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-13 19:52:44 +00:00
1 parent 9bd755f521
commit 6e2f5d1120
2 files changed
+157 -9

No files matched your search

+113 -1
View File
@@ -169,6 +169,56 @@ const controller = createLoginController({{
}
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))});
@@ -254,6 +304,68 @@ const controller = createLoginController({{
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))});
@@ -343,7 +455,7 @@ const elements = {{
'submit-sign-in': {{ disabled: false }},
}};
const context = {{
URLSearchParams, Response, setInterval, clearInterval,
URLSearchParams, Response, AbortController, setInterval, clearInterval, setTimeout, clearTimeout,
FormData: function () {{ return {{ get: () => 'operator-token' }}; }},
fetch: async () => new Response('{{}}', {{ status: 200 }}),
document: {{ getElementById: id => elements[id] }},