fix: recover stalled mobile sign-in (Closes #767)
This commit is contained in:
1 parent
9bd755f521
commit
6e2f5d1120
2 files changed
+157
-9
No files matched your search
@@ -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] }},
|
||||
|
||||
Reference in new issue
Block a user