From d345a439e35b4a55b32706b391a5afa750e2d37e Mon Sep 17 00:00:00 2001 From: Chris Smith <1979423+chris13524@users.noreply.github.com> Date: Mon, 19 Aug 2024 14:19:14 -0400 Subject: [PATCH] chore: Verify API tests (#2467) --- apps/laboratory/package.json | 2 +- .../pages/library/verify-domain-mismatch.tsx | 47 +++++++ .../src/pages/library/verify-evil.tsx | 55 ++++++++ .../src/pages/library/verify-valid.tsx | 56 ++++++++ .../pages/library/wagmi-permissions-async.tsx | 2 +- .../pages/library/wagmi-permissions-sync.tsx | 2 +- apps/laboratory/src/utils/WagmiConstants.ts | 8 +- .../tests/shared/fixtures/w3m-fixture.ts | 6 - .../w3m-verify-domain-mismatch-fixture.ts | 12 ++ .../fixtures/w3m-verify-evil-fixture.ts | 12 ++ .../fixtures/w3m-verify-valid-fixture.ts | 12 ++ .../tests/shared/pages/ModalPage.ts | 23 +++- apps/laboratory/tests/shared/utils/verify.ts | 38 ++++++ apps/laboratory/tests/verify.spec.ts | 125 ++++++++++++++++++ 14 files changed, 384 insertions(+), 16 deletions(-) create mode 100644 apps/laboratory/src/pages/library/verify-domain-mismatch.tsx create mode 100644 apps/laboratory/src/pages/library/verify-evil.tsx create mode 100644 apps/laboratory/src/pages/library/verify-valid.tsx create mode 100644 apps/laboratory/tests/shared/fixtures/w3m-verify-domain-mismatch-fixture.ts create mode 100644 apps/laboratory/tests/shared/fixtures/w3m-verify-evil-fixture.ts create mode 100644 apps/laboratory/tests/shared/fixtures/w3m-verify-valid-fixture.ts create mode 100644 apps/laboratory/tests/shared/utils/verify.ts create mode 100644 apps/laboratory/tests/verify.spec.ts diff --git a/apps/laboratory/package.json b/apps/laboratory/package.json index 2a78e67758..62023f71d0 100644 --- a/apps/laboratory/package.json +++ b/apps/laboratory/package.json @@ -11,7 +11,7 @@ "playwright:start": "pnpm start", "playwright:install": "playwright install --with-deps", "synpress": "synpress ./tests/wallet-setup", - "playwright:test": "playwright test --grep-invert 'metamask.spec.ts'", + "playwright:test": "playwright test", "playwright:test:metamask": "playwright test --grep 'metamask.spec.ts'", "playwright:test:basic": "playwright test --grep 'basic-tests.spec.ts'", "playwright:test:wallet": "playwright test --grep 'wallet.spec.ts'", diff --git a/apps/laboratory/src/pages/library/verify-domain-mismatch.tsx b/apps/laboratory/src/pages/library/verify-domain-mismatch.tsx new file mode 100644 index 0000000000..2c2c097b38 --- /dev/null +++ b/apps/laboratory/src/pages/library/verify-domain-mismatch.tsx @@ -0,0 +1,47 @@ +import { createWeb3Modal } from '@web3modal/wagmi/react' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { useEffect, useState } from 'react' +import { WagmiProvider } from 'wagmi' +import { AppKitButtons } from '../../components/AppKitButtons' +import { WagmiTests } from '../../components/Wagmi/WagmiTests' +import { ThemeStore } from '../../utils/StoreUtil' +import { ConstantsUtil } from '../../utils/ConstantsUtil' +import { WagmiModalInfo } from '../../components/Wagmi/WagmiModalInfo' +import { getWagmiConfig } from '../../utils/WagmiConstants' + +// Special project ID with verify enabled on localhost +const projectId = 'e4eae1aad4503db9966a04fd045a7e4d' + +const queryClient = new QueryClient() + +const wagmiConfig = getWagmiConfig('default', { + projectId +}) + +const modal = createWeb3Modal({ + wagmiConfig, + projectId, + metadata: ConstantsUtil.Metadata, + termsConditionsUrl: 'https://walletconnect.com/terms', + privacyPolicyUrl: 'https://walletconnect.com/privacy' +}) + +ThemeStore.setModal(modal) + +export default function Wagmi() { + const [ready, setReady] = useState(false) + + useEffect(() => { + setReady(true) + }, []) + + return ready ? ( + + + + + + + + ) : null +} diff --git a/apps/laboratory/src/pages/library/verify-evil.tsx b/apps/laboratory/src/pages/library/verify-evil.tsx new file mode 100644 index 0000000000..f9d7905a2c --- /dev/null +++ b/apps/laboratory/src/pages/library/verify-evil.tsx @@ -0,0 +1,55 @@ +import { createWeb3Modal } from '@web3modal/wagmi/react' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { useEffect, useState } from 'react' +import { WagmiProvider } from 'wagmi' +import { AppKitButtons } from '../../components/AppKitButtons' +import { WagmiTests } from '../../components/Wagmi/WagmiTests' +import { ThemeStore } from '../../utils/StoreUtil' +import { WagmiModalInfo } from '../../components/Wagmi/WagmiModalInfo' +import { getWagmiConfig } from '../../utils/WagmiConstants' + +const metadata = { + name: 'Evil Web3Modal', + description: 'Evil Web3Modal Laboratory', + url: 'https://malicious-app-verify-simulation.vercel.app/', + icons: ['https://avatars.githubusercontent.com/u/37784886'], + verifyUrl: '' +} + +// Special project ID with https://malicious-app-verify-simulation.vercel.app/ as the verified domain and this domain is marked as a scam +const projectId = '9d176efa3150a1df0a76c8c138b6b657' + +const queryClient = new QueryClient() + +const wagmiConfig = getWagmiConfig('default', { + projectId, + metadata +}) + +const modal = createWeb3Modal({ + wagmiConfig, + projectId, + metadata, + termsConditionsUrl: 'https://walletconnect.com/terms', + privacyPolicyUrl: 'https://walletconnect.com/privacy' +}) + +ThemeStore.setModal(modal) + +export default function Wagmi() { + const [ready, setReady] = useState(false) + + useEffect(() => { + setReady(true) + }, []) + + return ready ? ( + + + + + + + + ) : null +} diff --git a/apps/laboratory/src/pages/library/verify-valid.tsx b/apps/laboratory/src/pages/library/verify-valid.tsx new file mode 100644 index 0000000000..7253dfcd41 --- /dev/null +++ b/apps/laboratory/src/pages/library/verify-valid.tsx @@ -0,0 +1,56 @@ +import { createWeb3Modal } from '@web3modal/wagmi/react' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { useEffect, useState } from 'react' +import { WagmiProvider } from 'wagmi' +import { AppKitButtons } from '../../components/AppKitButtons' +import { WagmiTests } from '../../components/Wagmi/WagmiTests' +import { ThemeStore } from '../../utils/StoreUtil' +import { WagmiModalInfo } from '../../components/Wagmi/WagmiModalInfo' +import { getWagmiConfig } from '../../utils/WagmiConstants' + +const metadata = { + name: 'Web3Modal', + description: 'Web3Modal Laboratory', + // Allow localhost + url: 'http://localhost:3000', + icons: ['https://avatars.githubusercontent.com/u/37784886'], + verifyUrl: '' +} + +// Special project ID with verify enabled on localhost +const projectId = 'e4eae1aad4503db9966a04fd045a7e4d' + +const queryClient = new QueryClient() + +const wagmiConfig = getWagmiConfig('default', { + projectId, + metadata +}) + +const modal = createWeb3Modal({ + wagmiConfig, + projectId, + metadata, + termsConditionsUrl: 'https://walletconnect.com/terms', + privacyPolicyUrl: 'https://walletconnect.com/privacy' +}) + +ThemeStore.setModal(modal) + +export default function Wagmi() { + const [ready, setReady] = useState(false) + + useEffect(() => { + setReady(true) + }, []) + + return ready ? ( + + + + + + + + ) : null +} diff --git a/apps/laboratory/src/pages/library/wagmi-permissions-async.tsx b/apps/laboratory/src/pages/library/wagmi-permissions-async.tsx index 1007d3e578..88f9bfe1f6 100644 --- a/apps/laboratory/src/pages/library/wagmi-permissions-async.tsx +++ b/apps/laboratory/src/pages/library/wagmi-permissions-async.tsx @@ -20,7 +20,7 @@ const connectors = [ optionalMethods: [...OPTIONAL_METHODS, 'wallet_grantPermissions'] }) ] -const wagmiEmailConfig = getWagmiConfig('email', connectors) +const wagmiEmailConfig = getWagmiConfig('email', { connectors }) const modal = createWeb3Modal({ wagmiConfig: wagmiEmailConfig, projectId: ConstantsUtil.ProjectId, diff --git a/apps/laboratory/src/pages/library/wagmi-permissions-sync.tsx b/apps/laboratory/src/pages/library/wagmi-permissions-sync.tsx index e28c634f0c..d320566bab 100644 --- a/apps/laboratory/src/pages/library/wagmi-permissions-sync.tsx +++ b/apps/laboratory/src/pages/library/wagmi-permissions-sync.tsx @@ -20,7 +20,7 @@ const connectors = [ optionalMethods: [...OPTIONAL_METHODS, 'wallet_grantPermissions'] }) ] -const wagmiEmailConfig = getWagmiConfig('email', connectors) +const wagmiEmailConfig = getWagmiConfig('email', { connectors }) const modal = createWeb3Modal({ wagmiConfig: wagmiEmailConfig, projectId: ConstantsUtil.ProjectId, diff --git a/apps/laboratory/src/utils/WagmiConstants.ts b/apps/laboratory/src/utils/WagmiConstants.ts index b02e82914c..361a2e620e 100644 --- a/apps/laboratory/src/utils/WagmiConstants.ts +++ b/apps/laboratory/src/utils/WagmiConstants.ts @@ -18,7 +18,6 @@ import { type Chain } from 'wagmi/chains' import { ConstantsUtil } from './ConstantsUtil' -import type { CreateConnectorFn } from 'wagmi' export const WagmiConstantsUtil = { chains: [ @@ -40,21 +39,20 @@ export const WagmiConstantsUtil = { ] as [Chain, ...Chain[]] } -export function getWagmiConfig(type: 'default' | 'email', connectors: CreateConnectorFn[] = []) { +export function getWagmiConfig(type: 'default' | 'email', override = {}) { const config = { chains: WagmiConstantsUtil.chains, projectId: ConstantsUtil.ProjectId, metadata: ConstantsUtil.Metadata, ssr: true, - connectors + ...override } const emailConfig = { ...config, auth: { socials: ['google', 'x', 'discord', 'farcaster', 'github', 'apple', 'facebook'] - }, - connectors + } } const wagmiConfig = defaultWagmiConfig(type === 'email' ? emailConfig : config) diff --git a/apps/laboratory/tests/shared/fixtures/w3m-fixture.ts b/apps/laboratory/tests/shared/fixtures/w3m-fixture.ts index 4e4b7613d5..2ae08d7c79 100644 --- a/apps/laboratory/tests/shared/fixtures/w3m-fixture.ts +++ b/apps/laboratory/tests/shared/fixtures/w3m-fixture.ts @@ -1,14 +1,12 @@ /* eslint no-console: 0 */ import { ModalPage } from '../pages/ModalPage' -import { ModalValidator } from '../validators/ModalValidator' import { timeStart, timeEnd } from '../utils/logs' import { timingFixture } from './timing-fixture' // Declare the types of fixtures to use export interface ModalFixture { modalPage: ModalPage - modalValidator: ModalValidator library: string } @@ -32,10 +30,6 @@ export const testMSiwe = timingFixture.extend({ const modalPage = new ModalPage(page, library, 'siwe') await modalPage.load() await use(modalPage) - }, - modalValidator: async ({ modalPage }, use) => { - const modalValidator = new ModalValidator(modalPage.page) - await use(modalValidator) } }) diff --git a/apps/laboratory/tests/shared/fixtures/w3m-verify-domain-mismatch-fixture.ts b/apps/laboratory/tests/shared/fixtures/w3m-verify-domain-mismatch-fixture.ts new file mode 100644 index 0000000000..e87f4a3707 --- /dev/null +++ b/apps/laboratory/tests/shared/fixtures/w3m-verify-domain-mismatch-fixture.ts @@ -0,0 +1,12 @@ +import type { ModalFixture } from './w3m-fixture' +import { ModalPage } from '../pages/ModalPage' +import { timingFixture } from './timing-fixture' + +export const testMVerifyDomainMismatch = timingFixture.extend({ + library: ['wagmi', { option: true }], + modalPage: async ({ page, library }, use) => { + const modalPage = new ModalPage(page, library, 'verify-domain-mismatch') + await modalPage.load() + await use(modalPage) + } +}) diff --git a/apps/laboratory/tests/shared/fixtures/w3m-verify-evil-fixture.ts b/apps/laboratory/tests/shared/fixtures/w3m-verify-evil-fixture.ts new file mode 100644 index 0000000000..9e65c6e300 --- /dev/null +++ b/apps/laboratory/tests/shared/fixtures/w3m-verify-evil-fixture.ts @@ -0,0 +1,12 @@ +import type { ModalFixture } from './w3m-fixture' +import { ModalPage } from '../pages/ModalPage' +import { timingFixture } from './timing-fixture' + +export const testMVerifyEvil = timingFixture.extend({ + library: ['wagmi', { option: true }], + modalPage: async ({ page, library }, use) => { + const modalPage = new ModalPage(page, library, 'verify-evil') + await modalPage.load() + await use(modalPage) + } +}) diff --git a/apps/laboratory/tests/shared/fixtures/w3m-verify-valid-fixture.ts b/apps/laboratory/tests/shared/fixtures/w3m-verify-valid-fixture.ts new file mode 100644 index 0000000000..8b42c63bb0 --- /dev/null +++ b/apps/laboratory/tests/shared/fixtures/w3m-verify-valid-fixture.ts @@ -0,0 +1,12 @@ +import type { ModalFixture } from './w3m-fixture' +import { ModalPage } from '../pages/ModalPage' +import { timingFixture } from './timing-fixture' + +export const testMVerifyValid = timingFixture.extend({ + library: ['wagmi', { option: true }], + modalPage: async ({ page, library }, use) => { + const modalPage = new ModalPage(page, library, 'verify-valid') + await modalPage.load() + await use(modalPage) + } +}) diff --git a/apps/laboratory/tests/shared/pages/ModalPage.ts b/apps/laboratory/tests/shared/pages/ModalPage.ts index 81a02a4a35..4cc2aa2658 100644 --- a/apps/laboratory/tests/shared/pages/ModalPage.ts +++ b/apps/laboratory/tests/shared/pages/ModalPage.ts @@ -8,13 +8,28 @@ import { DeviceRegistrationPage } from './DeviceRegistrationPage' import type { TimingRecords } from '../fixtures/timing-fixture' import { WalletPage } from './WalletPage' import { WalletValidator } from '../validators/WalletValidator' +import { routeInterceptUrl } from '../utils/verify' -export type ModalFlavor = 'default' | 'siwe' | 'email' | 'wallet' | 'external' | 'all' +const maliciousUrl = 'https://malicious-app-verify-simulation.vercel.app' + +export type ModalFlavor = + | 'default' + | 'siwe' + | 'email' + | 'wallet' + | 'external' + | 'verify-valid' + | 'verify-domain-mismatch' + | 'verify-evil' + | 'all' function getUrlByFlavor(baseUrl: string, library: string, flavor: ModalFlavor) { const urlsByFlavor: Partial> = { default: `${baseUrl}library/${library}/`, - external: `${baseUrl}library/external/` + external: `${baseUrl}library/external/`, + 'verify-valid': `${baseUrl}library/verify-valid/`, + 'verify-domain-mismatch': `${baseUrl}library/verify-domain-mismatch/`, + 'verify-evil': maliciousUrl } return urlsByFlavor[flavor] || `${baseUrl}library/${library}-${flavor}/` @@ -37,6 +52,10 @@ export class ModalPage { } async load() { + if (this.flavor === 'verify-evil') { + await routeInterceptUrl(this.page, maliciousUrl, this.baseURL, '/library/verify-evil/') + } + await this.page.goto(this.url) } diff --git a/apps/laboratory/tests/shared/utils/verify.ts b/apps/laboratory/tests/shared/utils/verify.ts new file mode 100644 index 0000000000..eaccb4dee0 --- /dev/null +++ b/apps/laboratory/tests/shared/utils/verify.ts @@ -0,0 +1,38 @@ +import type { Page } from '@playwright/test' + +/* + * This function makes requests to the intercept URL be handled by the base URL + * This allows the browser APIs to think interceptUrl is the URL the page is on + */ +// eslint-disable-next-line max-params +export async function routeInterceptUrl( + page: Page, + interceptUrl: string, + baseUrl: string, + path: string +) { + await page.route(`${interceptUrl}/**/*`, async (route, request) => { + // eslint-disable-next-line init-declarations + let url: string + if (request.url() === `${interceptUrl}/`) { + url = `${baseUrl}${path}` + } else { + url = request.url().replace(interceptUrl, baseUrl) + } + const response = await fetch(url, { + method: request.method(), + headers: request.headers(), + body: request.postData() + }) + const headers: Record = {} + response.headers.forEach((value: string, key: string) => { + headers[key] = value + }) + const body = Buffer.from(await response.arrayBuffer()) + await route.fulfill({ + status: response.status, + headers, + body + }) + }) +} diff --git a/apps/laboratory/tests/verify.spec.ts b/apps/laboratory/tests/verify.spec.ts new file mode 100644 index 0000000000..4452f33ec2 --- /dev/null +++ b/apps/laboratory/tests/verify.spec.ts @@ -0,0 +1,125 @@ +import { DEFAULT_CHAIN_NAME, DEFAULT_SESSION_PARAMS } from './shared/constants' +import { testM } from './shared/fixtures/w3m-fixture' +import { testMVerifyDomainMismatch } from './shared/fixtures/w3m-verify-domain-mismatch-fixture' +import { testMVerifyEvil } from './shared/fixtures/w3m-verify-evil-fixture' +import { testMVerifyValid } from './shared/fixtures/w3m-verify-valid-fixture' +import { WalletPage } from './shared/pages/WalletPage' +import { ModalValidator } from './shared/validators/ModalValidator' +import { WalletValidator } from './shared/validators/WalletValidator' +import { expect } from '@playwright/test' + +testM( + 'connection and signature requests from non-verified project should show as cannot verify', + async ({ modalPage, context }) => { + const modalValidator = new ModalValidator(modalPage.page) + const walletPage = new WalletPage(await context.newPage()) + await walletPage.load() + const walletValidator = new WalletValidator(walletPage.page) + + const uri = await modalPage.getConnectUri() + await walletPage.connectWithUri(uri) + await expect(walletPage.page.getByText('Cannot Verify')).toBeVisible() + await walletPage.handleSessionProposal(DEFAULT_SESSION_PARAMS) + await modalValidator.expectConnected() + await walletValidator.expectConnected() + + await modalPage.sign() + const chainName = modalPage.library === 'solana' ? 'Solana' : DEFAULT_CHAIN_NAME + await walletValidator.expectReceivedSign({ chainName }) + await expect(walletPage.page.getByText('Cannot Verify')).toBeVisible() + await walletPage.handleRequest({ accept: true }) + await modalValidator.expectAcceptedSign() + + await modalPage.disconnect() + await modalValidator.expectDisconnected() + await walletValidator.expectDisconnected() + } +) + +testMVerifyValid( + 'connection and signature requests from non-scam verified domain should show as domain match', + async ({ modalPage, context }) => { + const modalValidator = new ModalValidator(modalPage.page) + const walletPage = new WalletPage(await context.newPage()) + await walletPage.load() + const walletValidator = new WalletValidator(walletPage.page) + + const uri = await modalPage.getConnectUri() + await walletPage.connectWithUri(uri) + await expect(walletPage.page.getByTestId('session-info-verified')).toBeVisible() + await walletPage.handleSessionProposal(DEFAULT_SESSION_PARAMS) + await modalValidator.expectConnected() + await walletValidator.expectConnected() + + await modalPage.sign() + const chainName = modalPage.library === 'solana' ? 'Solana' : DEFAULT_CHAIN_NAME + await walletValidator.expectReceivedSign({ chainName }) + await expect(walletPage.page.getByTestId('session-info-verified')).toBeVisible() + await walletPage.handleRequest({ accept: true }) + await modalValidator.expectAcceptedSign() + + await modalPage.disconnect() + await modalValidator.expectDisconnected() + await walletValidator.expectDisconnected() + } +) + +testMVerifyDomainMismatch( + 'connection and signature requests from non-scam verified domain but on localhost should show as invalid domain', + async ({ modalPage, context }) => { + const modalValidator = new ModalValidator(modalPage.page) + const walletPage = new WalletPage(await context.newPage()) + await walletPage.load() + const walletValidator = new WalletValidator(walletPage.page) + + const uri = await modalPage.getConnectUri() + await walletPage.connectWithUri(uri) + await expect(walletPage.page.getByText('Invalid Domain')).toBeVisible() + await walletPage.handleSessionProposal(DEFAULT_SESSION_PARAMS) + await modalValidator.expectConnected() + await walletValidator.expectConnected() + + await modalPage.sign() + const chainName = modalPage.library === 'solana' ? 'Solana' : DEFAULT_CHAIN_NAME + await walletValidator.expectReceivedSign({ chainName }) + await expect(walletPage.page.getByText('Invalid Domain')).toBeVisible() + await walletPage.handleRequest({ accept: true }) + await modalValidator.expectAcceptedSign() + + await modalPage.disconnect() + await modalValidator.expectDisconnected() + await walletValidator.expectDisconnected() + } +) + +testMVerifyEvil( + 'connection and signature requests from scam verified domain should show as scam domain', + async ({ modalPage, context }) => { + const modalValidator = new ModalValidator(modalPage.page) + const walletPage = new WalletPage(await context.newPage()) + await walletPage.load() + const walletValidator = new WalletValidator(walletPage.page) + + const uri = await modalPage.getConnectUri() + await walletPage.connectWithUri(uri) + await expect(walletPage.page.getByText('Website flagged')).toBeVisible() + await walletPage.page.getByText('Proceed anyway').click() + await expect(walletPage.page.getByText('Potential threat')).toBeVisible() + await walletPage.handleSessionProposal(DEFAULT_SESSION_PARAMS) + await modalValidator.expectConnected() + await walletValidator.expectConnected() + + await modalPage.sign() + const chainName = modalPage.library === 'solana' ? 'Solana' : DEFAULT_CHAIN_NAME + await expect(walletPage.page.getByText('Website flagged')).toBeVisible() + await walletPage.page.getByText('Proceed anyway').click() + await walletValidator.expectReceivedSign({ chainName }) + await expect(walletPage.page.getByText('Potential threat')).toBeVisible() + await walletPage.handleRequest({ accept: true }) + await modalValidator.expectAcceptedSign() + + await modalPage.disconnect() + await modalValidator.expectDisconnected() + await walletValidator.expectDisconnected() + } +)