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

test(e2e): test reconnect wallet with automatic key addition #830

Merged
merged 10 commits into from
Jan 15, 2025
3 changes: 3 additions & 0 deletions .github/workflows/tests-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions tests/e2e/helpers/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ConnectDetails, 'walletAddressUrl'>,
Expand Down
195 changes: 195 additions & 0 deletions tests/e2e/reconnectAutoKeyTestWallet.spec.ts
Original file line number Diff line number Diff line change
@@ -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\/(?<accountId>.+)\/wallet-addresses\/(?<walletId>.+)\/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);
});
});
Loading