Files
pocket-id/tests/specs/email-verification.spec.ts
T

191 lines
7.2 KiB
TypeScript

import base, { expect, type APIRequestContext } from '@playwright/test';
import { emailVerificationTokens, oidcClients, users } from '../data';
import { setAppConfigValues } from '../utils/app-config.util';
import authUtil from '../utils/auth.util';
import { cleanupBackend } from '../utils/cleanup.util';
import { startSmtpSink, verificationLinkPath, type SmtpSink } from '../utils/smtp.util';
import { saveUnsavedChanges } from '../utils/unsaved-changes.util';
const smtpSinkPort = 18125;
const test = base.extend<{ smtp: SmtpSink }>({
smtp: async ({}, use) => {
const sink = await startSmtpSink(smtpSinkPort);
try {
await use(sink);
} finally {
await sink.close();
}
}
});
// Requires email verification and points Pocket ID at the SMTP sink, so the verification links can be followed
async function requireEmailVerificationWithSmtp(request: APIRequestContext) {
await setAppConfigValues(request, {
emailVerification: 'required',
smtpHost: 'host.docker.internal',
smtpPort: String(smtpSinkPort),
smtpFrom: 'pocket-id@test.com',
smtpTls: 'none'
});
}
// Builds the query of an authorization code request for the Nextcloud client
function nextcloudAuthorizeParams() {
const oidcClient = oidcClients.nextcloud;
return new URLSearchParams({
client_id: oidcClient.id,
response_type: 'code',
scope: 'openid profile email',
redirect_uri: oidcClient.callbackUrl,
state: 'nXx-6Qr-owc1SHBa',
nonce: 'P1gN3PtpKHJgKUVcLpLjm'
});
}
// Marks Craig's email as verified through the admin API
async function markCraigVerified(request: APIRequestContext) {
const craig = await request.get(`/api/users/${users.craig.id}`).then((r) => r.json());
const response = await request.put(`/api/users/${users.craig.id}`, {
data: { ...craig, emailVerified: true }
});
expect(response.ok()).toBe(true);
}
test.describe('Required email verification', () => {
test.beforeEach(async ({ page }) => {
await cleanupBackend();
await requireEmailVerificationWithSmtp(page.request);
// Craig is a regular user whose email is not verified yet
await authUtil.changeUser(page, 'craig');
});
test('Unverified user can only reach their account', async ({ page }) => {
await page.goto('/settings/apps');
await page.waitForURL('/settings/account');
await expect(page.getByText('Verify your email address to use your account')).toBeVisible();
await expect(page.getByRole('link', { name: 'My Apps' })).toHaveCount(0);
const response = await page.request.get('/api/oidc/users/me/clients');
expect(response.status()).toBe(403);
});
test('Unverified user cannot sign in to a client', async ({ page }) => {
await page.goto(`/authorize?${nextcloudAuthorizeParams().toString()}`);
await expect(page).toHaveURL(/\/interaction\/error\?error=/);
await expect(
page.getByRole('paragraph').filter({
hasText: 'You must verify your email address before you can access this service.'
})
).toBeVisible();
await page.getByRole('link', { name: 'Verify email address' }).click();
await page.waitForURL('/settings/account');
});
test('Unverified user is stopped right after signing in during authorization', async ({
page
}) => {
// Sign out so the sign-in happens on the interaction page, with the passkey the setup already registered for Craig
await page.context().clearCookies();
await page.goto(`/authorize?${nextcloudAuthorizeParams().toString()}`);
await page.getByRole('button', { name: 'Sign in' }).click();
// Completing the sign-in step already requires a verified email, so the consent screen is never shown
await expect(
page.getByText('You must verify your email address before you can sign in to applications.')
).toBeVisible();
await expect(page.getByTestId('scopes')).toHaveCount(0);
await page.getByRole('link', { name: 'Verify email address' }).click();
await page.waitForURL('/settings/account');
});
test('Verifying the email restores full access', async ({ page }) => {
const token = emailVerificationTokens.find((t) => !t.expired)!.token;
await page.goto(`/verify-email?token=${token}`);
await page.waitForURL('/settings/account?emailVerificationState=success');
await page.goto('/settings/apps');
await expect(page).toHaveURL('/settings/apps');
await expect(page.getByRole('link', { name: 'My Apps' })).toBeVisible();
});
});
test.describe('Email change with required verification', () => {
test.beforeEach(async ({ page }) => {
await cleanupBackend();
await requireEmailVerificationWithSmtp(page.request);
await markCraigVerified(page.request);
await authUtil.changeUser(page, 'craig');
});
test('A new email takes effect once it is confirmed', async ({ page, smtp }) => {
const newEmail = 'craig.new@test.com';
await page.goto('/settings/account');
await page.getByLabel('Email').fill(newEmail);
await saveUnsavedChanges(page);
// The verified email stays in effect and the account keeps full access until the change is confirmed
await expect(page.getByText('Confirm your new email address')).toBeVisible();
await expect(page.getByLabel('Email')).toHaveValue(users.craig.email);
await page.goto('/settings/apps');
await expect(page).toHaveURL('/settings/apps');
await page.goto(verificationLinkPath(smtp, newEmail));
await page.waitForURL('/settings/account?emailVerificationState=success');
await expect(page.getByLabel('Email')).toHaveValue(newEmail);
await expect(page.getByText('Confirm your new email address')).toHaveCount(0);
});
test('A canceled email change can no longer be confirmed', async ({ page, smtp }) => {
const newEmail = 'craig.canceled@test.com';
await page.goto('/settings/account');
await page.getByLabel('Email').fill(newEmail);
await saveUnsavedChanges(page);
await page.getByRole('button', { name: 'Cancel change' }).click();
await expect(page.getByText('Email change canceled.')).toBeVisible();
await expect(page.getByText('Confirm your new email address')).toHaveCount(0);
await page.goto(verificationLinkPath(smtp, newEmail));
await page.waitForURL(
'/settings/account?emailVerificationState=Email+verification+token+is+invalid'
);
await expect(page.getByLabel('Email')).toHaveValue(users.craig.email);
});
});
test('Signing up sends a verification link that unlocks the account', async ({ page, smtp }) => {
await cleanupBackend();
await requireEmailVerificationWithSmtp(page.request);
await setAppConfigValues(page.request, { allowUserSignups: 'open' });
await page.context().clearCookies();
const email = 'verified.signup@test.com';
await page.goto('/signup');
await page.getByLabel('First name').fill('Verified');
await page.getByLabel('Last name').fill('Signup');
await page.getByLabel('Username').fill('verifiedsignup');
await page.getByLabel('Email').fill(email);
await page.getByRole('button', { name: 'Sign Up' }).click();
await page.waitForURL('/signup/add-passkey');
await page.getByRole('button', { name: 'Skip for now' }).click();
await page
.getByRole('alertdialog', { name: 'Skip Passkey Setup' })
.getByRole('button', { name: 'Skip for now' })
.click();
await page.waitForURL('/settings/account');
await page.goto(verificationLinkPath(smtp, email));
await page.waitForURL('/settings/account?emailVerificationState=success');
await page.goto('/settings/apps');
await expect(page).toHaveURL('/settings/apps');
});