317 lines
12 KiB
JavaScript
317 lines
12 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import process from 'node:process';
|
|
const { chromium } = await import(
|
|
process.env.PLAYWRIGHT_MODULE ?? 'playwright'
|
|
);
|
|
const browser = await chromium.launch({ channel: 'chrome', headless: true });
|
|
const base = 'http://127.0.0.1:4185';
|
|
const context = await browser.newContext({
|
|
viewport: { width: 1440, height: 1000 },
|
|
permissions: ['clipboard-read', 'clipboard-write'],
|
|
});
|
|
const page = await context.newPage();
|
|
page.setDefaultTimeout(10000);
|
|
const errors = [];
|
|
page.on('pageerror', (error) => errors.push(error.message));
|
|
page.on('dialog', (dialog) => dialog.accept());
|
|
const result = (value) =>
|
|
context.addCookies([{ name: 'security_test_result', value, url: base }]);
|
|
const enabled = (value) =>
|
|
context.addCookies([
|
|
{ name: 'security_enabled', value: String(value), url: base },
|
|
]);
|
|
const button = (name) => page.getByRole('button', { name, exact: true });
|
|
const dialog = () => page.getByRole('dialog');
|
|
let recoveryFetches = 0;
|
|
page.on('request', (request) => {
|
|
if (
|
|
request.url().endsWith('/user/two-factor-recovery-codes') &&
|
|
request.method() === 'GET'
|
|
) {
|
|
recoveryFetches++;
|
|
}
|
|
});
|
|
|
|
try {
|
|
for (const width of [1440, 390, 320]) {
|
|
await page.setViewportSize({ width, height: 1000 });
|
|
for (const theme of ['light', 'dark']) {
|
|
await page.emulateMedia({ colorScheme: theme });
|
|
for (const path of ['security', 'applications']) {
|
|
await page.goto(`${base}/settings/${path}`);
|
|
await page
|
|
.getByRole('heading', {
|
|
name:
|
|
path === 'security'
|
|
? 'Update password'
|
|
: 'Applications',
|
|
exact: true,
|
|
})
|
|
.waitFor();
|
|
assert.equal(
|
|
await page.evaluate(
|
|
() =>
|
|
document.documentElement.scrollWidth <=
|
|
window.innerWidth,
|
|
),
|
|
true,
|
|
`${path} overflow at ${width}/${theme}`,
|
|
);
|
|
assert.equal(
|
|
await page
|
|
.locator('html')
|
|
.evaluate((el) => el.classList.contains('dark')),
|
|
theme === 'dark',
|
|
);
|
|
assert.ok(
|
|
(await page.locator('section').first().boundingBox())
|
|
.width <= width,
|
|
`${path} content is clipped at ${width}/${theme}`,
|
|
);
|
|
}
|
|
await page.goto(`${base}/settings/security`);
|
|
await button('Enable 2FA').click();
|
|
await page.getByLabel('Manual setup key').waitFor();
|
|
assert.equal(
|
|
await page.getByLabel('Manual setup key').inputValue(),
|
|
'MANUALSETUPTESTKEY',
|
|
);
|
|
assert.equal(
|
|
await dialog().evaluate(
|
|
(el) => el.scrollWidth <= el.clientWidth,
|
|
),
|
|
true,
|
|
);
|
|
await button('Continue').click();
|
|
const code = page.getByLabel('Authentication code', {
|
|
exact: true,
|
|
});
|
|
await code.waitFor();
|
|
assert.equal(await code.getAttribute('inputmode'), 'numeric');
|
|
assert.equal(
|
|
await code.getAttribute('autocomplete'),
|
|
'one-time-code',
|
|
);
|
|
await code.focus();
|
|
await page.keyboard.press('Tab');
|
|
assert.equal(
|
|
await button('Back').evaluate(
|
|
(el) => el === document.activeElement,
|
|
),
|
|
true,
|
|
);
|
|
await page.keyboard.press('Escape');
|
|
await dialog().waitFor({ state: 'hidden' });
|
|
assert.equal(await page.getByLabel('Manual setup key').count(), 0);
|
|
assert.match(
|
|
await page.locator('body').innerText(),
|
|
/Awaiting confirmation/,
|
|
);
|
|
}
|
|
}
|
|
await page.setViewportSize({ width: 390, height: 1000 });
|
|
await button('Continue setup').click();
|
|
await button('Continue').click();
|
|
await result('validation');
|
|
await page
|
|
.getByLabel('Authentication code', { exact: true })
|
|
.fill('000000');
|
|
await button('Confirm').click();
|
|
await page
|
|
.getByText('The provided two factor authentication code was invalid.', {
|
|
exact: true,
|
|
})
|
|
.waitFor();
|
|
assert.equal(
|
|
await page
|
|
.getByLabel('Authentication code', { exact: true })
|
|
.getAttribute('aria-invalid'),
|
|
'true',
|
|
);
|
|
assert.equal(
|
|
await page
|
|
.getByLabel('Authentication code', { exact: true })
|
|
.inputValue(),
|
|
'',
|
|
);
|
|
await result('success');
|
|
await page
|
|
.getByLabel('Authentication code', { exact: true })
|
|
.fill('123456');
|
|
await button('Confirm').click();
|
|
await dialog().waitFor({ state: 'hidden' });
|
|
await button('Disable 2FA').waitFor();
|
|
assert.equal(
|
|
recoveryFetches,
|
|
0,
|
|
'Recovery codes are fetched only on request',
|
|
);
|
|
await button('View recovery codes').click();
|
|
await page.getByText('old-code-0001', { exact: true }).waitFor();
|
|
await button('Hide recovery codes').click();
|
|
assert.equal(
|
|
await page.getByText('old-code-0001', { exact: true }).count(),
|
|
0,
|
|
);
|
|
await button('View recovery codes').click();
|
|
await page.getByText('old-code-0001', { exact: true }).waitFor();
|
|
assert.equal(recoveryFetches, 2);
|
|
await button('Regenerate codes').click();
|
|
await dialog().getByRole('button', { name: 'Cancel', exact: true }).click();
|
|
assert.equal(
|
|
await page.getByText('old-code-0001', { exact: true }).count(),
|
|
1,
|
|
);
|
|
await button('Regenerate codes').click();
|
|
await dialog()
|
|
.getByRole('button', { name: 'Regenerate codes', exact: true })
|
|
.click();
|
|
await dialog().waitFor({ state: 'hidden' });
|
|
await page.getByText('old-code-0001', { exact: true }).waitFor();
|
|
assert.equal(recoveryFetches, 3);
|
|
await button('Hide recovery codes').click();
|
|
await page.route('**/user/two-factor-recovery-codes', async (route) => {
|
|
if (route.request().method() === 'GET') {
|
|
await new Promise((resolve) => setTimeout(resolve, 350));
|
|
await route.fulfill({ json: ['late-secret-code'] });
|
|
} else {
|
|
await route.continue();
|
|
}
|
|
});
|
|
await button('View recovery codes').click();
|
|
await button('Hide recovery codes').click();
|
|
await page.waitForTimeout(450);
|
|
assert.equal(
|
|
await page.getByText('late-secret-code', { exact: true }).count(),
|
|
0,
|
|
);
|
|
await page.unroute('**/user/two-factor-recovery-codes');
|
|
await page.route('**/user/two-factor-recovery-codes', (route) =>
|
|
route.fulfill({ status: 423, json: {} }),
|
|
);
|
|
await button('View recovery codes').click();
|
|
await page
|
|
.getByText(
|
|
'Your session or password confirmation has expired. Reload this page and confirm your password to continue.',
|
|
{ exact: true },
|
|
)
|
|
.waitFor();
|
|
await page.unroute('**/user/two-factor-recovery-codes');
|
|
await button('Try again').click();
|
|
await page.getByText('old-code-0001', { exact: true }).waitFor();
|
|
await button('Disable 2FA').click();
|
|
await dialog().getByRole('button', { name: 'Cancel', exact: true }).click();
|
|
await button('Disable 2FA').click();
|
|
await dialog()
|
|
.getByRole('button', { name: 'Disable 2FA', exact: true })
|
|
.click();
|
|
await dialog().waitFor({ state: 'hidden' });
|
|
await button('Enable 2FA').waitFor();
|
|
assert.equal(
|
|
await page.getByText('old-code-0001', { exact: true }).count(),
|
|
0,
|
|
);
|
|
await page.route('**/user/two-factor-secret-key', (route) => route.abort());
|
|
await button('Enable 2FA').click();
|
|
await page
|
|
.getByText(
|
|
'Authenticator setup could not be loaded. Please try again.',
|
|
{ exact: true },
|
|
)
|
|
.waitFor();
|
|
assert.equal(await button('Continue').count(), 0);
|
|
await page.unroute('**/user/two-factor-secret-key');
|
|
await button('Try again').click();
|
|
await page.getByLabel('Manual setup key').waitFor();
|
|
await page.locator('[data-slot="dialog-close"]').click();
|
|
await dialog().waitFor({ state: 'hidden' });
|
|
await page.getByLabel('Current password', { exact: true }).fill('wrong');
|
|
await page.getByLabel('New password', { exact: true }).fill('new-password');
|
|
await page
|
|
.getByLabel('Confirm password', { exact: true })
|
|
.fill('new-password');
|
|
await result('validation');
|
|
await button('Save password').click();
|
|
await page
|
|
.getByText('The password is incorrect.', { exact: true })
|
|
.waitFor();
|
|
assert.equal(
|
|
await page.getByLabel('Current password', { exact: true }).inputValue(),
|
|
'',
|
|
);
|
|
assert.equal(
|
|
await page.getByLabel('New password', { exact: true }).inputValue(),
|
|
'',
|
|
);
|
|
assert.equal(
|
|
await page
|
|
.getByLabel('Current password', { exact: true })
|
|
.getAttribute('aria-invalid'),
|
|
'true',
|
|
);
|
|
assert.doesNotMatch(
|
|
await page.evaluate(() => JSON.stringify(history.state)),
|
|
/MANUALSETUPTESTKEY|123456|new-password/,
|
|
);
|
|
|
|
await result('success');
|
|
await page.goto(`${base}/settings/applications`);
|
|
assert.equal(await button('Copy key').count(), 0);
|
|
await page.getByLabel('Application name').fill('My tool');
|
|
await page.route('**/settings/applications', (route) =>
|
|
route.request().method() === 'POST' ? route.abort() : route.continue(),
|
|
);
|
|
await button('Create application').click();
|
|
await page.locator('[data-form-state="network"]').waitFor();
|
|
assert.equal(
|
|
await page.getByLabel('Application name').inputValue(),
|
|
'My tool',
|
|
);
|
|
await page.unroute('**/settings/applications');
|
|
await button('Try again').click();
|
|
await page.getByLabel('API key', { exact: true }).waitFor();
|
|
assert.ok(
|
|
(await page.locator('section').first().boundingBox()).width <= 390,
|
|
'One-time key content is clipped on mobile',
|
|
);
|
|
await button('Copy key').click();
|
|
assert.equal(
|
|
await page.evaluate(() => navigator.clipboard.readText()),
|
|
'kg_app_one_time_test_key',
|
|
);
|
|
await page.reload();
|
|
assert.equal(await button('Copy key').count(), 0);
|
|
assert.equal(await page.getByLabel('API key', { exact: true }).count(), 0);
|
|
await page
|
|
.getByRole('button', { name: 'Delete Desktop tool', exact: true })
|
|
.click();
|
|
await dialog().getByRole('button', { name: 'Cancel', exact: true }).click();
|
|
await page
|
|
.getByRole('button', { name: 'Delete Desktop tool', exact: true })
|
|
.click();
|
|
await result('expired');
|
|
await dialog().getByRole('button', { name: 'Delete', exact: true }).click();
|
|
await dialog()
|
|
.getByText('Your session has expired', { exact: true })
|
|
.waitFor();
|
|
assert.equal(
|
|
await page.locator('button[aria-label="Delete Desktop tool"]').count(),
|
|
1,
|
|
);
|
|
await dialog().getByRole('button', { name: 'Cancel', exact: true }).click();
|
|
await result('success');
|
|
await page
|
|
.getByRole('button', { name: 'Delete Desktop tool', exact: true })
|
|
.click();
|
|
await dialog().getByRole('button', { name: 'Delete', exact: true }).click();
|
|
await page
|
|
.getByText('No applications have been created yet.', { exact: true })
|
|
.waitFor();
|
|
assert.deepEqual(errors, []);
|
|
console.log(
|
|
'Security browser behavior checks passed: 2FA setup, confirmation, cancellation, recovery hiding and regeneration, late responses, errors, password reset, one-time key copy, deletion, keyboard, mobile and both themes.',
|
|
);
|
|
} finally {
|
|
await browser.close();
|
|
}
|