Skip to content

Commit

Permalink
Implement related origins e2e tests (#2846)
Browse files Browse the repository at this point in the history
* Implement related origins e2e tests

* Implement related origins e2e tests

* Implement related origins e2e tests

* Implement related origins e2e tests

* Strictly check expected related origins.

* Set related origins in e2e test canister deployment.

* Escape quotes.

* Clarify comment
  • Loading branch information
sea-snake authored Feb 11, 2025
1 parent 61c3de3 commit fe2dc02
Show file tree
Hide file tree
Showing 9 changed files with 288 additions and 8 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/canister-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,7 @@ jobs:
# (i.e. capitalize the first letter)
captcha_variant="Captcha$(tr '[:lower:]' '[:upper:]' <<< ${captcha_flag:0:1})${captcha_flag:1}"
# NOTE: dfx install will run the postinstall scripts from dfx.json
dfx canister install internet_identity --wasm internet_identity_test.wasm.gz --argument "(opt record { captcha_config = opt record { max_unsolved_captchas= 50:nat64; captcha_trigger = variant {Static = variant { $captcha_variant }}}})"
dfx canister install internet_identity --wasm internet_identity_test.wasm.gz --argument "(opt record { captcha_config = opt record { max_unsolved_captchas= 50:nat64; captcha_trigger = variant {Static = variant { $captcha_variant }}}; related_origins = opt vec { \"https://identity.internetcomputer.org\"; \"https://identity.ic0.app\" }})"
dfx canister install test_app --wasm demos/test-app/test_app.wasm
dfx canister install issuer --wasm demos/vc_issuer/vc_demo_issuer.wasm.gz
Expand Down
2 changes: 1 addition & 1 deletion HACKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ We have a set of Selenium tests that run through the various flows. To set up a

1. Start a local replica with `dfx start`
1. Deploy II and the other test canisters with `dfx deploy --no-wallet`
1. Start the vite dev server with TLS enabled: `TLS_DEV_SERVER=1 npm run dev`
1. Start the vite dev server with TLS enabled and hot reloading disabled: `TLS_DEV_SERVER=1 NO_HOT_RELOAD=1 npm run dev`

To watch the tests run in the browser remove the `headless` option from `src/frontend/src/test-e2e/util.ts`.

Expand Down
2 changes: 1 addition & 1 deletion dfx.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"candid": "src/internet_identity/internet_identity.did",
"wasm": "internet_identity.wasm.gz",
"build": "bash -c 'II_DEV_CSP=1 II_FETCH_ROOT_KEY=1 II_DUMMY_CAPTCHA=${II_DUMMY_CAPTCHA:-1} scripts/build'",
"init_arg": "(opt record { captcha_config = opt record { max_unsolved_captchas= 50:nat64; captcha_trigger = variant {Static = variant {CaptchaDisabled}}}; openid_google = opt opt record { client_id = \"45431994619-cbbfgtn7o0pp0dpfcg2l66bc4rcg7qbu.apps.googleusercontent.com\" }})",
"init_arg": "(opt record { captcha_config = opt record { max_unsolved_captchas= 50:nat64; captcha_trigger = variant {Static = variant {CaptchaDisabled}}}; openid_google = opt opt record { client_id = \"45431994619-cbbfgtn7o0pp0dpfcg2l66bc4rcg7qbu.apps.googleusercontent.com\" }; related_origins = opt vec { \"https://identity.internetcomputer.org\"; \"https://identity.ic0.app\" } })",
"shrink": false
},
"test_app": {
Expand Down
2 changes: 1 addition & 1 deletion src/frontend/src/flows/manage/authenticatorsSection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ const itemWarning = ({
const itemInfo = (msg: TemplateResult): TemplateResult => html`<div
class="c-action-list__action"
>
<span class="c-tooltip c-icon" tabindex="0"
<span class="c-tooltip c-icon" tabindex="0" data-icon="info"
>${infoIcon}<span class="c-tooltip__message c-card c-card--tight"
>${msg}</span
></span
Expand Down
11 changes: 11 additions & 0 deletions src/frontend/src/test-e2e/flows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,17 @@ export const FLOWS = {
const mainView = new MainView(browser);
await mainView.waitForDeviceDisplay(deviceName);
},
loginExistingAuthenticateView: async (
userNumber: string,
deviceName: string,
browser: WebdriverIO.Browser
): Promise<void> => {
const authenticateView = new AuthenticateView(browser);
await authenticateView.waitForDisplay();
await authenticateView.continueWithAnchor(userNumber);
const mainView = new MainView(browser);
await mainView.waitForDeviceDisplay(deviceName);
},
loginPinAuthenticateView: async (
userNumber: string,
pin: string,
Expand Down
182 changes: 182 additions & 0 deletions src/frontend/src/test-e2e/relatedOrigins.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import {
addVirtualAuthenticator,
createActor,
mimickPasskeyExtension,
removeVirtualAuthenticator,
runInBrowser,
setDomainCompatibilityFeatureFlag,
} from "./util";

// Read canister ids from the corresponding dfx files.
// This assumes that they have been successfully dfx-deployed
import { FLOWS } from "$src/test-e2e/flows";
import { MainView } from "$src/test-e2e/views";
import { DEVICE_NAME1, II_URL } from "./constants";

const EXPECTED_RELATED_ORIGINS = [
"https://identity.internetcomputer.org",
"https://identity.ic0.app",
];

test("Sign in on related origins", async () => {
await runInBrowser(async (browser: WebdriverIO.Browser) => {
// Get related origins
await browser.url(II_URL);
const actor = await createActor(browser);
const config = await actor.config();
const relatedOrigins = config.related_origins[0] ?? [II_URL];

// Related origins should be configured to the expected values
expect(relatedOrigins).toStrictEqual(EXPECTED_RELATED_ORIGINS);

// Register on main (current) origin
await addVirtualAuthenticator(browser);
const userNumber = await FLOWS.registerNewIdentityWelcomeView(browser);
const mainView = new MainView(browser);
await mainView.waitForDisplay();

// Sign in on each related origin
for (const relatedOrigin of relatedOrigins) {
// Sign out and navigate to different origin
await mainView.logout();
await browser.url(relatedOrigin);

// Enable feature flag and sign in
await setDomainCompatibilityFeatureFlag(browser, true);
await FLOWS.loginExistingAuthenticateView(
userNumber,
DEVICE_NAME1,
browser
);
}
});
}, 300_000);

test("Add devices on related origins with same origin", async () => {
await runInBrowser(async (browser: WebdriverIO.Browser) => {
// Get related origins
await browser.url(II_URL);
const actor = await createActor(browser);
const config = await actor.config();
const relatedOrigins = config.related_origins[0] ?? [II_URL];

// Related origins should be configured to the expected values
expect(relatedOrigins).toStrictEqual(EXPECTED_RELATED_ORIGINS);

// Register on main (current) origin
const signInDevice = await addVirtualAuthenticator(browser);
await FLOWS.registerNewIdentityWelcomeView(browser);
const mainView = new MainView(browser);
await mainView.waitForDisplay();
await removeVirtualAuthenticator(browser, signInDevice);

// Since we're going to need to sign in to add a new device on each origin,
// let's use the recovery as workaround to the single device limitation.
const seedPhrase = await FLOWS.addRecoveryMechanismSeedPhrase(browser);

// Register device on each related origin
for (const relatedOrigin of relatedOrigins) {
// Sign out and navigate to different origin
await mainView.logout();
const additionalDevice = await addVirtualAuthenticator(browser);
await browser.url(relatedOrigin);

// Enable feature flag
await setDomainCompatibilityFeatureFlag(browser, true);

// Sign in using seed phrase and register device
await FLOWS.recoverUsingSeedPhrase(browser, seedPhrase);
await FLOWS.addFidoDevice(browser);
await mainView.waitForDisplay();
await removeVirtualAuthenticator(browser, additionalDevice);
}

// Check if all devices are registered with same origin
await mainView.waitForDeviceCount(DEVICE_NAME1, relatedOrigins.length + 1);
await mainView.waitForDifferentOriginDevice(false);
});
}, 300_000);

test("Add devices on related origins with different origin", async () => {
await runInBrowser(async (browser: WebdriverIO.Browser) => {
// Get related origins
await browser.url(II_URL);
const actor = await createActor(browser);
const config = await actor.config();
const relatedOrigins = config.related_origins[0] ?? [II_URL];

// Related origins should be configured to the expected values
expect(relatedOrigins).toStrictEqual(EXPECTED_RELATED_ORIGINS);

// Register on main (current) origin
const signInDevice = await addVirtualAuthenticator(browser);
await FLOWS.registerNewIdentityWelcomeView(browser);
const mainView = new MainView(browser);
await mainView.waitForDisplay();
await removeVirtualAuthenticator(browser, signInDevice);

// Since we're going to need to sign in to add a new device on each origin,
// let's use the recovery as workaround to the single device limitation.
const seedPhrase = await FLOWS.addRecoveryMechanismSeedPhrase(browser);

// Register device on each related origin
for (const relatedOrigin of relatedOrigins) {
// Sign out and navigate to different origin
await mainView.logout();
const additionalDevice = await addVirtualAuthenticator(browser);
await browser.url(relatedOrigin);

// Enable feature flag
await setDomainCompatibilityFeatureFlag(browser, true);

// Disable RoR by mimicking a passkey browser extension,
// this enables adding devices in different origins.
await mimickPasskeyExtension(browser);

// Sign in using seed phrase and register device
await FLOWS.recoverUsingSeedPhrase(browser, seedPhrase);
await FLOWS.addFidoDevice(browser);
await mainView.waitForDisplay();
await removeVirtualAuthenticator(browser, additionalDevice);
}

// Check if any devices are registered with different origins
await mainView.waitForDeviceCount(DEVICE_NAME1, relatedOrigins.length + 1);
await mainView.waitForDifferentOriginDevice(true);
});
}, 300_000);

test("Use recovery device on related origins", async () => {
await runInBrowser(async (browser: WebdriverIO.Browser) => {
// Get related origins
await browser.url(II_URL);
const actor = await createActor(browser);
const config = await actor.config();
const relatedOrigins = config.related_origins[0] ?? [II_URL];

// Related origins should be configured to the expected values
expect(relatedOrigins).toStrictEqual(EXPECTED_RELATED_ORIGINS);

// Register on main (current) origin
const signInDevice = await addVirtualAuthenticator(browser);
const userNumber = await FLOWS.registerNewIdentityWelcomeView(browser);
const mainView = new MainView(browser);
await mainView.waitForDisplay();
await removeVirtualAuthenticator(browser, signInDevice);

// Create recovery device
await addVirtualAuthenticator(browser);
await FLOWS.addRecoveryMechanismDevice(browser);

// Recover on each related origin
for (const relatedOrigin of relatedOrigins) {
// Sign out and navigate to different origin
await mainView.logout();
await browser.url(relatedOrigin);

// Enable feature flag and recover using device
await setDomainCompatibilityFeatureFlag(browser, true);
await FLOWS.recoverUsingDevice(browser, userNumber);
}
});
}, 300_000);
52 changes: 50 additions & 2 deletions src/frontend/src/test-e2e/util.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { idlFactory as internet_identity_idl } from "$generated/internet_identity_idl";
import { _SERVICE } from "$generated/internet_identity_types";
import { randomString, wrapError } from "$src/utils/utils";
import { Actor, ActorSubclass, HttpAgent } from "@dfinity/agent";
import { nonNullish } from "@dfinity/utils";
import { ChromeOptions } from "@wdio/types/build/Capabilities";
import * as fs from "fs";
Expand Down Expand Up @@ -95,7 +98,7 @@ export async function runInBrowser(
const browser = await remoteRetry({
capabilities: {
browserName: "chrome",
browserVersion: "122.0.6261.111", // More information about available versions can be found here: https://github.com/GoogleChromeLabs/chrome-for-testing
browserVersion: "133.0.6943.53", // More information about available versions can be found here: https://github.com/GoogleChromeLabs/chrome-for-testing
"goog:chromeOptions": chromeOptions,
},
});
Expand Down Expand Up @@ -466,7 +469,7 @@ export async function wipeStorage(browser: WebdriverIO.Browser): Promise<void> {

export const setOpenIdFeatureFlag = async (
browser: WebdriverIO.Browser,
enabled: true
enabled: boolean
): Promise<void> => {
await browser.execute((enabled) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
Expand Down Expand Up @@ -499,3 +502,48 @@ export const mockFedCM = async (
};
}, token);
};

export const setDomainCompatibilityFeatureFlag = async (
browser: WebdriverIO.Browser,
enabled: boolean
): Promise<void> => {
await browser.execute((enabled) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
window.__featureFlags.DOMAIN_COMPATIBILITY.set(enabled);
}, enabled);
};

/**
* Monkey patch WebAuthn methods to make them not '[native code]' and
* thus detected as a passkey extension which doesn't support RoR.
*/
export const mimickPasskeyExtension = async (
browser: WebdriverIO.Browser
): Promise<void> => {
await browser.execute(() => {
const create = navigator.credentials.create;
const get = navigator.credentials.get;
navigator.credentials.create = (...args) =>
create.call(navigator.credentials, ...args);
navigator.credentials.get = (...args) =>
get.call(navigator.credentials, ...args);
});
};

export const createActor = async (
browser: WebdriverIO.Browser
): Promise<ActorSubclass<_SERVICE>> => {
const script = await browser.$("[data-canister-id]");
const canisterId = await script.getAttribute("data-canister-id");
const agent = await HttpAgent.create({
// Always go through vite dev server and fetch the root key
host: "https://localhost:5173",
shouldFetchRootKey: true,
verifyQuerySignatures: false,
});
return Actor.createActor<_SERVICE>(internet_identity_idl, {
agent,
canisterId,
});
};
Loading

0 comments on commit fe2dc02

Please sign in to comment.