From 9a8be4dd85eecbdb3aed05a1383e7555beb36214 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mih=C3=A1ly=20Lengyel?= Date: Tue, 10 Oct 2023 07:17:49 +0200 Subject: [PATCH] test: add tests for account linking example (#749) * test: add tests into account-linking example * test: clean up account linking example test * ci: auto install deps in frontend and backend dirs of examples before tests * ci: auto install deps in frontend and backend dirs of examples before tests * ci: auto install deps in frontend and backend dirs of examples before tests * ci: auto install deps in frontend and backend dirs of examples before tests * ci: auto install deps in frontend and backend dirs of examples before tests * ci: make updateExampleAppDeps support with-localstorage * chore: update lib dep versions in example --- .github/workflows/test-examples.yml | 3 +- .../with-account-linking/backend/config.ts | 2 +- .../with-account-linking/backend/package.json | 2 +- .../frontend/package.json | 3 +- .../frontend/src/LinkingPage/index.tsx | 11 +- .../frontend/src/config.tsx | 7 +- .../with-account-linking/test/basic.test.js | 200 ++++++++++++++++++ test/exampleTestHelpers.js | 5 + test/updateExampleAppDeps.sh | 20 ++ 9 files changed, 242 insertions(+), 11 deletions(-) create mode 100644 examples/with-account-linking/test/basic.test.js create mode 100755 test/updateExampleAppDeps.sh diff --git a/.github/workflows/test-examples.yml b/.github/workflows/test-examples.yml index 0e5e8a632..a799a33da 100644 --- a/.github/workflows/test-examples.yml +++ b/.github/workflows/test-examples.yml @@ -25,8 +25,7 @@ jobs: working-directory: ${{ matrix.examplePath }} steps: - uses: actions/checkout@v2 - - run: npm install git+https://github.com:supertokens/supertokens-auth-react.git#$GITHUB_SHA - - run: npm install + - run: bash ../../test/updateExampleAppDeps.sh . - run: npm install mocha@6.1.4 jsdom-global@3.0.2 puppeteer@^11.0.0 isomorphic-fetch@^3.0.0 - run: npm run build || true - run: | diff --git a/examples/with-account-linking/backend/config.ts b/examples/with-account-linking/backend/config.ts index 3d03ffb2d..c565136d9 100644 --- a/examples/with-account-linking/backend/config.ts +++ b/examples/with-account-linking/backend/config.ts @@ -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(), diff --git a/examples/with-account-linking/backend/package.json b/examples/with-account-linking/backend/package.json index 59a4adeee..c4ba073e1 100644 --- a/examples/with-account-linking/backend/package.json +++ b/examples/with-account-linking/backend/package.json @@ -13,7 +13,7 @@ "helmet": "^5.1.0", "morgan": "^1.10.0", "npm-run-all": "^4.1.5", - "supertokens-node": "github:supertokens/supertokens-node#account-linking", + "supertokens-node": "latest", "ts-node-dev": "^2.0.0", "typescript": "^4.7.2" }, diff --git a/examples/with-account-linking/frontend/package.json b/examples/with-account-linking/frontend/package.json index cd6e6e145..009f61f7b 100644 --- a/examples/with-account-linking/frontend/package.json +++ b/examples/with-account-linking/frontend/package.json @@ -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": "latest", + "supertokens-web-js": "latest", "typescript": "^4.8.2", "web-vitals": "^2.1.4" }, diff --git a/examples/with-account-linking/frontend/src/LinkingPage/index.tsx b/examples/with-account-linking/frontend/src/LinkingPage/index.tsx index 90d47a34b..7c39ebbfa 100644 --- a/examples/with-account-linking/frontend/src/LinkingPage/index.tsx +++ b/examples/with-account-linking/frontend/src/LinkingPage/index.tsx @@ -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`, { @@ -62,7 +62,7 @@ export const LinkingPage: React.FC = () => { setSuccess("Successfully added password"); } loadUserInfo(); - }, [setError, setSuccess, loadUserInfo]); + }, [setError, setSuccess, loadUserInfo, phoneNumber]); useEffect(() => { loadUserInfo(); @@ -87,7 +87,7 @@ export const LinkingPage: React.FC = () => { ) : ( diff --git a/examples/with-account-linking/frontend/src/config.tsx b/examples/with-account-linking/frontend/src/config.tsx index 1a6ba9d8a..2e4dd5a2e 100644 --- a/examples/with-account-linking/frontend/src/config.tsx +++ b/examples/with-account-linking/frontend/src/config.tsx @@ -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; @@ -50,6 +52,9 @@ export const SuperTokensConfig = { ], }, }), + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + }), Session.init(), ], }; @@ -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]; diff --git a/examples/with-account-linking/test/basic.test.js b/examples/with-account-linking/test/basic.test.js new file mode 100644 index 000000000..b7e05fbd2 --- /dev/null +++ b/examples/with-account-linking/test/basic.test.js @@ -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: true, + }); + 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(); + + 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); +} diff --git a/test/exampleTestHelpers.js b/test/exampleTestHelpers.js index 70a089e3a..3ae7aeb2d 100644 --- a/test/exampleTestHelpers.js +++ b/test/exampleTestHelpers.js @@ -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, @@ -142,6 +146,7 @@ module.exports = { getFieldErrors, setInputValues, getTestEmail, + getTestPhoneNumber, getSignInOrSignUpSwitchLink, toggleSignInSignUp, }; diff --git a/test/updateExampleAppDeps.sh b/test/updateExampleAppDeps.sh new file mode 100755 index 000000000..be8602111 --- /dev/null +++ b/test/updateExampleAppDeps.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +cd $1; +npm i; +npm install git+https://github.com:supertokens/supertokens-auth-react.git#$GITHUB_SHA; + +if [ -d "frontend" ]; then + pushd frontend; + npm i; + grep supertokens-auth-react package.json && npm install git+https://github.com:supertokens/supertokens-auth-react.git#$GITHUB_SHA; + popd; +else + npm install git+https://github.com:supertokens/supertokens-auth-react.git#$GITHUB_SHA; +fi + +if [ -d "backend" ]; then + pushd backend; + npm i; + popd; +fi