Files
pocket-id/tests/specs/application-configuration.spec.ts
T

426 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
});
});