mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-10-29 20:19:04 +01:00
426 lines
17 KiB
TypeScript
426 lines
17 KiB
TypeScript
import { expect, test } from '@playwright/test';
|
||
import { users } from '../data';
|
||
import { setAppConfigValues } from '../utils/app-config.util';
|
||
import { cleanupBackend } from '../utils/cleanup.util';
|
||
import { saveUnsavedChanges, unsavedChangesBar } from '../utils/unsaved-changes.util';
|
||
|
||
const configUrl = '/settings/admin/application-configuration';
|
||
|
||
test.beforeEach(async () => {
|
||
await cleanupBackend();
|
||
});
|
||
|
||
test('Update general configuration', async ({ page }) => {
|
||
await page.goto(configUrl);
|
||
await page.waitForURL(`${configUrl}/general`);
|
||
|
||
await page.getByLabel('Application name', { exact: true }).fill('Updated Name');
|
||
await page.getByLabel('Session duration', { exact: true }).fill('30');
|
||
|
||
await page.getByRole('button', { name: 'Home page' }).click();
|
||
await page.getByRole('option', { name: 'My apps' }).click();
|
||
await expect(unsavedChangesBar(page)).toContainText('3 unsaved changes');
|
||
|
||
await saveUnsavedChanges(page);
|
||
|
||
await page.reload();
|
||
|
||
await expect(page.getByLabel('Application name', { exact: true })).toHaveValue('Updated Name');
|
||
await expect(page.getByLabel('Session duration', { exact: true })).toHaveValue('30');
|
||
|
||
await page.getByRole('link', { name: 'Updated Name', exact: true }).click();
|
||
await page.waitForURL('/settings/apps');
|
||
});
|
||
|
||
test('Disable automatic client secret creation', async ({ page }) => {
|
||
await page.goto(`${configUrl}/oidc`);
|
||
const autoCreateSecret = page.getByRole('switch', { name: 'Create a secret for new clients' });
|
||
await expect(autoCreateSecret).toBeChecked();
|
||
await autoCreateSecret.click();
|
||
await saveUnsavedChanges(page);
|
||
|
||
await page.reload();
|
||
await expect(autoCreateSecret).not.toBeChecked();
|
||
|
||
const response = await page.request.post('/api/oidc/clients', {
|
||
data: { name: 'No Automatic Secret' }
|
||
});
|
||
expect(response.status()).toBe(201);
|
||
const createdClient = await response.json();
|
||
expect(createdClient.createdSecret).toBeUndefined();
|
||
expect(createdClient.credentials.secrets ?? []).toHaveLength(0);
|
||
});
|
||
|
||
test('Turning on a feature adds its tabs', async ({ page }) => {
|
||
await page.goto(`/settings/admin/users/${users.craig.id}`);
|
||
await expect(page.getByRole('tab', { name: 'General' })).toBeVisible();
|
||
await expect(page.getByRole('tab', { name: /^Custom claims/ })).toHaveCount(0);
|
||
|
||
await page.goto(`${configUrl}/features`);
|
||
await page.getByRole('switch', { name: 'Custom claims' }).click();
|
||
await expect(page.locator('[data-type="success"]')).toHaveText('Custom claims turned on');
|
||
await expect(page.getByRole('switch', { name: 'Custom claims' })).toBeChecked();
|
||
|
||
await page.goto(`/settings/admin/users/${users.craig.id}`);
|
||
await expect(page.getByRole('tab', { name: /^Custom claims/ })).toBeVisible();
|
||
});
|
||
|
||
test('A feature in use cannot be turned off', async ({ page }) => {
|
||
await page.goto(`${configUrl}/features`);
|
||
|
||
const apis = page.getByRole('switch', { name: 'APIs' });
|
||
await expect(apis).toBeChecked();
|
||
await expect(apis).toBeDisabled();
|
||
await expect(page.getByText('Stays on while any API exists.')).toBeVisible();
|
||
await expect(page.getByRole('link', { name: 'APIs' })).toBeVisible();
|
||
});
|
||
|
||
test('Unsaved changes keep the current tab until they are saved or discarded', async ({ page }) => {
|
||
await page.goto(`${configUrl}/general`);
|
||
await page.getByLabel('Application name', { exact: true }).fill('Kept On Tab');
|
||
|
||
await page.getByRole('tab', { name: 'Email' }).click();
|
||
await expect(page.getByRole('tab', { name: 'General' })).toHaveAttribute('aria-selected', 'true');
|
||
await expect(page).toHaveURL(`${configUrl}/general`);
|
||
|
||
await saveUnsavedChanges(page);
|
||
await page.getByRole('tab', { name: 'Email' }).click();
|
||
await page.waitForURL(`${configUrl}/email`);
|
||
await page.getByLabel('Host').fill('smtp.combined.test');
|
||
await page.getByLabel('Port').fill('587');
|
||
await page.getByLabel('From address').fill('combined@example.com');
|
||
await saveUnsavedChanges(page);
|
||
|
||
await page.getByRole('tab', { name: 'General' }).click();
|
||
await expect(page.getByLabel('Application name', { exact: true })).toHaveValue('Kept On Tab');
|
||
await page.getByRole('tab', { name: 'Email' }).click();
|
||
await expect(page.getByLabel('Host')).toHaveValue('smtp.combined.test');
|
||
});
|
||
|
||
test('Invalid email configuration is not saved', async ({ page }) => {
|
||
let updateRequests = 0;
|
||
page.on('request', (request) => {
|
||
if (
|
||
request.method() === 'PUT' &&
|
||
new URL(request.url()).pathname === '/api/application-configuration'
|
||
) {
|
||
updateRequests++;
|
||
}
|
||
});
|
||
|
||
await page.goto(`${configUrl}/email`);
|
||
await page.getByLabel('Host').fill('smtp.validation.test');
|
||
await page.getByLabel('From address').fill('validation@example.com');
|
||
|
||
await page.getByRole('button', { name: 'Save', exact: true }).click();
|
||
|
||
const smtpPort = page.getByLabel('Port');
|
||
await expect(
|
||
unsavedChangesBar(page).getByText('Couldn’t save. Check the highlighted fields.')
|
||
).toBeVisible();
|
||
await expect(smtpPort).toHaveAttribute('aria-invalid', 'true');
|
||
await expect(smtpPort).toBeFocused();
|
||
expect(updateRequests).toBe(0);
|
||
|
||
await smtpPort.fill('587');
|
||
await expect(smtpPort).toHaveAttribute('aria-invalid', 'false');
|
||
await saveUnsavedChanges(page);
|
||
|
||
await page.reload();
|
||
await expect(page.getByLabel('Host')).toHaveValue('smtp.validation.test');
|
||
await expect(page.getByLabel('Port')).toHaveValue('587');
|
||
await expect(page.getByLabel('From address')).toHaveValue('validation@example.com');
|
||
});
|
||
|
||
test.describe('Update sign-up configuration', () => {
|
||
test.beforeEach(async ({ page }) => {
|
||
await page.goto(`${configUrl}/signup`);
|
||
});
|
||
|
||
test('should save sign up mode', async ({ page }) => {
|
||
await page.getByRole('radio', { name: /^Anyone/ }).click();
|
||
|
||
await saveUnsavedChanges(page);
|
||
|
||
await page.reload();
|
||
|
||
await expect(page.getByRole('radio', { name: /^Anyone/ })).toBeChecked();
|
||
});
|
||
|
||
test('should save default user groups for new signups', async ({ page }) => {
|
||
const developersOption = page.getByRole('option', { name: 'Developers' });
|
||
const designersOption = page.getByRole('option', { name: 'Designers' });
|
||
|
||
await page.getByRole('combobox', { name: 'Default groups' }).click();
|
||
await developersOption.click();
|
||
await expect(developersOption).toBeChecked();
|
||
await designersOption.click();
|
||
await expect(designersOption).toBeChecked();
|
||
await page.keyboard.press('Escape');
|
||
|
||
await saveUnsavedChanges(page);
|
||
|
||
await page.reload();
|
||
|
||
await page.getByRole('combobox', { name: 'Default groups' }).click();
|
||
|
||
await expect(page.getByRole('option', { name: 'Developers' })).toBeChecked();
|
||
await expect(page.getByRole('option', { name: 'Designers' })).toBeChecked();
|
||
});
|
||
|
||
test('should save default custom claims for new signups', async ({ page }) => {
|
||
await setAppConfigValues(page.request, { enabledFeatures: '["customClaims"]' });
|
||
await page.reload();
|
||
await page.getByRole('button', { name: 'Add custom claim' }).click();
|
||
await page.getByPlaceholder('Key').fill('test-claim');
|
||
await page.getByPlaceholder('Value').fill('test-value');
|
||
await page.getByRole('button', { name: 'Add another' }).click();
|
||
await page.getByPlaceholder('Key').nth(1).fill('another-claim');
|
||
await page.getByPlaceholder('Value').nth(1).fill('another-value');
|
||
|
||
await saveUnsavedChanges(page);
|
||
|
||
await page.reload();
|
||
|
||
await expect(page.getByPlaceholder('Key').first()).toHaveValue('test-claim');
|
||
await expect(page.getByPlaceholder('Value').first()).toHaveValue('test-value');
|
||
await expect(page.getByPlaceholder('Key').nth(1)).toHaveValue('another-claim');
|
||
await expect(page.getByPlaceholder('Value').nth(1)).toHaveValue('another-value');
|
||
});
|
||
});
|
||
|
||
test('Update passkey configuration', async ({ page }) => {
|
||
await page.goto(`${configUrl}/passkeys`);
|
||
|
||
const userVerification = page.getByLabel('User verification');
|
||
const authenticatorType = page.getByLabel('Allowed authenticator type');
|
||
const allowSyncedPasskeys = page.getByRole('switch', { name: 'Allow synced passkeys' });
|
||
|
||
await expect(userVerification).toContainText('Required');
|
||
await userVerification.click();
|
||
await page.getByRole('option', { name: /^Preferred/ }).click();
|
||
|
||
await expect(authenticatorType).toContainText('Any passkey');
|
||
await authenticatorType.click();
|
||
await page.getByRole('option', { name: /^Security keys only/ }).click();
|
||
|
||
await expect(allowSyncedPasskeys).toBeChecked();
|
||
await allowSyncedPasskeys.click();
|
||
await expect(
|
||
page.getByText('People whose only passkey is synced won’t be able to sign in.')
|
||
).toBeVisible();
|
||
|
||
await saveUnsavedChanges(page);
|
||
|
||
const registrationResponse = await page.request.get('/api/webauthn/register/start');
|
||
expect(registrationResponse.ok()).toBeTruthy();
|
||
await expect(registrationResponse.json()).resolves.toMatchObject({
|
||
authenticatorSelection: {
|
||
authenticatorAttachment: 'cross-platform',
|
||
userVerification: 'preferred'
|
||
}
|
||
});
|
||
|
||
const loginResponse = await page.request.get('/api/webauthn/login/start');
|
||
expect(loginResponse.ok()).toBeTruthy();
|
||
await expect(loginResponse.json()).resolves.toMatchObject({
|
||
userVerification: 'preferred'
|
||
});
|
||
|
||
await page.reload();
|
||
|
||
await expect(page.getByLabel('User verification')).toContainText('Preferred');
|
||
await expect(page.getByLabel('Allowed authenticator type')).toContainText('Security keys only');
|
||
await expect(page.getByRole('switch', { name: 'Allow synced passkeys' })).not.toBeChecked();
|
||
});
|
||
|
||
test('Update email configuration', async ({ page }) => {
|
||
await page.goto(`${configUrl}/email`);
|
||
|
||
await page.getByLabel('Host').fill('smtp.gmail.com');
|
||
await page.getByLabel('Port').fill('587');
|
||
await page.getByLabel('Username').fill('test@gmail.com');
|
||
await page.getByLabel('Password').fill('password');
|
||
await page.getByLabel('From address').fill('test@gmail.com');
|
||
await page.getByLabel('Sign-in notifications').click();
|
||
await page.getByRole('option', { name: /^Unrecognized browser/ }).click();
|
||
await page.getByLabel('Login codes requested by people').click();
|
||
await page.getByLabel('Login codes sent by admins').click();
|
||
await page.getByLabel('API key expiry warnings').click();
|
||
await page.getByLabel('Email verification', { exact: true }).click();
|
||
await page.getByRole('option', { name: /^Required/ }).click();
|
||
|
||
await saveUnsavedChanges(page);
|
||
|
||
await page.reload();
|
||
|
||
await expect(page.getByLabel('Host')).toHaveValue('smtp.gmail.com');
|
||
await expect(page.getByLabel('Port')).toHaveValue('587');
|
||
await expect(page.getByLabel('Username')).toHaveValue('test@gmail.com');
|
||
await expect(page.getByLabel('Password')).toHaveValue('password');
|
||
await expect(page.getByLabel('From address')).toHaveValue('test@gmail.com');
|
||
await expect(page.getByLabel('Sign-in notifications')).toContainText('Unrecognized browser');
|
||
await expect(page.getByLabel('Login codes requested by people')).toBeChecked();
|
||
await expect(page.getByLabel('Login codes sent by admins')).toBeChecked();
|
||
await expect(page.getByLabel('API key expiry warnings')).toBeChecked();
|
||
await expect(page.getByLabel('Email verification', { exact: true })).toContainText('Required');
|
||
});
|
||
|
||
test('Required email verification turns off treating new addresses as verified', async ({
|
||
page
|
||
}) => {
|
||
await page.goto(`${configUrl}/email`);
|
||
|
||
const treatAsVerified = page.getByLabel('Treat new addresses as verified');
|
||
await treatAsVerified.click();
|
||
await expect(treatAsVerified).toBeChecked();
|
||
|
||
await page.getByLabel('Email verification', { exact: true }).click();
|
||
await page.getByRole('option', { name: /^Required/ }).click();
|
||
|
||
await expect(treatAsVerified).not.toBeChecked();
|
||
await expect(treatAsVerified).toBeDisabled();
|
||
|
||
await page.getByLabel('Email verification', { exact: true }).click();
|
||
await page.getByRole('option', { name: /^Optional/ }).click();
|
||
|
||
await expect(treatAsVerified).toBeEnabled();
|
||
});
|
||
|
||
test('Save LDAP configuration while LDAP remains disabled', async ({ page }) => {
|
||
await page.goto(`${configUrl}/ldap`);
|
||
|
||
// isVisible does not wait, so wait for the status region that renders together with its action
|
||
await expect(page.getByRole('region', { name: 'Status' })).toBeVisible();
|
||
const disableButton = page.getByRole('button', { name: 'Disable LDAP…' });
|
||
if (await disableButton.isVisible()) {
|
||
await disableButton.click();
|
||
await page.getByRole('button', { name: 'Disable LDAP', exact: true }).click();
|
||
await expect(page.getByRole('button', { name: 'Enable LDAP…' })).toBeVisible();
|
||
}
|
||
|
||
const softDeleteUsers = page.getByRole('switch', { name: 'Keep people removed from LDAP' });
|
||
const originalValue = await softDeleteUsers.isChecked();
|
||
await softDeleteUsers.click();
|
||
await expect(unsavedChangesBar(page)).toContainText(/\d+ unsaved changes?/);
|
||
|
||
await page.getByRole('button', { name: 'Discard', exact: true }).click();
|
||
await expect(softDeleteUsers).toBeChecked({ checked: originalValue });
|
||
|
||
await softDeleteUsers.click();
|
||
await saveUnsavedChanges(page);
|
||
|
||
await page.reload();
|
||
await expect(page.getByRole('button', { name: 'Enable LDAP…' })).toBeVisible();
|
||
await expect(page.getByText('Not connected', { exact: true })).toBeVisible();
|
||
await expect(page.getByRole('switch', { name: 'Keep people removed from LDAP' })).toBeChecked({
|
||
checked: !originalValue
|
||
});
|
||
});
|
||
|
||
test.describe('Update application images', () => {
|
||
test.beforeEach(async ({ page }) => {
|
||
await page.goto(`${configUrl}/branding`);
|
||
});
|
||
|
||
test('should detect image removals as unsaved changes', async ({ page }) => {
|
||
await page
|
||
.getByLabel('Replace sign-in background', { exact: true })
|
||
.setInputFiles('resources/images/clouds.jpg');
|
||
await saveUnsavedChanges(page);
|
||
|
||
const removeBackground = page.getByRole('button', {
|
||
name: 'Remove sign-in background',
|
||
exact: true
|
||
});
|
||
await removeBackground.click();
|
||
await expect(removeBackground).toBeDisabled();
|
||
await expect(unsavedChangesBar(page)).toContainText(/\d+ unsaved changes?/);
|
||
|
||
await page.getByRole('button', { name: 'Discard', exact: true }).click();
|
||
await expect(removeBackground).toBeEnabled();
|
||
});
|
||
|
||
test('should upload images and reset custom logos', async ({ page }) => {
|
||
await page
|
||
.getByLabel('Replace favicon', { exact: true })
|
||
.setInputFiles('resources/images/w3-schools-favicon.ico');
|
||
await page
|
||
.getByLabel('Replace light logo', { exact: true })
|
||
.setInputFiles('resources/images/pingvin-share-logo.png');
|
||
await page
|
||
.getByLabel('Replace dark logo', { exact: true })
|
||
.setInputFiles('resources/images/cloud-logo.png');
|
||
await page
|
||
.getByLabel('Replace email logo', { exact: true })
|
||
.setInputFiles('resources/images/pingvin-share-logo.png');
|
||
await page
|
||
.getByLabel('Replace default profile picture', { exact: true })
|
||
.setInputFiles('resources/images/pingvin-share-logo.png');
|
||
await page
|
||
.getByLabel('Replace sign-in background', { exact: true })
|
||
.setInputFiles('resources/images/clouds.jpg');
|
||
await saveUnsavedChanges(page);
|
||
|
||
await page.request
|
||
.get('/api/application-images/favicon')
|
||
.then((res) => expect.soft(res.status()).toBe(200));
|
||
await page.request
|
||
.get('/api/application-images/logo?light=true')
|
||
.then((res) => expect.soft(res.status()).toBe(200));
|
||
await page.request
|
||
.get('/api/application-images/logo?light=false')
|
||
.then((res) => expect.soft(res.status()).toBe(200));
|
||
await page.request
|
||
.get('/api/application-images/email')
|
||
.then((res) => expect.soft(res.status()).toBe(200));
|
||
await page.request
|
||
.get('/api/application-images/background')
|
||
.then((res) => expect.soft(res.status()).toBe(200));
|
||
|
||
await page.getByRole('button', { name: 'Reset to default, light logo', exact: true }).click();
|
||
await page.getByRole('button', { name: 'Reset to default, dark logo', exact: true }).click();
|
||
const logoDeleteResponses = [true, false].map((light) =>
|
||
page.waitForResponse((response) => {
|
||
const url = new URL(response.url());
|
||
return (
|
||
response.request().method() === 'DELETE' &&
|
||
url.pathname === '/api/application-images/logo' &&
|
||
url.searchParams.get('light') === String(light)
|
||
);
|
||
})
|
||
);
|
||
await saveUnsavedChanges(page);
|
||
for (const response of await Promise.all(logoDeleteResponses)) {
|
||
expect(response.status()).toBe(204);
|
||
}
|
||
|
||
// Without a custom logo the endpoint falls back to the logo bundled with Pocket ID
|
||
await page.request
|
||
.get('/api/application-images/logo?light=true')
|
||
.then((res) => expect.soft(res.status()).toBe(200));
|
||
await page.request
|
||
.get('/api/application-images/logo?light=false')
|
||
.then((res) => expect.soft(res.status()).toBe(200));
|
||
|
||
// The bundled logo can be skipped to check whether a custom logo is set
|
||
await page.request
|
||
.get('/api/application-images/logo?light=true&default=false')
|
||
.then((res) => expect.soft(res.status()).toBe(404));
|
||
await page.request
|
||
.get('/api/application-images/logo?light=false&default=false')
|
||
.then((res) => expect.soft(res.status()).toBe(404));
|
||
});
|
||
|
||
test('should only allow png/jpeg for email logo', async ({ page }) => {
|
||
const emailLogoInput = page.getByLabel('Replace email logo', { exact: true });
|
||
|
||
await emailLogoInput.setInputFiles('resources/images/cloud-logo.svg');
|
||
await page.getByRole('button', { name: 'Save', exact: true }).click();
|
||
|
||
await expect(
|
||
unsavedChangesBar(page).getByText('File must be of type PNG or JPEG', { exact: true })
|
||
).toBeVisible();
|
||
});
|
||
});
|