Automatic update from web-platform-tests [digital-credentials] Test that coordinator returns to idle after mid-flight abort (#60262) Adds a regression test ensuring that after a get() call is aborted via AbortSignal mid-flight, a subsequent get() call works (i.e., the user agent's interaction state has returned to "idle" and is not stuck in "requesting"). This guards against state-machine bugs where an aborted credential request leaves the coordinator unable to accept new requests. -- wpt-commits: 2c78775094da0c30803074399d48b52592db6bb1 wpt-pr: 60262
364 lines
12 KiB
HTML
364 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<title>Digital Credential tests.</title>
|
|
<link rel="help" href="https://w3c-fedid.github.io/digital-credentials/" />
|
|
<script src="/common/get-host-info.sub.js"></script>
|
|
<script src="/resources/testharness.js"></script>
|
|
<script src="/resources/testharnessreport.js"></script>
|
|
<script src="/resources/testdriver.js"></script>
|
|
<script src="/resources/testdriver-vendor.js"></script>
|
|
|
|
<body>
|
|
<iframe id="cross-origin" allow="digital-credentials-get"></iframe>
|
|
<iframe id="same-origin"></iframe>
|
|
</body>
|
|
<script type="module">
|
|
import { makeGetOptions, sendMessage, loadIframe } from "./support/helper.js";
|
|
|
|
const iframeSameOrigin = document.querySelector("iframe#same-origin");
|
|
const iframeCrossOrigin = document.querySelector("iframe#cross-origin");
|
|
|
|
promise_setup(async () => {
|
|
const hostInfo = get_host_info();
|
|
await Promise.all([
|
|
loadIframe(
|
|
iframeCrossOrigin,
|
|
`${hostInfo.HTTPS_REMOTE_ORIGIN}/digital-credentials/support/iframe.html`
|
|
),
|
|
loadIframe(iframeSameOrigin, "/digital-credentials/support/iframe.html"),
|
|
]);
|
|
});
|
|
|
|
promise_test(async (t) => {
|
|
const invalidData = [null, "", 123, true, false];
|
|
for (const data of invalidData) {
|
|
const options = makeGetOptions({ data });
|
|
await promise_rejects_js(
|
|
t,
|
|
TypeError,
|
|
navigator.credentials.get(options)
|
|
);
|
|
}
|
|
}, "Type conversion happens on the data member of the DigitalCredentialGetRequest object.");
|
|
|
|
promise_test(async (t) => {
|
|
iframeSameOrigin.focus();
|
|
for (const global of [window, iframeSameOrigin.contentWindow]) {
|
|
await promise_rejects_dom(
|
|
t,
|
|
"NotSupportedError",
|
|
global.DOMException,
|
|
global.navigator.credentials.get()
|
|
);
|
|
|
|
await promise_rejects_dom(
|
|
t,
|
|
"NotSupportedError",
|
|
global.DOMException,
|
|
global.navigator.credentials.get({})
|
|
);
|
|
|
|
await promise_rejects_dom(
|
|
t,
|
|
"NotSupportedError",
|
|
global.DOMException,
|
|
global.navigator.credentials.get({ x: "y" })
|
|
);
|
|
|
|
await promise_rejects_dom(
|
|
t,
|
|
"NotSupportedError",
|
|
global.DOMException,
|
|
global.navigator.credentials.get({ x: "y", y: "z" })
|
|
);
|
|
|
|
await promise_rejects_dom(
|
|
t,
|
|
"NotSupportedError",
|
|
global.DOMException,
|
|
global.navigator.credentials.get({ mediation: "required" })
|
|
);
|
|
|
|
const abortController = new AbortController();
|
|
const { signal } = abortController;
|
|
|
|
await promise_rejects_dom(
|
|
t,
|
|
"NotSupportedError",
|
|
global.DOMException,
|
|
global.navigator.credentials.get({ signal })
|
|
);
|
|
|
|
await promise_rejects_dom(
|
|
t,
|
|
"NotSupportedError",
|
|
global.DOMException,
|
|
global.navigator.credentials.get({ signal, mediation: "required" })
|
|
);
|
|
}
|
|
}, "Calling navigator.credentials.get() without a digital member same origin.");
|
|
|
|
promise_test(async (t) => {
|
|
for (const r of [undefined, []]) {
|
|
const options = {
|
|
digital: {
|
|
requests: r
|
|
},
|
|
};
|
|
await promise_rejects_js(
|
|
t,
|
|
TypeError,
|
|
navigator.credentials.get(options)
|
|
);
|
|
}
|
|
}, "navigator.credentials.get() API rejects if there are no credential request.");
|
|
|
|
promise_test(async (t) => {
|
|
iframeSameOrigin.focus();
|
|
const { contentWindow: iframeWindow } = iframeSameOrigin;
|
|
for (const r of [undefined, []]) {
|
|
const options = {
|
|
digital: {
|
|
requests: r
|
|
},
|
|
};
|
|
await test_driver.bless("user activation");
|
|
await promise_rejects_js(
|
|
t,
|
|
iframeWindow.TypeError,
|
|
iframeWindow.navigator.credentials.get(options)
|
|
);
|
|
}
|
|
}, "navigator.credentials.get() API rejects if there are no credential request for same-origin iframe.");
|
|
|
|
promise_test(async (t) => {
|
|
iframeCrossOrigin.focus();
|
|
for (const r of [undefined, []]) {
|
|
const options = {
|
|
digital: {
|
|
requests: r
|
|
},
|
|
};
|
|
const result = await sendMessage(iframeCrossOrigin, {
|
|
action: "get",
|
|
options,
|
|
});
|
|
assert_equals(result.constructor, "TypeError");
|
|
}
|
|
}, "navigator.credentials.get() API rejects if there are no credential request in cross-origin iframe.");
|
|
|
|
promise_test(async (t) => {
|
|
const abortController = new AbortController();
|
|
const { signal } = abortController;
|
|
abortController.abort();
|
|
for (const options of [{ signal }, makeGetOptions({protocol: [], signal})]) {
|
|
await promise_rejects_dom(
|
|
t,
|
|
"AbortError",
|
|
navigator.credentials.get(options)
|
|
);
|
|
}
|
|
}, "navigator.credentials.get() promise is rejected if called with an aborted controller.");
|
|
|
|
promise_test(async (t) => {
|
|
iframeSameOrigin.focus();
|
|
const { contentWindow: iframeWindow } = iframeSameOrigin;
|
|
const abortController = new iframeWindow.AbortController();
|
|
const { signal } = abortController;
|
|
abortController.abort();
|
|
for (const options of [{ signal }, makeGetOptions({protocol: [], signal})]) {
|
|
await test_driver.bless("user activation");
|
|
await promise_rejects_dom(
|
|
t,
|
|
"AbortError",
|
|
iframeWindow.DOMException,
|
|
iframeWindow.navigator.credentials.get(options)
|
|
);
|
|
assert_true(
|
|
navigator.userActivation.isActive,
|
|
"User activation is still active."
|
|
);
|
|
}
|
|
}, "navigator.credentials.get() promise is rejected if called with an aborted controller in same-origin iframe.");
|
|
|
|
promise_test(async (t) => {
|
|
iframeCrossOrigin.focus();
|
|
for (const options of [undefined, {}, makeGetOptions({protocol: []})]) {
|
|
const result = await sendMessage(iframeCrossOrigin, {
|
|
abort: "before",
|
|
action: "get",
|
|
options,
|
|
});
|
|
assert_equals(result.constructor, "DOMException");
|
|
assert_equals(result.name, "AbortError");
|
|
}
|
|
}, "navigator.credentials.get() promise is rejected if called with an aborted signal in cross-origin iframe.");
|
|
|
|
promise_test(async (t) => {
|
|
const abortController = new AbortController();
|
|
const { signal } = abortController;
|
|
const options = makeGetOptions({signal});
|
|
await test_driver.bless("user activation");
|
|
const promise = promise_rejects_dom(
|
|
t,
|
|
"AbortError",
|
|
DOMException,
|
|
navigator.credentials.get(options)
|
|
);
|
|
abortController.abort();
|
|
await promise;
|
|
}, "navigator.credentials.get() promise is rejected if abort controller is aborted after call to get().");
|
|
|
|
promise_test(async (t) => {
|
|
iframeCrossOrigin.focus();
|
|
const result = await sendMessage(iframeCrossOrigin, {
|
|
abort: "after",
|
|
action: "get",
|
|
needsActivation: true,
|
|
options: makeGetOptions(),
|
|
});
|
|
assert_equals(result.constructor, "DOMException");
|
|
assert_equals(result.name, "AbortError");
|
|
}, "navigator.credentials.get() promise is rejected if abort controller is aborted after call to get() in cross-origin iframe.");
|
|
|
|
promise_test(async (t) => {
|
|
for (const mediation of ["silent", "optional", "conditional", "required"]) {
|
|
const abortController = new AbortController();
|
|
const { signal } = abortController;
|
|
await test_driver.bless("user activation");
|
|
const requestPromise = navigator.credentials.get(makeGetOptions({
|
|
mediation,
|
|
signal,
|
|
}));
|
|
abortController.abort(new Error(`Aborted for testing mediation "${mediation}"`));
|
|
await promise_rejects_js(t, Error, requestPromise);
|
|
}
|
|
}, "Mediation is implicitly required and hence ignored. Request is aborted regardless.");
|
|
|
|
promise_test(async t => {
|
|
const throwingValues = [
|
|
{ val: BigInt(123) },
|
|
(() => { const o = {}; o.self = o; return o; })(),
|
|
{ toJSON() { throw new TypeError("toJSON throws"); } }
|
|
];
|
|
|
|
for (const badValue of throwingValues) {
|
|
const options = makeGetOptions({ data: badValue });
|
|
await test_driver.bless("user activation");
|
|
await promise_rejects_js(
|
|
t,
|
|
TypeError,
|
|
navigator.credentials.get(options),
|
|
`Should throw for: ${String(badValue)}`
|
|
);
|
|
}
|
|
}, "Throws TypeError when request data is not JSON stringifiable.");
|
|
|
|
promise_test(async (t) => {
|
|
await promise_rejects_js(
|
|
t,
|
|
TypeError,
|
|
navigator.credentials.get({digital: {}}));
|
|
}, "`requests` field is required in the options object.");
|
|
|
|
promise_test(async (t) => {
|
|
const controller = new AbortController();
|
|
const signal = controller.signal;
|
|
|
|
// makeGetOptions() with a manual request in `requests` will create a mix
|
|
// of that request plus the default canonical ones (openid4vp, org-iso-mdoc).
|
|
const options = makeGetOptions({
|
|
requests: [
|
|
{
|
|
protocol: "unknown-protocol",
|
|
data: { val: BigInt(1) }, // malformed/non-serializable
|
|
},
|
|
],
|
|
signal,
|
|
});
|
|
|
|
await test_driver.bless("user activation");
|
|
const promise = navigator.credentials.get(options);
|
|
controller.abort();
|
|
await promise_rejects_dom(t, "AbortError", promise);
|
|
}, "navigator.credentials.get() with one unknown protocol with malformed data and one known protocol with proper data should skip the unknown one and not fail.");
|
|
|
|
promise_test(async (t) => {
|
|
const options = {
|
|
digital: {
|
|
requests: [
|
|
{ protocol: "bogus-1", data: { val: BigInt(1) } },
|
|
{ protocol: "bogus-2", data: { val: BigInt(2) } },
|
|
{ protocol: "bogus-3", data: { val: BigInt(3) } },
|
|
],
|
|
},
|
|
};
|
|
|
|
await test_driver.bless("user activation");
|
|
await promise_rejects_js(t, TypeError, navigator.credentials.get(options));
|
|
}, "navigator.credentials.get() with only bogus requests should reject with TypeError.");
|
|
|
|
promise_test(async (t) => {
|
|
const controller = new AbortController();
|
|
const signal = controller.signal;
|
|
|
|
const [openidReq] = makeGetOptions({ protocol: "openid4vp-v1-unsigned" }).digital.requests;
|
|
const [mdocReq] = makeGetOptions({ protocol: "org-iso-mdoc" }).digital.requests;
|
|
|
|
const options = makeGetOptions({
|
|
protocol: [], // disable defaults
|
|
requests: [
|
|
openidReq,
|
|
{ protocol: "bogus-1", data: { val: BigInt(1) } },
|
|
{ protocol: "bogus-2", data: { val: BigInt(2) } },
|
|
mdocReq,
|
|
],
|
|
signal,
|
|
});
|
|
|
|
await test_driver.bless("user activation");
|
|
const promise = navigator.credentials.get(options);
|
|
controller.abort();
|
|
await promise_rejects_dom(t, "AbortError", promise);
|
|
}, "navigator.credentials.get() with good requests at the start and end should skip the bogus ones and not fail.");
|
|
|
|
promise_test(async (t) => {
|
|
const controller = new AbortController();
|
|
const signal = controller.signal;
|
|
|
|
const [openidReq] = makeGetOptions({ protocol: "openid4vp-v1-unsigned" }).digital.requests;
|
|
const [mdocReq] = makeGetOptions({ protocol: "org-iso-mdoc" }).digital.requests;
|
|
|
|
const options = makeGetOptions({
|
|
protocol: [], // disable automatic protocol requests
|
|
requests: [
|
|
{ protocol: "bogus-1", data: { val: BigInt(1) } },
|
|
openidReq,
|
|
mdocReq,
|
|
{ protocol: "bogus-2", data: { val: BigInt(2) } },
|
|
],
|
|
signal,
|
|
});
|
|
|
|
await test_driver.bless("user activation");
|
|
const promise = navigator.credentials.get(options);
|
|
controller.abort();
|
|
await promise_rejects_dom(t, "AbortError", promise);
|
|
}, "navigator.credentials.get() with bogus requests at both ends should skip them and not fail.");
|
|
|
|
// State machine: after a mid-flight abort via AbortSignal, the coordinator
|
|
// must return to the idle state so a subsequent get() works.
|
|
promise_test(async (t) => {
|
|
const ac1 = new AbortController();
|
|
await test_driver.bless("user activation");
|
|
const p1 = navigator.credentials.get(makeGetOptions({ signal: ac1.signal }));
|
|
ac1.abort();
|
|
await promise_rejects_dom(t, "AbortError", DOMException, p1);
|
|
|
|
const ac2 = new AbortController();
|
|
await test_driver.bless("user activation");
|
|
const p2 = navigator.credentials.get(makeGetOptions({ signal: ac2.signal }));
|
|
ac2.abort();
|
|
await promise_rejects_dom(t, "AbortError", DOMException, p2);
|
|
}, "navigator.credentials.get() returns coordinator to idle after mid-flight abort.");
|
|
</script>
|