Skip to content

Commit

Permalink
test: add tests into account-linking example
Browse files Browse the repository at this point in the history
  • Loading branch information
porcellus committed Oct 4, 2023
1 parent e84e083 commit ad4fa00
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 8 deletions.
2 changes: 1 addition & 1 deletion examples/with-account-linking/backend/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export const SuperTokensConfig: TypeInput = {
],
}),
Passwordless.init({
contactMethod: "PHONE",
contactMethod: "EMAIL_OR_PHONE",
flowType: "USER_INPUT_CODE_AND_MAGIC_LINK",
}),
Session.init(),
Expand Down
3 changes: 2 additions & 1 deletion examples/with-account-linking/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"react-dom": "^18.2.0",
"react-router-dom": "^6.2.1",
"react-scripts": "5.0.1",
"supertokens-auth-react": "github:supertokens/supertokens-auth-react#feat/account-linking",
"supertokens-auth-react": "^0.35.4",
"supertokens-web-js": "^0.8.0",
"typescript": "^4.8.2",
"web-vitals": "^2.1.4"
},
Expand Down
11 changes: 6 additions & 5 deletions examples/with-account-linking/frontend/src/LinkingPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export const LinkingPage: React.FC = () => {
setSuccess("Successfully added password");
setError(null);
}
}, [setError, setSuccess]);
}, [setError, setSuccess, password]);

const addPhoneNumber = useCallback(async () => {
const resp = await fetch(`${getApiDomain()}/addPhoneNumber`, {
Expand All @@ -62,7 +62,7 @@ export const LinkingPage: React.FC = () => {
setSuccess("Successfully added password");
}
loadUserInfo();
}, [setError, setSuccess, loadUserInfo]);
}, [setError, setSuccess, loadUserInfo, phoneNumber]);

useEffect(() => {
loadUserInfo();
Expand All @@ -87,7 +87,7 @@ export const LinkingPage: React.FC = () => {
) : (
<ul className="loginMethods">
{passwordLoginMethods.map((lm: any) => (
<div key={lm.recipeUserId} className="email-password login-method">
<div key={lm.recipeUserId} className="emailpassword login-method">
<span className="recipeId">{lm.recipeId}</span>
<span className="userId">{lm.recipeUserId}</span>
<span className="contactInfo"> Email: {lm.email}</span>
Expand All @@ -103,10 +103,11 @@ export const LinkingPage: React.FC = () => {
</div>
))}
{phoneLoginMethod.map((lm: any) => (
<div key={lm.recipeUserId} className="thirdparty login-method">
<div key={lm.recipeUserId} className="passwordless login-method">
<span className="recipeId">{lm.recipeId}</span>
<span className="userId">{lm.recipeUserId}</span>
<span className="contactInfo"> Phone number: {lm.phoneNumber}</span>
{lm.phoneNumber && <span className="contactInfo"> Phone number: {lm.phoneNumber}</span>}
{lm.email && <span className="contactInfo"> Email: {lm.email}</span>}
</div>
))}
</ul>
Expand Down
7 changes: 6 additions & 1 deletion examples/with-account-linking/frontend/src/config.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import ThirdPartyEmailPassword, { Google, Github, Apple } from "supertokens-auth-react/recipe/thirdpartyemailpassword";
import EmailVerification from "supertokens-auth-react/recipe/emailverification";
import { ThirdPartyEmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/thirdpartyemailpassword/prebuiltui";
import { PasswordlessPreBuiltUI } from "supertokens-auth-react/recipe/passwordless/prebuiltui";
import { EmailVerificationPreBuiltUI } from "supertokens-auth-react/recipe/emailverification/prebuiltui";
import Session from "supertokens-auth-react/recipe/session";
import Passwordless from "supertokens-auth-react/recipe/passwordless";

export function getApiDomain() {
const apiPort = process.env.REACT_APP_API_PORT || 3001;
Expand Down Expand Up @@ -50,6 +52,9 @@ export const SuperTokensConfig = {
],
},
}),
Passwordless.init({
contactMethod: "EMAIL_OR_PHONE",
}),
Session.init(),
],
};
Expand All @@ -58,4 +63,4 @@ export const recipeDetails = {
docsLink: "https://supertokens.com/docs/thirdpartyemailpassword/introduction",
};

export const PreBuiltUIList = [ThirdPartyEmailPasswordPreBuiltUI, EmailVerificationPreBuiltUI];
export const PreBuiltUIList = [ThirdPartyEmailPasswordPreBuiltUI, PasswordlessPreBuiltUI, EmailVerificationPreBuiltUI];
200 changes: 200 additions & 0 deletions examples/with-account-linking/test/basic.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved.
*
* This software is licensed under the Apache License, Version 2.0 (the
* "License") as published by the Apache Software Foundation.
*
* You may not use this file except in compliance with the License. You may
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

/*
* Imports
*/

const assert = require("assert");
const puppeteer = require("puppeteer");
const {
getTestEmail,
getTestPhoneNumber,
setInputValues,
submitForm,
toggleSignInSignUp,
waitForSTElement,
} = require("../../../test/exampleTestHelpers");

const SuperTokensNode = require("../backend/node_modules/supertokens-node");
const Session = require("../backend/node_modules/supertokens-node/recipe/session");
const EmailVerification = require("../backend/node_modules/supertokens-node/recipe/emailverification");
const EmailPassword = require("../backend/node_modules/supertokens-node/recipe/emailpassword");
const Passwordless = require("../backend/node_modules/supertokens-node/recipe/passwordless");

// Run the tests in a DOM environment.
require("jsdom-global")();

const apiDomain = "http://localhost:3001";
const websiteDomain = "http://localhost:3000";
SuperTokensNode.init({
supertokens: {
// We are running these tests without running a local ST instance
connectionURI: "https://try.supertokens.com",
},
appInfo: {
// These largely shouldn't matter except for creating links which we can change anyway
apiDomain: apiDomain,
websiteDomain: websiteDomain,
appName: "testNode",
},
recipeList: [
EmailVerification.init({ mode: "OPTIONAL" }),
EmailPassword.init(),
Session.init(),
Passwordless.init({
contactMethod: "EMAIL_OR_PHONE",
flowType: "USER_INPUT_CODE_AND_MAGIC_LINK",
}),
],
});

describe("SuperTokens Example Basic tests", function () {
let browser;
let page;
const email = getTestEmail();
const phoneNumber = getTestPhoneNumber();
const testOTP = "1234";
const testPW = "Str0ngP@ssw0rd";

before(async function () {
browser = await puppeteer.launch({
args: ["--no-sandbox", "--disable-setuid-sandbox"],
headless: false,
});
page = await browser.newPage();
});

after(async function () {
await browser.close();
});

describe("Email Password test", function () {
it("Successful signup with multiple credentials", async function () {
await Promise.all([page.goto(websiteDomain), page.waitForNavigation({ waitUntil: "networkidle0" })]);

// redirected to /auth
await toggleSignInSignUp(page);
await setInputValues(page, [
{ name: "email", value: email },
{ name: "password", value: testPW },
]);
await submitForm(page);

// Redirected to email verification screen
await waitForSTElement(page, "[data-supertokens~='sendVerifyEmailIcon']");
const userId = await page.evaluate(() => window.__supertokensSessionRecipe.getUserId());

// Attempt reloading Home
await Promise.all([page.goto(websiteDomain), page.waitForNavigation({ waitUntil: "networkidle0" })]);
await waitForSTElement(page, "[data-supertokens~='sendVerifyEmailIcon']");

// Create a new token and use it (we don't have access to the originally sent one)
const tokenInfo = await EmailVerification.createEmailVerificationToken(
"public",
SuperTokensNode.convertToRecipeUserId(userId),
email
);
await page.goto(`${websiteDomain}/auth/verify-email?token=${tokenInfo.token}`);

await submitForm(page);

await page.waitForSelector(".sessionButton");

await page.goto(`${websiteDomain}/link`);

await page.waitForSelector(".emailpassword.login-method");
await checkLoginMethods(page, [{ loginMethod: "emailpassword", email }]);

const input = await page.waitForSelector("[type=tel]");
await input.type(phoneNumber);

await page.click("[type=tel]+button");
await page.waitForSelector(".passwordless.login-method");

await checkLoginMethods(page, [
{ loginMethod: "emailpassword", email },
{ loginMethod: "passwordless", phoneNumber },
]);

await page.evaluate(() => __supertokensSessionRecipe.signOut({}));
await new Promise((res) => setTimeout(res, 200));

await page.goto(`${websiteDomain}/auth?rid=passwordless`);

await setInputValues(page, [{ name: "emailOrPhone", value: email }]);
await submitForm(page);

await waitForSTElement(page, "[name=userInputCode]");

const loginAttemptInfo = JSON.parse(
await page.evaluate(() => localStorage.getItem("supertokens-passwordless-loginAttemptInfo"))
);

await Passwordless.createNewCodeForDevice({
tenantId: "public",
deviceId: loginAttemptInfo.deviceId,
userInputCode: testOTP,
});

await setInputValues(page, [{ name: "userInputCode", value: testOTP }]);
await submitForm(page);
const callApiBtn = await page.waitForSelector(".sessionButton");
let setAlertContent;
let alertContent = new Promise((res) => (setAlertContent = res));
page.on("dialog", async (dialog) => {
setAlertContent(dialog.message());
await dialog.dismiss();
});
await callApiBtn.click();

const alertText = await alertContent;
assert(alertText.startsWith("Session Information:"));
const sessionInfo = JSON.parse(alertText.replace(/^Session Information:/, ""));
assert.strictEqual(sessionInfo.userId, userId);

await page.goto(`${websiteDomain}/link`);

await page.waitForSelector(".emailpassword.login-method");
await checkLoginMethods(page, [
{ loginMethod: "emailpassword", email },
{ loginMethod: "passwordless", phoneNumber },
{ loginMethod: "passwordless", email },
]);
});
});
});

async function checkLoginMethods(page, expectedLoginMethods) {
assert.strictEqual(await page.url(), `${websiteDomain}/link`);
const methodDivs = await page.$$(".login-method");

for (const div of methodDivs) {
const classNameProp = await div.getProperty("className");
const className = await classNameProp.jsonValue();
const method = className.split(" ")[0];
const contactInfo = (await (await div.$(".contactInfo")).evaluate((el) => el.textContent)).trim();
console.log({ expectedLoginMethods, method, contactInfo });
assert(
expectedLoginMethods.some(
(m) =>
m.loginMethod === method &&
(m.email === undefined || contactInfo === `Email: ${m.email}`) &&
(m.phoneNumber === undefined || contactInfo === `Phone number: ${m.phoneNumber}`)
)
);
}
assert.strictEqual(methodDivs.length, expectedLoginMethods.length);
}
5 changes: 5 additions & 0 deletions test/exampleTestHelpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ function getTestEmail() {
return `john.doe+${Date.now()}@supertokens.io`;
}

function getTestPhoneNumber() {
return `+3670${Date.now().toString().substring(6)}`;
}

async function getSignInOrSignUpSwitchLink(page) {
return waitForSTElement(
page,
Expand All @@ -142,6 +146,7 @@ module.exports = {
getFieldErrors,
setInputValues,
getTestEmail,
getTestPhoneNumber,
getSignInOrSignUpSwitchLink,
toggleSignInSignUp,
};

0 comments on commit ad4fa00

Please sign in to comment.