From bb3e0a705b72eb7a7e0238b47e7e5e220ce09643 Mon Sep 17 00:00:00 2001 From: darianm <47156919+DarianM@users.noreply.github.com> Date: Wed, 15 Jan 2025 12:15:30 +0200 Subject: [PATCH] test(e2e): test reconnect wallet with automatic key addition (#830) Co-authored-by: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> --- .github/workflows/tests-e2e.yml | 3 + tests/e2e/helpers/common.ts | 8 + tests/e2e/reconnectAutoKeyTestWallet.spec.ts | 195 +++++++++++++++++++ 3 files changed, 206 insertions(+) create mode 100644 tests/e2e/reconnectAutoKeyTestWallet.spec.ts diff --git a/.github/workflows/tests-e2e.yml b/.github/workflows/tests-e2e.yml index 77b74a24..2bf99b5e 100644 --- a/.github/workflows/tests-e2e.yml +++ b/.github/workflows/tests-e2e.yml @@ -32,6 +32,9 @@ jobs: environment: test steps: - uses: actions/checkout@v4 + with: + # In a pull request trigger, ref is required as GitHub Actions checks out in detached HEAD mode, meaning it doesn’t check out your branch by default. + ref: ${{ github.event.pull_request.head.sha }} - name: Environment setup uses: ./.github/actions/setup diff --git a/tests/e2e/helpers/common.ts b/tests/e2e/helpers/common.ts index c565e355..c8e9a8a6 100644 --- a/tests/e2e/helpers/common.ts +++ b/tests/e2e/helpers/common.ts @@ -12,6 +12,14 @@ export async function waitForWelcomePage(page: Page) { ); } +export async function waitForReconnectWelcomePage(page: Page) { + await page.waitForURL( + (url) => + url.href.startsWith(OPEN_PAYMENTS_REDIRECT_URL) && + url.searchParams.get('result') === 'key_add_success', + ); +} + export async function getContinueWaitTime( context: BrowserContext, params: Pick, diff --git a/tests/e2e/reconnectAutoKeyTestWallet.spec.ts b/tests/e2e/reconnectAutoKeyTestWallet.spec.ts new file mode 100644 index 00000000..4be5c007 --- /dev/null +++ b/tests/e2e/reconnectAutoKeyTestWallet.spec.ts @@ -0,0 +1,195 @@ +import { test, expect } from './fixtures/base'; +import { getJWKS, withResolvers } from '@/shared/helpers'; +import { + acceptGrant, + API_URL_ORIGIN, + DEFAULT_CONTINUE_WAIT_MS, + KEYS_PAGE_URL, + revokeKey, + waitForGrantConsentPage, +} from './helpers/testWallet'; +import { getStorage } from './fixtures/helpers'; +import { spy } from 'tinyspy'; +import { + getContinueWaitTime, + waitForWelcomePage, + waitForReconnectWelcomePage, +} from './helpers/common'; +import { disconnectWallet, fillPopup } from './pages/popup'; + +test('Reconnect to test wallet with automatic key addition', async ({ + page, + popup, + persistentContext: context, + background, + i18n, +}) => { + const walletAddressUrl = process.env.TEST_WALLET_ADDRESS_URL; + const monetizationCallback = spy<[Event], void>(); + const revokeInfo = await test.step('connect wallet', async () => { + const connectButton = await test.step('fill popup', async () => { + const connectButton = await fillPopup(popup, i18n, { + walletAddressUrl, + amount: '10', + recurring: false, + }); + return connectButton; + }); + + await test.step('asks for key-add consent', async () => { + await connectButton.click(); + expect( + popup.getByTestId('connect-wallet-auto-key-consent'), + ).toBeVisible(); + await popup + .getByRole('button', { + name: i18n.getMessage('connectWalletKeyService_label_consentAccept'), + }) + .click(); + }); + + const continueWaitMsPromise = getContinueWaitTime( + context, + { walletAddressUrl }, + DEFAULT_CONTINUE_WAIT_MS, + ); + + const revokeInfo = await test.step('adds key to wallet', async () => { + page = await context.waitForEvent('page', { + predicate: (page) => page.url().startsWith(KEYS_PAGE_URL), + }); + + const { resolve, reject, promise } = withResolvers<{ + accountId: string; + walletId: string; + }>(); + page.on('requestfinished', async function intercept(req) { + if (req.serviceWorker()) return; + if (req.method() !== 'POST') return; + const url = new URL(req.url()); + if (url.origin !== API_URL_ORIGIN) return; + if (!url.pathname.startsWith('/accounts/')) return; + if (!url.pathname.includes('/upload-key')) return; + + const pattern = + /^\/accounts\/(?.+)\/wallet-addresses\/(?.+)\/upload-key$/; + const match = url.pathname.match(pattern); + if (!match) { + throw new Error('no match for URL pattern'); + } + const result = match.groups as { accountId: string; walletId: string }; + + const res = await req.response(); + page.off('requestfinished', intercept); + if (!res) { + reject('no response from /upload-key API'); + } else { + resolve(result); + } + }); + + const { keyId } = await getStorage(background, ['keyId']); + const { accountId, walletId } = await promise; + + const jwks = await getJWKS(walletAddressUrl); + expect(jwks.keys.length).toBeGreaterThan(0); + const key = jwks.keys.find((key) => key.kid === keyId); + expect(key).toMatchObject({ kid: keyId }); + + return { accountId, walletId, keyId }; + }); + + await test.step('shows connect consent page', async () => { + await page.waitForURL((url) => + url.pathname.startsWith('/grant-interactions'), + ); + await waitForGrantConsentPage(page); + }); + + await test.step('connects', async () => { + const continueWaitMs = await continueWaitMsPromise; + await acceptGrant(page, continueWaitMs); + await waitForWelcomePage(page); + + await expect(background).toHaveStorage({ connected: true }); + }); + + return revokeInfo; + }); + + await test.step('revoke key', async () => { + const newPage = await context.newPage(); + await revokeKey(newPage, revokeInfo); + await newPage.close(); + + const { keys } = await getJWKS(walletAddressUrl); + expect(keys.find((key) => key.kid === revokeInfo.keyId)).toBeUndefined(); + }); + + await test.step('start monetization', async () => { + const playgroundUrl = 'https://webmonetization.org/play/'; + await page.goto(playgroundUrl); + + await page.exposeFunction('monetizationCallback', monetizationCallback); + await page.evaluate(() => { + window.addEventListener('monetization', monetizationCallback); + }); + + await page + .getByLabel('Wallet address/Payment pointer') + .fill(walletAddressUrl); + await page.getByRole('button', { name: 'Add monetization link' }).click(); + + await expect(monetizationCallback).toHaveBeenCalledTimes(0); + }); + + await test.step('asks for key-add consent to reconnect wallet', async () => { + const reconnectButton = popup.getByRole('button', { + name: i18n.getMessage('keyRevoked_action_reconnect'), + }); + await expect(reconnectButton).toBeVisible(); + await reconnectButton.click(); + + expect(popup.getByTestId('connect-wallet-auto-key-consent')).toBeVisible(); + await popup + .getByRole('button', { + name: i18n.getMessage('connectWalletKeyService_label_consentAccept'), + }) + .click(); + + const newPage = await context.waitForEvent('page', { + predicate: (page) => page.url().startsWith(KEYS_PAGE_URL), + }); + + await waitForReconnectWelcomePage(newPage); + await newPage.close(); + }); + + await test.step('make one-time payment after reconnecting the wallet', async () => { + await popup.reload(); + await expect(popup.getByTestId('home-page')).toBeVisible(); + await expect(popup.getByRole('button', { name: 'Send now' })).toBeVisible(); + + await popup.getByRole('textbox').fill('1.5'); + await popup.getByRole('button', { name: 'Send now' }).click(); + + await expect(monetizationCallback).toHaveBeenCalledTimes(1, { + timeout: 1000, + }); + await expect(monetizationCallback).toHaveBeenLastCalledWithMatching({ + paymentPointer: walletAddressUrl, + amountSent: { + currency: expect.stringMatching(/^[A-Z]{3}$/), + value: expect.stringMatching(/^1\.\d+$/), + }, + incomingPayment: expect.stringContaining( + new URL(walletAddressUrl).origin, + ), + }); + }); + + await test.step('revoke keys and disconnect wallet', async () => { + await disconnectWallet(popup); + await revokeKey(page, revokeInfo); + }); +});