Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

E2E - Auth Dashboard #419

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
53 changes: 53 additions & 0 deletions .github/workflows/auth-dashboard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Auth Dashboard - E2ETests

on:
workflow_dispatch:
push:
branches:
- master
schedule:
- cron: "*/30 * * * *"

env:
NODE_VERSION: 20

jobs:
build:
name: Auth Dashboard - E2ETests
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Trigger tests
env:
CI: true
CI_MODE: ${{ secrets.CI_MODE }}
SMS_MOBILE_NUMBER: ${{ secrets.SMS_MOBILE_NUMBER }}
LOGIN_MOBILE_NUMBER: ${{ secrets.LOGIN_MOBILE_NUMBER }}
BACKUP_PHRASE_PROD: ${{ secrets.BACKUP_PHRASE_PROD }}
BACKUP_PHRASE_CYAN: ${{ secrets.BACKUP_PHRASE_CYAN }}
BACKUP_PHRASE_AQUA: ${{ secrets.BACKUP_PHRASE_AQUA }}
TESTMAIL_APP_APIKEY: ${{ secrets.TESTMAIL_APP_APIKEY }}
MAIL_APP: ${{ secrets.MAIL_APP }}
run: |
ifconfig && npm install && npx playwright install && npm run test:authdashboard

- name: Get current timestamp
id: get-time
run: echo "::set-output name=timestamp::$(date +%Y%m%d%H%M%S)"

- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: artifact-${{ github.run_id }}-${{ github.job }}-${{ steps.get-time.outputs.timestamp }}
path: test-results/*
if-no-files-found: ignore
- name: Update Discord
uses: sarisia/actions-status-discord@v1
if: always()
with:
webhook: ${{ secrets.DISCORD_WEBHOOK }}
title: ${{ github.workflow}} - ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
status: ${{ job.status }}
nocontext: true
128 changes: 128 additions & 0 deletions authservice/auth-dashboard/AuthDashboardPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// playwright-dev-page.ts
import { Page } from "@playwright/test";

import { getRecoveryPhase } from "../utils";

function validateDate(dateTime: string) {
const regex = /^([0-2]\d|3[01])\/(0\d|1[0-2])\/\d{2} ([0-1]\d|2[0-3]):[0-5]\d$/;
return regex.test(dateTime);
}

export class AuthDashboardPage {
readonly page: Page;

constructor(page: Page) {
this.page = page;
}

async verifyStrongSecurity() {
const imageDisplay = await this.page.locator(`img[alt="Strong Security"]`).isVisible();
const titleAlertDisplay = await this.page.locator(`text="Strong Security"`).isVisible();
const descriptionAlertDisplay = await this.page
.locator(`text="Your account is secured with at least three recovery factors. You can still add more for added protection."`)
.isVisible();

return imageDisplay && titleAlertDisplay && descriptionAlertDisplay;
}

async verifyAuthenticatorSetup(email: string) {
return this.page.locator(`//div[*/div[text()='Authenticator App']]//div[text()='Web3Auth-${email}']`).isVisible();
}

async verifyEmailPasswordlessSetup(email: string) {
return this.page.locator(`//div[text()='email account']/following-sibling::div[text()='${email}']`).first().isVisible();
}

async verifyDeviceSetup(browserType: string) {
const browserRecord = await this.page.locator(`//div[*/div[text()='Device(s)']]//div[contains(text(),'${browserType}')]`).isVisible();
const currentTag = await this.page.locator(`//div[*/div[text()='Device(s)']]//div[contains(text(),'Current')]`).isVisible();

const timeFortmat = validateDate(
(await this.page.locator(`//div[*/div[text()='Device(s)']]//span[contains(text(),'Created: ')]`).textContent()).replace("Created: ", "")
);
return browserRecord && currentTag && timeFortmat;
}

async addPasswordFactor() {
await this.page.click(`text=" Setup Password"`);
await this.page.fill(`input[aria-placeholder="Set your password"]`, "Testing@123");
await this.page.fill(`input[aria-placeholder="Re-enter your password"]`, "Testing@123");
await this.page.click(`button[aria-label="Confirm"][type="submit"]`);
}

async addRecoverPhrase(emailRecovery: string, tag: string) {
await this.page.click(`text=" Generate recovery phrase"`);
await this.page.fill(`input[aria-placeholder="[email protected]"]`, emailRecovery);
await this.page.click(`button[data-testid="send-recovery-factor"]`);

const recoveryPhrase = await getRecoveryPhase({
email: emailRecovery,
tag,
timestamp: Math.floor(Date.now() / 1000),
});

await this.page.fill(`textarea[placeholder="Paste your recovery phrase"]`, recoveryPhrase);
await this.page.click(`button[data-testid="verify"]`);

await this.page.locator(`text="Recovery phrase sent to Email"`).waitFor({ state: "visible" });

return recoveryPhrase;
}

async verifyPasswordSetup() {
return !this.page.locator(`text=" Setup Password"`).isVisible() && this.page.locator(`text="Change Password"`).isVisible();
}

async verifyPasswordNotSetupYet() {
return this.page.locator(`text=" Setup Password"`).isVisible();
}

async changePasswordSetup() {
await this.page.click(`text="Change Password"`);
await this.page.fill(`input[aria-placeholder="Set your password"]`, "Testing@123");
await this.page.fill(`input[aria-placeholder="Re-enter your password"]`, "Testing@123");
await this.page.click(`button[aria-label="Confirm"][type="submit"]`);
}

async deletePasswordSetup() {
await this.page.click(`button[aria-label="Delete Password"]`);

await this.page.locator(`text="Remove Password"`).waitFor({ state: "visible" });

const lisEle = await this.page.$$(`button[aria-label="Confirm"]`);
for (const element of lisEle) {
if (await element.isVisible()) {
await element.click();
break;
}
}

await this.page.locator(`text=" Setup Password"`).waitFor({ state: "visible" });
}

async deleteRecoveryPhrase() {
await this.page.click(`button[aria-label="Delete Recovery Share"]`);

await this.page.locator(`text="Delete Recovery Phrase"`).waitFor({ state: "visible" });

const lisEle = await this.page.$$(`button[aria-label="Confirm"]`);
for (const element of lisEle) {
if (await element.isVisible()) {
await element.click();
break;
}
}

await this.page.locator(`text=" Generate recovery phrase"`).waitFor({ state: "visible" });
}

async verifyRecoverPhraseSetup(phrase: string, bkEmail: string) {
const content = await this.page.locator(`//div[text()='Recovery phrase']/parent::div/parent::div`).first().textContent();

const containPhrase = content.includes(phrase);
const containBkEmail = content.includes(bkEmail);
const dateTimeFormat = validateDate(content.split("Generated on: ")[1]);

return containBkEmail && containPhrase && dateTimeFormat;
}
}
27 changes: 27 additions & 0 deletions authservice/auth-dashboard/LoginAuthDashboardPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// playwright-dev-page.ts
import { Page } from "@playwright/test";

export class LoginAuthDashboardPage {
readonly page: Page;

constructor(page: Page) {
this.page = page;
}

async gotoLoginAuthDashboardPage() {
await this.page.goto("https://develop-account.web3auth.io/");
}

async clickLoginButton() {
await this.page.click(`text="Connect with Phone or Email"`);
}

async inputEmailPasswordless(email: string) {
await this.page.fill(`input[aria-labelledby="Phone or Email"]`, email);
}

async logout() {
await this.page.click(`button[aria-label="Logout"]`);
await this.page.locator(`input[aria-labelledby="Phone or Email"]`).waitFor({ state: "visible" });
}
}
66 changes: 66 additions & 0 deletions authservice/auth-dashboard/auth-dashboard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { expect, test } from "@playwright/test";

import { AuthServicePage } from "../login-with-passwordless/AuthServicePage";
import { delay, generateEmailWithTag, verifyEmailPasswordlessWithVerificationCode } from "../utils";
import { AuthDashboardPage } from "./AuthDashboardPage";
import { LoginAuthDashboardPage } from "./LoginAuthDashboardPage";

test.describe("Passwordless Login scenarios", () => {
test.setTimeout(90000);

test("Login and set up Auth Dashboard, @authdashboard", async ({ page, browser }) => {
const testEmail = generateEmailWithTag();
const testBackupEmail = generateEmailWithTag();
const loginPage = new LoginAuthDashboardPage(page);

// LOGIN TO THE DASHBOARD

await loginPage.gotoLoginAuthDashboardPage();
await loginPage.inputEmailPasswordless(testEmail);
await loginPage.clickLoginButton();

const tag = testEmail.split("@")[0].split(".")[1];
const tagBk = testBackupEmail.split("@")[0].split(".")[1];

await verifyEmailPasswordlessWithVerificationCode(page, browser, {
email: testEmail,
tag,
timestamp: Math.floor(Date.now() / 1000),
redirectMode: false,
previousCode: "",
});

await delay(2000);
const pages = browser.contexts()[0].pages();

const authServicePage = new AuthServicePage(pages[1]);
await pages[1].bringToFront();

await authServicePage.clickSetup2FA();
await authServicePage.setupAuthenticatorNewMFAFlow();
await authServicePage.finishSetupNewMFAList();
await authServicePage.setupPasskeyLater();
await authServicePage.confirmDone2FASetup();

const authDashboardPage = new AuthDashboardPage(page);
await delay(5000);
expect(await authDashboardPage.verifyEmailPasswordlessSetup(testEmail)).toBeTruthy();
expect(await authDashboardPage.verifyAuthenticatorSetup(testEmail)).toBeTruthy();
expect(await authDashboardPage.verifyDeviceSetup("Chrome")).toBeTruthy();

await authDashboardPage.addPasswordFactor();
await authDashboardPage.verifyPasswordSetup();

await authDashboardPage.changePasswordSetup();
await authDashboardPage.verifyPasswordSetup();

await authDashboardPage.deletePasswordSetup();
expect(await authDashboardPage.verifyPasswordNotSetupYet()).toBeTruthy();

const phrase = await authDashboardPage.addRecoverPhrase(testBackupEmail, tagBk);
await authDashboardPage.verifyRecoverPhraseSetup(phrase, testBackupEmail);
await authDashboardPage.deleteRecoveryPhrase();

await loginPage.logout();
});
});
6 changes: 5 additions & 1 deletion authservice/login-with-passwordless/AuthServicePage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ export class AuthServicePage {
await this.page.click(`[data-testid="skip"]`);
}

async skipMFASetup() {
await this.page.click(`[data-testid="skip"]`);
}

async skipPasskeySetup() {
await this.page.click(`[data-testid="skipPasskey"]`);
}
Expand All @@ -47,7 +51,7 @@ export class AuthServicePage {
}

async setupPasskeyLater() {
await this.page.click(`[data-testid="setupLater"]`);
if (process.env.CI !== "true") await this.page.click(`[data-testid="setupLater"]`);
}

async confirmDone2FASetup() {
Expand Down
36 changes: 36 additions & 0 deletions authservice/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/* eslint-disable no-unmodified-loop-condition */
import { Browser, expect, Page } from "@playwright/test";
// eslint-disable-next-line import/no-extraneous-dependencies
import * as cheerio from "cheerio";

import confirmEmail from "./confirmEmail";
process.env.APP_VERSION = "v4";
Expand Down Expand Up @@ -550,6 +552,39 @@ async function signInWithEmailWithTestEmailApp(page: Page, email: string, browse
}
}

async function getRecoveryPhase(config: { email: string; tag: string; timestamp: number }) {
try {
// Fetch the list of emails
const ENDPOINT = `https://api.testmail.app/api/json?apikey=${testEmailAppApiKey}&namespace=kelg8`;
const res = await axios.get(`${ENDPOINT}&tag=${config.tag}&livequery=true&timestamp_from=${config.timestamp}`);
const inbox = await res.data;
let preTagText = "";
let count = 0;

while (count < 5) {
await delay(2000);
if (inbox.emails && inbox.emails.length > 0) {
const emailBody = inbox.emails[0].html; // Get the first email's ID

// Parse the HTML using Cheerio
const $ = cheerio.load(emailBody);
preTagText = $("pre").first().text();

console.log("Text inside <pre> tag:", preTagText);
break;
} else {
console.log("No emails found.");
}

count++;
}

return preTagText;
} catch (error) {
console.error("Error fetching emails:", error);
}
}

/**
* Verify the email by retrieve the code in the email and input to the OTP box.
*
Expand Down Expand Up @@ -765,6 +800,7 @@ export {
generateEmailWithTag,
generateRandomEmail,
getBackUpPhrase,
getRecoveryPhase,
// signInWithDapps,
signInWithDiscord,
signInWithEmail,
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"test:authservice:case1": "playwright test authservice --grep=@nomfa --config=index.config.ts",
"test:authservice:case2": "playwright test authservice --grep=@nonemandatorymfa --config=index.config.ts",
"test:authservice:case3": "playwright test authservice --grep=@mandatorymfa --config=index.config.ts",
"test:authdashboard": "playwright test authservice --grep=@authdashboard --config=index.config.ts",
"walletservice:config": "playwright test walletservices/wallet-service --grep=@smoke --workers=1 --headed --config=index.config.ts",
"demowalletservice:config": "playwright test walletservices/demo-wallet-service --grep=@demo --workers=1 --headed --config=index.config.ts",
"trace:show": "playwright show-trace",
Expand All @@ -23,6 +24,7 @@
"axios": "^1.7.2",
"bip39": "^3.1.0",
"chance": "^1.1.12",
"cheerio": "^1.0.0",
"dotenv": "^16.4.5",
"generate-password": "^1.7.1",
"playwright": "^1.45.3",
Expand Down