mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-10-14 21:59:05 +02:00
191 lines
7.2 KiB
TypeScript
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');
|
|
});
|