diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /testing/web-platform/tests/credential-management/support | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/credential-management/support')
26 files changed, 690 insertions, 0 deletions
diff --git a/testing/web-platform/tests/credential-management/support/README.md b/testing/web-platform/tests/credential-management/support/README.md new file mode 100644 index 0000000000..a6d33ff6f7 --- /dev/null +++ b/testing/web-platform/tests/credential-management/support/README.md @@ -0,0 +1,54 @@ +# CredentialManagement Testing + +## OTPCredential Testing + +In this test suite `otpcredential-helper.js` is a testing framework that enables +engines to test OTPCredential by intercepting the connection between the browser +and the underlying operating system and mock its behavior. + +Usage: + +1. Include the following in your test: +```html +<script src="/resources/test-only-api.js"></script> +<script src="support/otpcredential-helper.js"></script> +``` +2. Set expectations +```javascript +await expect(receive).andReturn(() => { + // mock behavior +}) +``` +3. Call `navigator.credentials.get({otp: {transport: ["sms"]}})` +4. Verify results + +The mocking API is browser agnostic and is designed such that other engines +could implement it too. + +Here are the symbols that are exposed to tests that need to be implemented +per engine: + +- function receive(): the main/only function that can be mocked +- function expect(): the main/only function that enables us to mock it +- enum State {kSuccess, kTimeout}: allows you to mock success/failures + +## FedCM Testing + +`fedcm-mojojs-helper.js` exposes `fedcm_mojo_mock_test` which is a specialized +`promise_test` which comes pre-setup with the appropriate mocking infrastructure +to emulate platform federated auth backend. The mock is passed to the test +function as the second parameter. + +Example usage: +``` +<script type="module"> + import {fedcm_mojo_mock_test} from './support/fedcm-mojojs-helper.js'; + + fedcm_mojo_mock_test(async (t, mock) => { + mock.returnToken("https://idp.test/fedcm.json", "a_token"); + assert_equals("a_token", await navigator.credentials.get(options)); + }, "Successfully obtaining a token using mock."); +</script> +``` + +The chromium implementation uses the MojoJS shim. diff --git a/testing/web-platform/tests/credential-management/support/echoing-nester.html b/testing/web-platform/tests/credential-management/support/echoing-nester.html new file mode 100644 index 0000000000..d4f5899da7 --- /dev/null +++ b/testing/web-platform/tests/credential-management/support/echoing-nester.html @@ -0,0 +1,16 @@ +<body> + <script> + window.addEventListener('message', m => { + window.parent.postMessage(m.data, '*'); + }); + + var u = new URL(window.location.href); + var origin = u.searchParams.has('origin') ? u.searchParams.get('origin') : window.origin; + var file = u.searchParams.has('file') ? u.searchParams.get('file') : 'passwordcredential-get.html'; + + var url = origin + "/credential-management/support/" + file; + var i = document.createElement('iframe'); + i.src = url; + document.body.appendChild(i); + </script> +</body> diff --git a/testing/web-platform/tests/credential-management/support/fedcm-helper.sub.js b/testing/web-platform/tests/credential-management/support/fedcm-helper.sub.js new file mode 100644 index 0000000000..87a1337a06 --- /dev/null +++ b/testing/web-platform/tests/credential-management/support/fedcm-helper.sub.js @@ -0,0 +1,99 @@ +const alt_manifest_origin = 'https://{{hosts[alt][]}}:{{ports[https][0]}}'; + +// Set the identity provider cookie. +export function set_fedcm_cookie(host) { + return new Promise(resolve => { + if (host == undefined) { + document.cookie = 'cookie=1; SameSite=Strict; Path=/credential-management/support; Secure'; + resolve(); + } else { + let popup_window = window.open(host + '/credential-management/support/set_cookie'); + + const popup_message_handler = (event) => { + if (event.origin == host) { + popup_window.close(); + window.removeEventListener('message', popup_message_handler); + resolve(); + } + }; + + window.addEventListener('message', popup_message_handler); + } + }); +} + +// Set the alternate identity provider cookie. +export function set_alt_fedcm_cookie() { + return set_fedcm_cookie(alt_manifest_origin); +} + +// Returns FedCM CredentialRequestOptions for which navigator.credentials.get() +// succeeds. +export function default_request_options(manifest_filename) { + if (manifest_filename === undefined) { + manifest_filename = "manifest.py"; + } + const manifest_path = `https://{{host}}:{{ports[https][0]}}/\ +credential-management/support/fedcm/${manifest_filename}`; + return { + identity: { + providers: [{ + configURL: manifest_path, + clientId: '1', + nonce: '2', + }] + } + }; +} + +// Returns alternate FedCM CredentialRequestOptions for which navigator.credentials.get() +// succeeds. +export function default_alt_request_options(manifest_filename) { + if (manifest_filename === undefined) { + manifest_filename = "manifest.py"; + } + const manifest_path = `${alt_manifest_origin}/\ +credential-management/support/fedcm/${manifest_filename}`; + return { + identity: { + providers: [{ + configURL: manifest_path, + clientId: '1', + nonce: '2', + }] + } + }; +} + +// Test wrapper which does FedCM-specific setup. +export function fedcm_test(test_func, test_name) { + promise_test(async t => { + await set_fedcm_cookie(); + await set_alt_fedcm_cookie(); + await test_func(t); + }, test_name); +} + +function select_manifest_impl(manifest_url) { + const url_query = (manifest_url === undefined) + ? '' : '?manifest_url=${manifest_url}'; + + return new Promise(resolve => { + const img = document.createElement('img'); + img.src = 'support/fedcm/select_manifest_in_root_manifest.py?${url_query}'; + img.addEventListener('error', resolve); + document.body.appendChild(img); + }); +} + +// Sets the manifest returned by the next fetch of /.well-known/web_identity +// select_manifest() only affects the next fetch and not any subsequent fetches +// (ex second next fetch). +export function select_manifest(test, test_options) { + // Add cleanup in case that /.well-known/web_identity is not fetched at all. + test.add_cleanup(async () => { + await select_manifest_impl(); + }); + const manifest_url = test_options.identity.providers[0].configURL; + return select_manifest_impl(manifest_url); +} diff --git a/testing/web-platform/tests/credential-management/support/fedcm-iframe-level2.html b/testing/web-platform/tests/credential-management/support/fedcm-iframe-level2.html new file mode 100644 index 0000000000..7622d988ff --- /dev/null +++ b/testing/web-platform/tests/credential-management/support/fedcm-iframe-level2.html @@ -0,0 +1,23 @@ +<!doctype html> +<link rel="help" href="https://wicg.github.io/FedCM"> +<script src="/common/get-host-info.sub.js"></script> +<div id=log> +<script> +'use strict'; + +const host = get_host_info(); +const remoteBaseURL = + host.AUTHENTICATED_ORIGIN + + window.location.pathname.replace(/\/[^\/]*$/, '/'); + +window.onload = async () => { + const urlParams = new URLSearchParams(window.location.search); + var iframe = document.createElement("iframe"); + iframe.src = remoteBaseURL + "fedcm-iframe.html"; + if (urlParams.get("permission") != '0') { + iframe.allow = "identity-credentials-get"; + } + document.body.appendChild(iframe); +}; + +</script> diff --git a/testing/web-platform/tests/credential-management/support/fedcm-iframe.html b/testing/web-platform/tests/credential-management/support/fedcm-iframe.html new file mode 100644 index 0000000000..c57f54e1da --- /dev/null +++ b/testing/web-platform/tests/credential-management/support/fedcm-iframe.html @@ -0,0 +1,23 @@ +<!doctype html> +<script type="module"> +import {default_request_options} from './fedcm-helper.sub.js'; + +// Loading fedcm-iframe.html in the test will make a FedCM call on load, and +// trigger a postMessage upon completion. +// +// message { +// string result: "Pass" | "Fail" +// string token: token.token +// string errorType: error.name +// } + +window.onload = async () => { + try { + const cred = await navigator.credentials.get(default_request_options()); + window.top.postMessage({result: "Pass", token: cred.token}, '*'); + } catch (error) { + window.top.postMessage({result: "Fail", errorType: error.name}, '*'); + } +}; + +</script> diff --git a/testing/web-platform/tests/credential-management/support/fedcm-mock.js b/testing/web-platform/tests/credential-management/support/fedcm-mock.js new file mode 100644 index 0000000000..f52bd6e0e7 --- /dev/null +++ b/testing/web-platform/tests/credential-management/support/fedcm-mock.js @@ -0,0 +1,105 @@ +import { RequestTokenStatus, LogoutRpsStatus, FederatedAuthRequest, FederatedAuthRequestReceiver } from '/gen/third_party/blink/public/mojom/webid/federated_auth_request.mojom.m.js'; + +function toMojoTokenStatus(status) { + return RequestTokenStatus["k" + status]; +} + +// A mock service for responding to federated auth requests. +export class MockFederatedAuthRequest { + constructor() { + this.receiver_ = new FederatedAuthRequestReceiver(this); + this.interceptor_ = new MojoInterfaceInterceptor(FederatedAuthRequest.$interfaceName); + this.interceptor_.oninterfacerequest = e => { + this.receiver_.$.bindHandle(e.handle); + } + this.interceptor_.start(); + this.token_ = null; + this.selected_identity_provider_config_url_ = null; + this.status_ = RequestTokenStatus.kError; + this.logoutRpsStatus_ = LogoutRpsStatus.kError; + this.returnPending_ = false; + this.pendingPromiseResolve_ = null; + } + + // Causes the subsequent `navigator.credentials.get()` to resolve with the token. + returnToken(selected_identity_provider_config_url, token) { + this.status_ = RequestTokenStatus.kSuccess; + this.selected_identity_provider_config_url_ = selected_identity_provider_config_url; + this.token_ = token; + this.returnPending_ = false; + } + + // Causes the subsequent `navigator.credentials.get()` to reject with the error. + returnError(error) { + if (error == "Success") + throw new Error("Success is not a valid error"); + this.status_ = toMojoTokenStatus(error); + this.selected_identity_provider_config_url_ = null; + this.token_ = null; + this.returnPending_ = false; + } + + // Causes the subsequent `navigator.credentials.get()` to return a pending promise + // that can be cancelled using `cancelTokenRequest()`. + returnPendingPromise() { + this.returnPending_ = true; + } + + logoutRpsReturn(status) { + let validated = LogoutRpsStatus[status]; + if (validated === undefined) + throw new Error("Invalid status: " + status); + this.logoutRpsStatus_ = validated; + } + + // Implements + // RequestToken(array<IdentityProviderGetParameters> idp_get_params) => + // (RequestTokenStatus status, + // url.mojom.Url? selected_identity_provider_config_url, + // string? token); + async requestToken(idp_get_params) { + if (this.returnPending_) { + this.pendingPromise_ = new Promise((resolve, reject) => { + this.pendingPromiseResolve_ = resolve; + }); + return this.pendingPromise_; + } + return Promise.resolve({ + status: this.status_, + selected_identity_provider_config_url: this.selected_identity_provider_config_url_, + token: this.token_ + }); + } + + async cancelTokenRequest() { + this.pendingPromiseResolve_({ + status: toMojoTokenStatus("ErrorCanceled"), + selected_identity_provider_config_url: null, + token: null + }); + this.pendingPromiseResolve_ = null; + } + + async logoutRps(logout_endpoints) { + return Promise.resolve({ + status: this.logoutRpsStatus_ + }); + } + + async setIdpSigninStatus(origin, status) { + } + + async reset() { + this.token_ = null; + this.selected_identity_provider_config_url_ = null; + this.status_ = RequestTokenStatus.kError; + this.logoutRpsStatus_ = LogoutRpsStatus.kError; + this.receiver_.$.close(); + this.interceptor_.stop(); + + // Clean up and reset mock stubs asynchronously, so that the blink side + // closes its proxies and notifies JS sensor objects before new test is + // started. + await new Promise(resolve => { step_timeout(resolve, 0); }); + } +} diff --git a/testing/web-platform/tests/credential-management/support/fedcm-mojojs-helper.js b/testing/web-platform/tests/credential-management/support/fedcm-mojojs-helper.js new file mode 100644 index 0000000000..40ab729b1f --- /dev/null +++ b/testing/web-platform/tests/credential-management/support/fedcm-mojojs-helper.js @@ -0,0 +1,20 @@ +// The testing infra for FedCM is loaded using mojo js shim. To enable these +// tests the browser must be run with these options: +// +// --enable-blink-features=MojoJS,MojoJSTest + +import { MockFederatedAuthRequest } from './fedcm-mock.js'; + +export function fedcm_mojo_mock_test(test_func, name, exception, properties) { + promise_test(async (t) => { + assert_implements(navigator.credentials, 'missing navigator.credentials'); + const mock = new MockFederatedAuthRequest(); + try { + await test_func(t, mock); + } catch (e) { + assert_equals(exception, e.message) + } finally { + await mock.reset(); + } + }, name, properties); +} diff --git a/testing/web-platform/tests/credential-management/support/fedcm/accounts.py b/testing/web-platform/tests/credential-management/support/fedcm/accounts.py new file mode 100644 index 0000000000..088ce5967c --- /dev/null +++ b/testing/web-platform/tests/credential-management/support/fedcm/accounts.py @@ -0,0 +1,24 @@ +def main(request, response): + if request.cookies.get(b"cookie") != b"1": + return (530, [], "Missing cookie") + if request.headers.get(b"Accept") != b"application/json": + return (531, [], "Wrong Accept") + if request.headers.get(b"Sec-Fetch-Dest") != b"webidentity": + return (532, [], "Wrong Sec-Fetch-Dest header") + if request.headers.get(b"Referer"): + return (533, [], "Should not have Referer") + if request.headers.get(b"Origin"): + return (534, [], "Should not have Origin") + + return """ +{ + "accounts": [{ + "id": "1234", + "given_name": "John", + "name": "John Doe", + "email": "john_doe@idp.example", + "picture": "https://idp.example/profile/123", + "approved_clients": ["123", "456", "789"] + }] +} +""" diff --git a/testing/web-platform/tests/credential-management/support/fedcm/client_metadata.py b/testing/web-platform/tests/credential-management/support/fedcm/client_metadata.py new file mode 100644 index 0000000000..e6f6a77632 --- /dev/null +++ b/testing/web-platform/tests/credential-management/support/fedcm/client_metadata.py @@ -0,0 +1,30 @@ +# 'import credential-management.support.fedcm.keys' does not work. +import importlib +keys = importlib.import_module("credential-management.support.fedcm.keys") + +def main(request, response): + if (request.GET.get(b'skip_checks', b'0') != b'1'): + if len(request.cookies) > 0: + return (530, [], "Cookie should not be sent to this endpoint") + if request.headers.get(b"Accept") != b"application/json": + return (531, [], "Wrong Accept") + if request.headers.get(b"Sec-Fetch-Dest") != b"webidentity": + return (532, [], "Wrong Sec-Fetch-Dest header") + if request.headers.get(b"Referer"): + return (533, [], "Should not have Referer") + if not request.headers.get(b"Origin"): + return (534, [], "Missing Origin") + + counter = request.server.stash.take(keys.CLIENT_METADATA_COUNTER_KEY) + try: + counter = int(counter) + 1 + except (TypeError, ValueError): + counter = 1 + + request.server.stash.put(keys.CLIENT_METADATA_COUNTER_KEY, str(counter).encode()) + + return """ +{{ + "privacy_policy_url": "https://privacypolicy{0}.com" +}} +""".format(str(counter)) diff --git a/testing/web-platform/tests/credential-management/support/fedcm/client_metadata.py.headers b/testing/web-platform/tests/credential-management/support/fedcm/client_metadata.py.headers new file mode 100644 index 0000000000..7164e5f818 --- /dev/null +++ b/testing/web-platform/tests/credential-management/support/fedcm/client_metadata.py.headers @@ -0,0 +1 @@ +Cache-Control: public, max-age=86400 diff --git a/testing/web-platform/tests/credential-management/support/fedcm/client_metadata_clear_count.py b/testing/web-platform/tests/credential-management/support/fedcm/client_metadata_clear_count.py new file mode 100644 index 0000000000..3c31bf5077 --- /dev/null +++ b/testing/web-platform/tests/credential-management/support/fedcm/client_metadata_clear_count.py @@ -0,0 +1,15 @@ +# 'import credential-management.support.fedcm.keys' does not work. +import importlib +keys = importlib.import_module("credential-management.support.fedcm.keys") + +def main(request, response): + client_metadata_url = "/credential-management/support/fedcm/client_metadata.py" + counter = request.server.stash.take(keys.CLIENT_METADATA_COUNTER_KEY, + client_metadata_url) + + try: + counter = counter.decode() + except (UnicodeDecodeError, AttributeError): + pass + + return counter diff --git a/testing/web-platform/tests/credential-management/support/fedcm/intercept_service_worker.js b/testing/web-platform/tests/credential-management/support/fedcm/intercept_service_worker.js new file mode 100644 index 0000000000..773e38fd21 --- /dev/null +++ b/testing/web-platform/tests/credential-management/support/fedcm/intercept_service_worker.js @@ -0,0 +1,10 @@ +var num_overridden = 0; + +self.addEventListener('fetch', event => { + const url = event.request.url; + if (url.indexOf('query_service_worker_intercepts.html') != -1) { + event.respondWith(new Response(num_overridden)); + } else if (url.indexOf('credential-management/support') != -1) { + ++num_overridden; + } +}); diff --git a/testing/web-platform/tests/credential-management/support/fedcm/keys.py b/testing/web-platform/tests/credential-management/support/fedcm/keys.py new file mode 100644 index 0000000000..6b7d67e21e --- /dev/null +++ b/testing/web-platform/tests/credential-management/support/fedcm/keys.py @@ -0,0 +1,2 @@ +CLIENT_METADATA_COUNTER_KEY = b"bdc14e3e-b8bc-44a1-8eec-78da5fdacbc3" +MANIFEST_URL_IN_MANIFEST_LIST_KEY = b"7f3f7478-b7f0-41c5-b357-f3ac16f5f25a" diff --git a/testing/web-platform/tests/credential-management/support/fedcm/manifest-not-in-list.json b/testing/web-platform/tests/credential-management/support/fedcm/manifest-not-in-list.json new file mode 100644 index 0000000000..c66903cfd2 --- /dev/null +++ b/testing/web-platform/tests/credential-management/support/fedcm/manifest-not-in-list.json @@ -0,0 +1,5 @@ +{ + "accounts_endpoint": "accounts.py", + "client_metadata_endpoint": "client_metadata.py", + "id_assertion_endpoint": "token.py" +} diff --git a/testing/web-platform/tests/credential-management/support/fedcm/manifest.py b/testing/web-platform/tests/credential-management/support/fedcm/manifest.py new file mode 100644 index 0000000000..3e72faf75a --- /dev/null +++ b/testing/web-platform/tests/credential-management/support/fedcm/manifest.py @@ -0,0 +1,20 @@ +def main(request, response): + if len(request.cookies) > 0: + return (530, [], "Cookie should not be sent to manifest endpoint") + if request.headers.get(b"Accept") != b"application/json": + return (531, [], "Wrong Accept") + if request.headers.get(b"Sec-Fetch-Dest") != b"webidentity": + return (532, [], "Wrong Sec-Fetch-Dest header") + if request.headers.get(b"Referer"): + return (533, [], "Should not have Referer") + if request.headers.get(b"Origin"): + return (534, [], "Should not have Origin") + + return """ +{ + "accounts_endpoint": "accounts.py", + "client_metadata_endpoint": "client_metadata.py", + "id_assertion_endpoint": "token.py", + "revocation_endpoint": "revoke.py" +} +""" diff --git a/testing/web-platform/tests/credential-management/support/fedcm/manifest_redirect_accounts.json b/testing/web-platform/tests/credential-management/support/fedcm/manifest_redirect_accounts.json new file mode 100644 index 0000000000..590704cfeb --- /dev/null +++ b/testing/web-platform/tests/credential-management/support/fedcm/manifest_redirect_accounts.json @@ -0,0 +1,5 @@ +{ + "accounts_endpoint": "/common/redirect.py?location=/credential-management/support/fedcm/accounts.py", + "client_metadata_endpoint": "client_metadata.py", + "id_assertion_endpoint": "token.py" +} diff --git a/testing/web-platform/tests/credential-management/support/fedcm/manifest_redirect_token.json b/testing/web-platform/tests/credential-management/support/fedcm/manifest_redirect_token.json new file mode 100644 index 0000000000..190420736d --- /dev/null +++ b/testing/web-platform/tests/credential-management/support/fedcm/manifest_redirect_token.json @@ -0,0 +1,6 @@ +{ + "accounts_endpoint": "accounts.py", + "client_metadata_endpoint": "client_metadata.py", + "id_assertion_endpoint": "/common/redirect.py?location=/credential-management/support/fedcm/token.py&status=308", + "revocation_endpoint": "revoke.py" +} diff --git a/testing/web-platform/tests/credential-management/support/fedcm/select_manifest_in_root_manifest.py b/testing/web-platform/tests/credential-management/support/fedcm/select_manifest_in_root_manifest.py new file mode 100644 index 0000000000..d4f1efff6a --- /dev/null +++ b/testing/web-platform/tests/credential-management/support/fedcm/select_manifest_in_root_manifest.py @@ -0,0 +1,17 @@ +import importlib +from urllib.parse import urlsplit + +# 'import credential-management.support.fedcm.keys' does not work. +keys = importlib.import_module("credential-management.support.fedcm.keys") + +def main(request, response): + root_manifest_url = "/.well-known/web-identity" + + # Clear stash so that a new value can be written. + request.server.stash.take(keys.MANIFEST_URL_IN_MANIFEST_LIST_KEY, root_manifest_url) + + request.server.stash.put(keys.MANIFEST_URL_IN_MANIFEST_LIST_KEY, + request.GET.first(b"manifest_url", b""), + root_manifest_url) + + return root_manifest_url diff --git a/testing/web-platform/tests/credential-management/support/fedcm/simple.html b/testing/web-platform/tests/credential-management/support/fedcm/simple.html new file mode 100644 index 0000000000..d62419ce8a --- /dev/null +++ b/testing/web-platform/tests/credential-management/support/fedcm/simple.html @@ -0,0 +1,3 @@ +<!DOCTYPE html> +<html><body> +Simple diff --git a/testing/web-platform/tests/credential-management/support/fedcm/token.py b/testing/web-platform/tests/credential-management/support/fedcm/token.py new file mode 100644 index 0000000000..867dab9592 --- /dev/null +++ b/testing/web-platform/tests/credential-management/support/fedcm/token.py @@ -0,0 +1,24 @@ +def main(request, response): + if request.cookies.get(b"cookie") != b"1": + return (530, [], "Missing cookie") + if request.method != "POST": + return (531, [], "Method is not POST") + if request.headers.get(b"Content-Type") != b"application/x-www-form-urlencoded": + return (532, [], "Wrong Content-Type") + if request.headers.get(b"Accept") != b"application/json": + return (533, [], "Wrong Accept") + if request.headers.get(b"Sec-Fetch-Dest") != b"webidentity": + return (500, [], "Wrong Sec-Fetch-Dest header") + if request.headers.get(b"Referer"): + return (534, [], "Should not have Referer") + if not request.headers.get(b"Origin"): + return (535, [], "Missing Origin") + + if not request.POST.get(b"client_id"): + return (536, [], "Missing 'client_id' POST parameter") + if not request.POST.get(b"account_id"): + return (537, [], "Missing 'account_id' POST parameter") + if not request.POST.get(b"disclosure_text_shown"): + return (538, [], "Missing 'disclosure_text_shown' POST parameter") + + return "{\"token\": \"token\"}" diff --git a/testing/web-platform/tests/credential-management/support/federatedcredential-get.html b/testing/web-platform/tests/credential-management/support/federatedcredential-get.html new file mode 100644 index 0000000000..476f32688f --- /dev/null +++ b/testing/web-platform/tests/credential-management/support/federatedcredential-get.html @@ -0,0 +1,17 @@ +<script> + navigator.credentials.get({ 'federated': { 'providers': ['https://example.com' ] } }) + .then(c => { + window.parent.postMessage({ + "status": "resolved", + "credential": c, + "exception": null + }, "*"); + }) + .catch(omg => { + window.parent.postMessage({ + "status": "rejected", + "credential": null, + "exception": omg.name + }, "*"); + }); +</script> diff --git a/testing/web-platform/tests/credential-management/support/otpcredential-helper.js b/testing/web-platform/tests/credential-management/support/otpcredential-helper.js new file mode 100644 index 0000000000..e07e9f5be3 --- /dev/null +++ b/testing/web-platform/tests/credential-management/support/otpcredential-helper.js @@ -0,0 +1,114 @@ +// These tests rely on the User Agent providing an implementation of +// MockWebOTPService. +// +// In Chromium-based browsers this implementation is provided by a polyfill +// in order to reduce the amount of test-only code shipped to users. To enable +// these tests the browser must be run with these options: +// // --enable-blink-features=MojoJS,MojoJSTest + +import {isChromiumBased} from '/resources/test-only-api.m.js'; + +/** + * This enumeration is used by WebOTP WPTs to control mock backend behavior. + * See MockWebOTPService below. + */ +export const Status = { + SUCCESS: 0, + UNHANDLED_REQUEST: 1, + CANCELLED: 2, + ABORTED: 3, +}; + +/** + * A interface which must be implemented by browsers to support WebOTP WPTs. + */ +export class MockWebOTPService { + /** + * Accepts a function to be invoked in response to the next OTP request + * received by the mock. The (optionally async) function, when executed, must + * return an object with a `status` field holding one of the `Status` values + * defined above, and -- if successful -- an `otp` field containing a + * simulated OTP string. + * + * Tests will call this method directly to inject specific response behavior + * into the browser-specific mock implementation. + */ + async handleNextOTPRequest(responseFunc) {} +} + +/** + * Returns a Promise resolving to a browser-specific MockWebOTPService subclass + * instance if one is available. + */ +async function createBrowserSpecificMockImpl() { + if (isChromiumBased) { + return await createChromiumMockImpl(); + } + throw new Error('Unsupported browser.'); +} + +const asyncMock = createBrowserSpecificMockImpl(); + +export function expectOTPRequest() { + return { + async andReturn(callback) { + const mock = await asyncMock; + mock.handleNextOTPRequest(callback); + } + } +} + +/** + * Instantiates a Chromium-specific subclass of MockWebOTPService. + */ +async function createChromiumMockImpl() { + const {SmsStatus, WebOTPService, WebOTPServiceReceiver} = await import( + '/gen/third_party/blink/public/mojom/sms/webotp_service.mojom.m.js'); + const MockWebOTPServiceChromium = class extends MockWebOTPService { + constructor() { + super(); + this.mojoReceiver_ = new WebOTPServiceReceiver(this); + this.interceptor_ = + new MojoInterfaceInterceptor(WebOTPService.$interfaceName); + this.interceptor_.oninterfacerequest = (e) => { + this.mojoReceiver_.$.bindHandle(e.handle); + }; + this.interceptor_.start(); + this.requestHandlers_ = []; + Object.freeze(this); + } + + handleNextOTPRequest(responseFunc) { + this.requestHandlers_.push(responseFunc); + } + + async receive() { + if (this.requestHandlers_.length == 0) { + throw new Error('Mock received unexpected OTP request.'); + } + + const responseFunc = this.requestHandlers_.shift(); + const response = await responseFunc(); + switch (response.status) { + case Status.SUCCESS: + if (typeof response.otp != 'string') { + throw new Error('Mock success results require an OTP string.'); + } + return {status: SmsStatus.kSuccess, otp: response.otp}; + case Status.UNHANDLED_REQUEST: + return {status: SmsStatus.kUnhandledRequest}; + case Status.CANCELLED: + return {status: SmsStatus.kCancelled}; + case Status.ABORTED: + return {status: SmsStatus.kAborted}; + default: + throw new Error( + `Mock result contains unknown status: ${response.status}`); + } + } + + async abort() {} + }; + return new MockWebOTPServiceChromium(); +} + diff --git a/testing/web-platform/tests/credential-management/support/otpcredential-iframe.html b/testing/web-platform/tests/credential-management/support/otpcredential-iframe.html new file mode 100644 index 0000000000..4affc00ecd --- /dev/null +++ b/testing/web-platform/tests/credential-management/support/otpcredential-iframe.html @@ -0,0 +1,26 @@ +<!doctype html> +<script type="module"> +import {Status, expectOTPRequest} from './otpcredential-helper.js'; + +// Loading otpcredential-iframe.html in the test will make an OTPCredentials +// call on load, and trigger a postMessage upon completion. +// +// message { +// string result: "Pass" | "Fail" +// string code: credentials.code +// string errorType: error.name +// } + +window.onload = async () => { + try { + await expectOTPRequest().andReturn( + () => ({status: Status.SUCCESS, otp: "ABC123"})); + const credentials = + await navigator.credentials.get({otp: {transport: ["sms"]}}); + window.parent.postMessage({result: "Pass", code: credentials.code}, '*'); + } catch (error) { + window.parent.postMessage({result: "Fail", errorType: error.name}, '*'); + } +} + +</script> diff --git a/testing/web-platform/tests/credential-management/support/passwordcredential-get.html b/testing/web-platform/tests/credential-management/support/passwordcredential-get.html new file mode 100644 index 0000000000..0ec584d73d --- /dev/null +++ b/testing/web-platform/tests/credential-management/support/passwordcredential-get.html @@ -0,0 +1,17 @@ +<script> + navigator.credentials.get({ 'password': true }) + .then(c => { + window.parent.postMessage({ + "status": "resolved", + "credential": c, + "exception": null + }, "*"); + }) + .catch(omg => { + window.parent.postMessage({ + "status": "rejected", + "credential": null, + "exception": omg.name + }, "*"); + }); +</script> diff --git a/testing/web-platform/tests/credential-management/support/set_cookie b/testing/web-platform/tests/credential-management/support/set_cookie new file mode 100644 index 0000000000..1080b366e4 --- /dev/null +++ b/testing/web-platform/tests/credential-management/support/set_cookie @@ -0,0 +1,12 @@ +<html> +<body> +<script> +// The important part of this page are the headers. + +// If this page was opened as a popup, notify the opener. +if (window.opener) { + window.opener.postMessage("done_loading", "*"); +} +</script> +</body> +</html> diff --git a/testing/web-platform/tests/credential-management/support/set_cookie.headers b/testing/web-platform/tests/credential-management/support/set_cookie.headers new file mode 100644 index 0000000000..cf5ea7fff9 --- /dev/null +++ b/testing/web-platform/tests/credential-management/support/set_cookie.headers @@ -0,0 +1,2 @@ +Content-Type: text/html +Set-Cookie: cookie=1; SameSite=Strict; Secure |