diff --git a/.github/workflows/auth-dashboard.yml b/.github/workflows/auth-dashboard.yml new file mode 100644 index 0000000..b534235 --- /dev/null +++ b/.github/workflows/auth-dashboard.yml @@ -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 diff --git a/authservice/auth-dashboard/AuthDashboardPage.ts b/authservice/auth-dashboard/AuthDashboardPage.ts new file mode 100644 index 0000000..bf3be6c --- /dev/null +++ b/authservice/auth-dashboard/AuthDashboardPage.ts @@ -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="name@example.com"]`, 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; + } +} diff --git a/authservice/auth-dashboard/LoginAuthDashboardPage.ts b/authservice/auth-dashboard/LoginAuthDashboardPage.ts new file mode 100644 index 0000000..04d113c --- /dev/null +++ b/authservice/auth-dashboard/LoginAuthDashboardPage.ts @@ -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" }); + } +} diff --git a/authservice/auth-dashboard/auth-dashboard.test.ts b/authservice/auth-dashboard/auth-dashboard.test.ts new file mode 100644 index 0000000..e0f2c7e --- /dev/null +++ b/authservice/auth-dashboard/auth-dashboard.test.ts @@ -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(); + }); +}); diff --git a/authservice/login-with-passwordless/AuthServicePage.ts b/authservice/login-with-passwordless/AuthServicePage.ts index b8444cc..644c84b 100644 --- a/authservice/login-with-passwordless/AuthServicePage.ts +++ b/authservice/login-with-passwordless/AuthServicePage.ts @@ -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"]`); } @@ -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() { diff --git a/authservice/utils/index.ts b/authservice/utils/index.ts index 9d6cf81..969406b 100644 --- a/authservice/utils/index.ts +++ b/authservice/utils/index.ts @@ -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"; @@ -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×tamp_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
 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.
  *
@@ -765,6 +800,7 @@ export {
   generateEmailWithTag,
   generateRandomEmail,
   getBackUpPhrase,
+  getRecoveryPhase,
   // signInWithDapps,
   signInWithDiscord,
   signInWithEmail,
diff --git a/package.json b/package.json
index 0335a05..0d8fcca 100644
--- a/package.json
+++ b/package.json
@@ -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",
@@ -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",