Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
2ef4cb9
Add third flow for testing webauthn signup on accountlinking disabled
deepjyoti30-st Mar 17, 2025
5a8a6d5
Add init support for testing e2e test flow 2 with webauthn user chang…
deepjyoti30-st Mar 18, 2025
4031702
Add some changes for the test flows
deepjyoti30-st Mar 18, 2025
37eea51
Add fixes for the e2e tests
deepjyoti30-st Mar 20, 2025
a23dda2
Add fix for properly checking error for webauthn
deepjyoti30-st Mar 21, 2025
0f6a3f6
Undo index changes
deepjyoti30-st Mar 21, 2025
2414931
test: update test to match new testing method
porcellus May 29, 2025
f947a91
Remove console log
deepjyoti30-st Jun 2, 2025
0ca95fb
Remove run tests check workflow
deepjyoti30-st Jun 3, 2025
d63bf45
Update pre-commit hook workflow to make it pass
deepjyoti30-st Jun 3, 2025
aa490f6
Fix examples test workflow to use latest actions
deepjyoti30-st Jun 3, 2025
6630842
Get rid of git commit step in pre-commit hook workflow
deepjyoti30-st Jun 3, 2025
1aa77da
Update pre-commit hook to remove chown step
deepjyoti30-st Jun 3, 2025
557a131
Fix dep version for svelte-navigator crashing the npm i step
deepjyoti30-st Jun 4, 2025
350f74d
Skip install post install scripts for deps
deepjyoti30-st Jun 4, 2025
0c57b8a
Fix some example build failures leading to test failures
deepjyoti30-st Jun 4, 2025
c05d096
Fix another example build failure leading to failing test
deepjyoti30-st Jun 4, 2025
08e01fe
Add fix for accountlinking and mfa tests
deepjyoti30-st Jun 5, 2025
8ac94ab
Fix webauthn accountlinking tests
deepjyoti30-st Jun 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 3 additions & 10 deletions .github/workflows/pre-commit-hook-run.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,8 @@ jobs:
pr-title:
name: Pre commit hook check
runs-on: ubuntu-latest
container: rishabhpoddar/supertokens_website_sdk_testing_node_16
steps:
- uses: actions/checkout@v2
- run: git init && git add --all && git -c user.name='test' -c user.email='[email protected]' commit -m 'init for pr action'
- run: npm i --force || true
# the below command is there cause otherwise running npm run check-circular-dependencies gives an error like:
# Your cache folder contains root-owned files, due to a bug in
# npm ERR! previous versions of npm which has since been addressed.
- run: chown -R 1001:121 "/github/home/.npm"
- run: npm i --force
- run: cd test/with-typescript && npm i --force
- uses: actions/checkout@v4
- run: npm ci
- run: cd test/with-typescript && npm ci
- run: ./hooks/pre-commit.sh
6 changes: 3 additions & 3 deletions .github/workflows/test-examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ jobs:
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- run: bash test/findExamplesWithTests.sh
- id: set-matrix
run: echo "::set-output name=matrix::{\"include\":$(bash test/findExamplesWithTests.sh)}"
Expand All @@ -24,7 +24,7 @@ jobs:
run:
working-directory: ${{ matrix.examplePath }}
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- run: bash ../../test/updateExampleAppDeps.sh .
- run: npm install [email protected] [email protected] puppeteer@^11.0.0 isomorphic-fetch@^3.0.0
- run: npm run build || true
Expand All @@ -36,7 +36,7 @@ jobs:
)
- name: The job has failed
if: ${{ failure() }}
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: screenshots
path: ./**/*screenshot.jpeg
24 changes: 0 additions & 24 deletions .github/workflows/tests-pass-check-pr.yml

This file was deleted.

2 changes: 1 addition & 1 deletion test/end-to-end/accountlinking.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -831,7 +831,7 @@ async function tryEmailPasswordSignUp(page, email) {
await new Promise((res) => setTimeout(res, 250));
}

async function tryPasswordlessSignInUp(page, email) {
export async function tryPasswordlessSignInUp(page, email) {
await page.evaluate(() => localStorage.removeItem("supertokens-passwordless-loginAttemptInfo"));
await Promise.all([
page.goto(`${TEST_CLIENT_BASE_URL}/auth/?authRecipe=passwordless`),
Expand Down
273 changes: 273 additions & 0 deletions test/end-to-end/webauthn.accountlinking.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
/*
* Copyright (c) 2022, SuperTokens.com
* All rights reserved.
*/

import { TEST_CLIENT_BASE_URL } from "../constants";
import {
setupBrowser,
screenshotOnFailure,
clearBrowserCookiesWithoutAffectingConsole,
toggleSignInSignUp,
getTestEmail,
waitForSTElement,
submitForm,
setInputValues,
getPasswordlessDevice,
waitForUrl,
changeEmail,
getLatestURLWithToken,
getUserIdWithFetch,
submitFormUnsafe,
backendHook,
setupCoreApp,
setupST,
isWebauthnSupported,
} from "../helpers";
import {
openRecoveryAccountPage,
tryWebauthnSignUp,
getTokenFromEmail,
openRecoveryWithToken,
tryWebauthnSignIn,
} from "./webauthn.helpers";
import { tryPasswordlessSignInUp } from "./accountlinking.test";
import assert from "assert";

/*
* Test case:
* 1. The app has account linking disabled
* 2. A user signs up using a non-webauthn factor (e.g.: passwordless)
* 3. The user now tries signing up with webauthn using the same email
* -> this should work and create an entirely separate user with an unverified email address
*/
describe("SuperTokens WebAuthn Account Linking", function () {
let browser;
let page;
let consoleLogs = [];
let skipped = false;
const appConfig = {
enabledRecipes: ["webauthn", "emailpassword", "session", "dashboard", "userroles", "multifactorauth"],
};

before(async function () {
if (!(await isWebauthnSupported())) {
skipped = true;
this.skip();
}

await backendHook("before");
const coreUrl = await setupCoreApp();
appConfig.coreUrl = coreUrl;
await setupST(appConfig);

browser = await setupBrowser();
page = await browser.newPage();
page.on("console", (consoleObj) => {
const log = consoleObj.text();
if (log.startsWith("ST_LOGS")) {
consoleLogs.push(log);
}
});
});

after(async function () {
if (skipped) {
return;
}

await page?.close();
await browser?.close();
await backendHook("after");
});

afterEach(async function () {
await screenshotOnFailure(this, browser);
await backendHook("afterEach");
});

beforeEach(async function () {
await backendHook("beforeEach");
consoleLogs = [];
consoleLogs = await clearBrowserCookiesWithoutAffectingConsole(page, consoleLogs);
await toggleSignInSignUp(page);
});

it("Should create separate users when signing up with same email using different auth methods (account linking disabled)", async function () {
// Disable account linking
await setupST({
...appConfig,
accountLinkingConfig: {
enabled: true,
shouldAutoLink: {
shouldAutomaticallyLink: false,
shouldRequireVerification: false,
},
},
});
const email = await getTestEmail();

await Promise.all([
page.goto(`${TEST_CLIENT_BASE_URL}/auth?authRecipe=passwordless`),
page.waitForNavigation({ waitUntil: "networkidle0" }),
]);

// Signup using the email
await setInputValues(page, [{ name: "email", value: email }]);
await submitForm(page);

await waitForSTElement(page, "[data-supertokens~=input][name=userInputCode]");

const loginAttemptInfo = JSON.parse(
await page.evaluate(() => localStorage.getItem("supertokens-passwordless-loginAttemptInfo"))
);
const device = await getPasswordlessDevice(loginAttemptInfo);
await setInputValues(page, [{ name: "userInputCode", value: device.codes[0].userInputCode }]);
await submitForm(page);
await page.waitForTimeout(2000);

// We want to parse the text inside the session-context-userId div
const userId1 = await page.evaluate(() => document.querySelector(".session-context-userId").textContent);

// Find the div with classname logoutButton and click it using normal
// puppeteer selector
const logoutButton = await page.waitForSelector("div.logoutButton");
await logoutButton.click();
await new Promise((res) => setTimeout(res, 1000));

await tryWebauthnSignUp(page, email);

// We should be in the confirmation page now.
await submitForm(page);
await page.waitForTimeout(4000);

// Extract second userId from console logs
const userId2 = await page.evaluate(() => document.querySelector(".session-context-userId").textContent);

// Verify that two different users were created
assert.notStrictEqual(
userId1,
userId2,
"Different auth methods with same email should create separate users when account linking is disabled"
);
});

it("should handle email updates correctly for user that signed up with webauthn", async () => {
await page.evaluate(() => window.localStorage.setItem("mode", "REQUIRED"));
await setupST({
...appConfig,
accountLinkingConfig: {
enabled: true,
shouldAutoLink: {
shouldAutomaticallyLink: false,
shouldRequireVerification: false,
},
},
});
const email = await getTestEmail();

await tryWebauthnSignUp(page, email);

// We should be in the confirmation page now.
await submitForm(page);

await waitForUrl(page, "/auth/verify-email");

// we wait for email to be created
await new Promise((r) => setTimeout(r, 1000));

// we fetch the email verification link and go to that
const latestURLWithToken = await getLatestURLWithToken();
await Promise.all([page.waitForNavigation({ waitUntil: "networkidle0" }), page.goto(latestURLWithToken)]);

// click on the continue button
await Promise.all([submitForm(page), page.waitForNavigation({ waitUntil: "networkidle0" })]);
await waitForUrl(page, "/dashboard");

await page.waitForTimeout(4000);

// Change the email for the webauthn user
await Promise.all([page.waitForSelector(".sessionInfo-user-id"), page.waitForNetworkIdle()]);
const recipeUserId = await getUserIdWithFetch(page);
assert.ok(recipeUserId);

// Find the div with classname logoutButton and click it using normal
// puppeteer selector
const logoutButton = await page.waitForSelector("div.logoutButton");
await logoutButton.click();
await new Promise((res) => setTimeout(res, 1000));

const newEmail = getTestEmail("new");
const res = await changeEmail("webauthn", recipeUserId, newEmail, null);

// Sign in with the new email
await tryWebauthnSignIn(page);

// Since mode is required, user should be redirected to verify email
// screen as the email was changed and the new email is not verified.
await waitForUrl(page, "/auth/verify-email");

await page.waitForTimeout(4000);
});

it("should allow same emails to be linked but requiring verification", async () => {
await page.evaluate(() => window.localStorage.setItem("mode", "REQUIRED"));
await setupST({
...appConfig,
accountLinkingConfig: {
enabled: true,
shouldAutoLink: {
shouldAutomaticallyLink: true,
shouldRequireVerification: true,
},
},
});
const email = await getTestEmail();

await tryPasswordlessSignInUp(page, email);
await page.waitForTimeout(1000);

await Promise.all([page.waitForSelector(".sessionInfo-user-id"), page.waitForNetworkIdle()]);
const userId1 = await getUserIdWithFetch(page);
assert.ok(userId1);

await page.waitForTimeout(1000);

// Find the div with classname logoutButton and click it using normal
// puppeteer selector
const logoutButton = await page.waitForSelector("div.logoutButton");
await logoutButton.click();
await new Promise((res) => setTimeout(res, 1000));

// Try to signup with the same email through webauthn now
await tryWebauthnSignUp(page, email);

// We should be in the confirmation page now.
await submitForm(page);

await page.waitForTimeout(1000);
await waitForSTElement(page, "[data-supertokens~='generalError']");

// Try to recover the webauthn account using the same email
await openRecoveryAccountPage(page, email, true);
await page.waitForTimeout(1000);

// Get the token from the email
const token = await getTokenFromEmail(email);
assert.ok(token);

// Use the token to recover the account
await openRecoveryWithToken(page, token);

// We should be in the recovery page now, click the continue button
await submitFormUnsafe(page);

await new Promise((res) => setTimeout(res, 2000));

const successContainer = await waitForSTElement(page, "[data-supertokens~='headerText']");
const headerText = await successContainer.evaluate((el) => el.textContent);

// Assert the text contains "Account recovered successfully!"
assert.deepStrictEqual(headerText, "Account recovered successfully!");
});
});
Loading