Skip to content

Commit 15e7fce

Browse files
author
Llorenç Muntaner
authored
Add more E2E to test different scenarios (dfinity#3212)
* Add E2E for same principal with alternative origins * Add delegation expiration E2E tests * Add E2E for post message edge cases * Fix lint * Change slightly what we wait for alert of no message * CR changes
1 parent 8f62a7d commit 15e7fce

4 files changed

Lines changed: 329 additions & 0 deletions

File tree

src/frontend/tests/e2e-playwright/authorize/alternativeOrigins.spec.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,3 +203,81 @@ test("Should not follow redirect returned by /.well-known/ii-alternative-origins
203203
// Verify error message is displayed in II (redirect should not be followed)
204204
await expect(authPage.getByText("Unverified origin")).toBeVisible();
205205
});
206+
207+
test("Should issue the same principal to nice url and canonical url", async ({
208+
page,
209+
}) => {
210+
// Create a new identity using dummy auth
211+
const auth = dummyAuth();
212+
213+
// First authentication: Test app configured with canonical URL as derivation origin
214+
await page.goto(TEST_APP_URL);
215+
await page.getByRole("textbox", { name: "Identity Provider" }).fill(II_URL);
216+
await page.locator("#hostUrl").fill("https://icp-api.io");
217+
218+
// Configure alternative origins to include the nice URL
219+
const alternativeOrigins = JSON.stringify({
220+
alternativeOrigins: [TEST_APP_URL],
221+
});
222+
await page.locator("#newAlternativeOrigins").fill(alternativeOrigins);
223+
await page.locator("#certified").click();
224+
await page.locator("#updateNewAlternativeOrigins").click();
225+
226+
// Wait for alternative origins to update
227+
await expect(page.locator("#alternativeOrigins")).toHaveText(
228+
alternativeOrigins,
229+
{ timeout: 6000 },
230+
);
231+
232+
// Set derivation origin to canonical URL
233+
await page.locator("#derivationOrigin").fill(TEST_APP_CANONICAL_URL);
234+
235+
// Authenticate and get the first principal
236+
const pagePromise1 = page.context().waitForEvent("page");
237+
await page.getByRole("button", { name: "Sign In" }).click();
238+
const authPage1 = await pagePromise1;
239+
240+
// Create identity in II
241+
await authPage1
242+
.getByRole("button", { name: "Continue with Passkey" })
243+
.click();
244+
await authPage1.getByRole("button", { name: "Set up a new Passkey" }).click();
245+
await authPage1.getByLabel("Identity name").fill("Test User");
246+
auth(authPage1);
247+
await authPage1.getByRole("button", { name: "Create Passkey" }).click();
248+
249+
// Wait for II window to close
250+
await authPage1.waitForEvent("close");
251+
252+
// Get the first principal
253+
await expect(page.locator("#principal")).not.toBeEmpty();
254+
const principal1 = await page.locator("#principal").textContent();
255+
expect(principal1).toBeTruthy();
256+
257+
// Second authentication: Simulate nice URL scenario with same derivation origin
258+
// Clear the current session by reloading the page
259+
await page.reload();
260+
await page.goto(TEST_APP_CANONICAL_URL);
261+
await page.getByRole("textbox", { name: "Identity Provider" }).fill(II_URL);
262+
await page.locator("#hostUrl").fill("https://icp-api.io");
263+
264+
// Authenticate with the existing identity
265+
const pagePromise2 = page.context().waitForEvent("page");
266+
await page.getByRole("button", { name: "Sign In" }).click();
267+
const authPage2 = await pagePromise2;
268+
269+
// Use existing passkey
270+
auth(authPage2);
271+
await authPage2.getByRole("button", { name: "Primary account" }).click();
272+
273+
// Wait for II window to close
274+
await authPage2.waitForEvent("close");
275+
276+
// Get the second principal
277+
await expect(page.locator("#principal")).not.toBeEmpty();
278+
const principal2 = await page.locator("#principal").textContent();
279+
expect(principal2).toBeTruthy();
280+
281+
// Verify both principals are the same
282+
expect(principal1).toEqual(principal2);
283+
});
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { expect, test } from "@playwright/test";
2+
import { dummyAuth, II_URL, TEST_APP_URL } from "../utils";
3+
4+
test("Delegation maxTimeToLive: 1 min", async ({ page }) => {
5+
const auth = dummyAuth();
6+
7+
// Open demo app and configure II URL
8+
await page.goto(TEST_APP_URL);
9+
await page.getByRole("textbox", { name: "Identity Provider" }).fill(II_URL);
10+
11+
// Set maxTimeToLive to 1 minute (60 seconds = 60_000_000_000 nanoseconds)
12+
await page.locator("#maxTimeToLive").fill("60000000000");
13+
14+
// Start authentication flow
15+
const pagePromise = page.context().waitForEvent("page");
16+
await page.getByRole("button", { name: "Sign In" }).click();
17+
const authPage = await pagePromise;
18+
19+
// Create new identity and authenticate
20+
await authPage.getByRole("button", { name: "Continue with Passkey" }).click();
21+
await authPage.getByRole("button", { name: "Set up a new Passkey" }).click();
22+
await authPage.getByLabel("Identity name").fill("Test User");
23+
auth(authPage);
24+
await authPage.getByRole("button", { name: "Create Passkey" }).click();
25+
26+
// Wait for authentication to complete and window to close
27+
await authPage.waitForEvent("close");
28+
29+
// Wait for principal to be populated (indicating successful authentication)
30+
await expect(page.locator("#principal")).not.toBeEmpty();
31+
32+
// Check expiration time - should be close to 1 minute (60_000_000_000 nanoseconds)
33+
const expiration = await page.locator("#expiration").textContent();
34+
const expirationNs = Number(expiration);
35+
// Compare only up to one decimal place for the 1min test
36+
expect(expirationNs / 60_000_000_000).toBeCloseTo(1, 0);
37+
});
38+
39+
test("Delegation maxTimeToLive: 1 day", async ({ page }) => {
40+
const auth = dummyAuth();
41+
42+
// Open demo app and configure II URL
43+
await page.goto(TEST_APP_URL);
44+
await page.getByRole("textbox", { name: "Identity Provider" }).fill(II_URL);
45+
46+
// Set maxTimeToLive to 1 day (86400 seconds = 86400_000_000_000 nanoseconds)
47+
await page.locator("#maxTimeToLive").fill("86400000000000");
48+
49+
// Start authentication flow
50+
const pagePromise = page.context().waitForEvent("page");
51+
await page.getByRole("button", { name: "Sign In" }).click();
52+
const authPage = await pagePromise;
53+
54+
// Create new identity and authenticate
55+
await authPage.getByRole("button", { name: "Continue with Passkey" }).click();
56+
await authPage.getByRole("button", { name: "Set up a new Passkey" }).click();
57+
await authPage.getByLabel("Identity name").fill("Test User");
58+
auth(authPage);
59+
await authPage.getByRole("button", { name: "Create Passkey" }).click();
60+
61+
// Wait for authentication to complete and window to close
62+
await authPage.waitForEvent("close");
63+
64+
// Wait for principal to be populated (indicating successful authentication)
65+
await expect(page.locator("#principal")).not.toBeEmpty();
66+
67+
// Check expiration time - should be close to 1 day (86400_000_000_000 nanoseconds)
68+
const expiration = await page.locator("#expiration").textContent();
69+
const expirationNs = Number(expiration);
70+
expect(expirationNs / 86400_000_000_000).toBeCloseTo(1);
71+
});
72+
73+
test("Delegation maxTimeToLive: 2 months", async ({ page }) => {
74+
const auth = dummyAuth();
75+
76+
// Open demo app and configure II URL
77+
await page.goto(TEST_APP_URL);
78+
await page.getByRole("textbox", { name: "Identity Provider" }).fill(II_URL);
79+
80+
// Set maxTimeToLive to 60 days (5_184_000_000_000_000 nanoseconds)
81+
await page.locator("#maxTimeToLive").fill("5184000000000000");
82+
83+
// Start authentication flow
84+
const pagePromise = page.context().waitForEvent("page");
85+
await page.getByRole("button", { name: "Sign In" }).click();
86+
const authPage = await pagePromise;
87+
88+
// Create new identity and authenticate
89+
await authPage.getByRole("button", { name: "Continue with Passkey" }).click();
90+
await authPage.getByRole("button", { name: "Set up a new Passkey" }).click();
91+
await authPage.getByLabel("Identity name").fill("Test User");
92+
auth(authPage);
93+
await authPage.getByRole("button", { name: "Create Passkey" }).click();
94+
95+
// Wait for authentication to complete and window to close
96+
await authPage.waitForEvent("close");
97+
98+
// Wait for principal to be populated (indicating successful authentication)
99+
await expect(page.locator("#principal")).not.toBeEmpty();
100+
101+
// Check expiration time - should be capped at 30 days (2_592_000_000_000_000 nanoseconds)
102+
const expiration = await page.locator("#expiration").textContent();
103+
const expirationNs = Number(expiration);
104+
// NB: Max out at 30 days
105+
expect(expirationNs / 2_592_000_000_000_000).toBeCloseTo(1);
106+
});
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { expect, test } from "@playwright/test";
2+
import {
3+
createNewIdentityInII,
4+
dummyAuth,
5+
getMessageText,
6+
II_URL,
7+
openIiTab,
8+
openTestAppWithII,
9+
waitForNthMessage,
10+
} from "../utils";
11+
12+
test("Authorize ready message should be sent immediately", async ({ page }) => {
13+
await openTestAppWithII(page);
14+
const iiPage = await openIiTab(page);
15+
await waitForNthMessage(page, 1);
16+
const messageText = await getMessageText(page, 1);
17+
expect(messageText).toContain("authorize-ready");
18+
await iiPage.close();
19+
});
20+
21+
test("Should allow valid message", async ({ page }) => {
22+
const auth = dummyAuth();
23+
24+
await openTestAppWithII(page);
25+
const iiPage = await openIiTab(page);
26+
27+
// Wait for authorize-ready message, then send valid message
28+
await waitForNthMessage(page, 1); // message 1: authorize-ready
29+
await page.locator("#validMessageBtn").click(); // message 2: authorize-client
30+
31+
// Complete authentication in II popup
32+
await createNewIdentityInII(iiPage, "Test User", auth);
33+
34+
// Wait for success notification in II
35+
await iiPage
36+
.getByRole("heading", { name: "Authentication successful" })
37+
.waitFor({ timeout: 15_000 });
38+
39+
// Setup the listener because clicking closes the page immediately
40+
// and we might not add the listener on time.
41+
const closePromise = iiPage.waitForEvent("close");
42+
await iiPage.getByRole("button", { name: "Return to app" }).click();
43+
await closePromise;
44+
45+
// Verify success message and expiration in test app
46+
await waitForNthMessage(page, 3); // message 3: authorize-success
47+
const successMessage = await getMessageText(page, 3);
48+
expect(successMessage).toContain("authorize-client-success");
49+
50+
// Wait for principal to be populated (indicating successful authentication)
51+
await expect(page.locator("#principal")).not.toBeEmpty();
52+
});
53+
54+
test("Should show error for invalid message", async ({ page }) => {
55+
await openTestAppWithII(page);
56+
const iiPage = await openIiTab(page);
57+
58+
// Wait for authorize-ready message, then send invalid data
59+
await waitForNthMessage(page, 1); // message 1: authorize-ready
60+
await page.locator("#invalidDataBtn").click(); // Send invalid post message
61+
62+
// Check for specific error message in II popup
63+
await expect(
64+
iiPage.getByRole("heading", { name: "Invalid request" }),
65+
).toBeVisible();
66+
67+
// Setup the listener because clicking closes the page immediately
68+
// and we might not add the listener on time.
69+
const closePromise = iiPage.waitForEvent("close");
70+
await iiPage.getByRole("button", { name: "Return to app" }).click();
71+
await closePromise;
72+
});
73+
74+
test("Should show error after not receiving message for 10 seconds", async ({
75+
page,
76+
}) => {
77+
await openTestAppWithII(page);
78+
const iiPage = await openIiTab(page);
79+
80+
await new Promise((resolve) => setTimeout(resolve, 10_000));
81+
82+
// Check for specific error message in II popup
83+
await expect(
84+
iiPage.getByRole("heading", { name: "Connection closed" }),
85+
).toBeVisible();
86+
87+
// Setup the listener because clicking closes the page immediately
88+
// and we might not add the listener on time.
89+
const closePromise = iiPage.waitForEvent("close");
90+
await iiPage.getByRole("button", { name: "Return to app" }).click();
91+
await closePromise;
92+
});
93+
94+
test("Should show error after manually navigating to authorize url", async ({
95+
page,
96+
}) => {
97+
await page.goto(II_URL + "#authorize");
98+
99+
// Check for specific error message in II popup
100+
await expect(
101+
page.getByRole("heading", { name: "Missing request" }),
102+
).toBeVisible();
103+
});

src/frontend/tests/e2e-playwright/utils.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export const authorizeWithUrl = async (
6969
await authPage.waitForEvent("close");
7070

7171
// Assert that the user is authenticated (valid principal)
72+
await expect(page.locator("#principal")).not.toBeEmpty();
7273
const principal = (await page.locator("#principal").textContent()) ?? "";
7374
expect(principal).toEqual(Principal.fromText(principal).toText());
7475

@@ -174,3 +175,44 @@ export const signOut = async (page: Page): Promise<void> => {
174175
await page.getByLabel("Switch identity").click();
175176
await page.getByRole("button", { name: "Sign Out" }).click();
176177
};
178+
179+
/**
180+
* Opens test app and configures II URL
181+
*/
182+
export const openTestAppWithII = async (page: Page): Promise<void> => {
183+
await page.goto(TEST_APP_URL);
184+
await page.getByRole("textbox", { name: "Identity Provider" }).fill(II_URL);
185+
};
186+
187+
/**
188+
* Opens II popup tab and returns the popup page
189+
*/
190+
export const openIiTab = async (page: Page): Promise<Page> => {
191+
const pagePromise = page.context().waitForEvent("page");
192+
await page.locator("#openIiWindowBtn").click();
193+
return await pagePromise;
194+
};
195+
196+
/**
197+
* Waits for nth message to appear in test app
198+
*/
199+
export const waitForNthMessage = async (
200+
page: Page,
201+
messageNo: number,
202+
): Promise<void> => {
203+
await page.locator(`div.postMessage:nth-child(${messageNo})`).waitFor();
204+
};
205+
206+
/**
207+
* Gets message text from nth message in test app
208+
*/
209+
export const getMessageText = async (
210+
page: Page,
211+
messageNo: number,
212+
): Promise<string> => {
213+
return (
214+
(await page
215+
.locator(`div.postMessage:nth-child(${messageNo}) > div:nth-child(2)`)
216+
.textContent()) ?? ""
217+
);
218+
};

0 commit comments

Comments
 (0)