/setup walks a fresh install to working calls: password (done by the forced change), company, network (detect public IP, LAN IP from /api/system/status, router checklist; restarts telephony because the transports changed), email (skippable), voip.ms trunk (Next unlocks on Registered), phone numbers, users (extension auto-numbered, temporary passwords shown once), main number routing, emergency (default E911 caller ID required once a trunk exists), done. Steps 7-8 ship now because the acceptance needs a user and a routed number; their M6 choices (everyone, IVR) come later. Admins land on /setup until it is finished or skipped. api: /api/setup/status and /complete, POST /api/settings/network/detect-public-ip (ipify, then ifconfig.me), GET /api/system/status (M3 subset), POST /api/system/asterisk/restart (an AMI disconnect during a restart counts as success). restartRequired clears on AMI connect once Asterisk's uptime shows it started after the change. The first phone number becomes the default outbound caller ID when none is set (found by the acceptance run: calls otherwise left with the provider's caller ID). Verified: scripts/e2e/m3-t5.mjs on a fresh database against the fake voip.ms: wizard end to end, then an inbound call to the DID rings the new user's softphone and an outbound call leaves as 13125550000 with the DID in Remote-Party-ID. m1-t4 now passes through "Skip setup". Task: M3-T5
103 lines
3.8 KiB
JavaScript
103 lines
3.8 KiB
JavaScript
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
// M1-T4 acceptance: admin logs in, is forced to change the password, then creates/edits/deletes a user.
|
|
// Usage: node m1-t4.mjs <baseUrl> <adminEmail> <adminInitialPassword> <shotsDir>
|
|
import assert from 'node:assert/strict';
|
|
import { launch } from './cdp.mjs';
|
|
|
|
const [base, email, initialPw, shotsDir] = process.argv.slice(2);
|
|
const NEW_PW = 'correct-horse-battery-staple';
|
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
|
const p = await launch({ shotsDir });
|
|
try {
|
|
await p.goto(`${base}/admin/users`);
|
|
await p.waitFor(`location.pathname === '/login'`, 'redirect to /login');
|
|
await p.shot('01-login');
|
|
|
|
await p.fill('Email', email);
|
|
await p.fill('Password', initialPw);
|
|
await p.click('Sign in');
|
|
await p.waitFor(`location.pathname === '/change-password'`, 'forced password change');
|
|
await p.shot('02-change-password');
|
|
|
|
await p.fill('Current password', initialPw);
|
|
await p.fill('New password', 'short');
|
|
await p.fill('Repeat new password', 'short');
|
|
await p.click('Change password');
|
|
await p.waitFor(
|
|
`document.body.innerText.includes('at least 10 characters')`,
|
|
'min length error shown',
|
|
);
|
|
|
|
await p.fill('New password', NEW_PW);
|
|
await p.fill('Repeat new password', NEW_PW);
|
|
await p.click('Change password');
|
|
// A fresh install opens the setup wizard (05 §2.1); this flow skips it (m3-t5.mjs walks it).
|
|
await p.waitFor(`location.pathname === '/setup'`, 'setup wizard after change');
|
|
await p.click('Skip setup');
|
|
await p.waitFor(`location.pathname === '/admin'`, 'admin dashboard after skipping setup');
|
|
assert.match(await p.text(), /Extensions online/);
|
|
|
|
await p.click('Users & Extensions');
|
|
await p.waitFor(`document.body.innerText.includes('Administrator')`, 'users table');
|
|
await p.shot('03-users');
|
|
|
|
// create
|
|
await p.click('New user');
|
|
await p.waitFor(`!!document.querySelector('dialog[open]')`, 'drawer open');
|
|
await p.fill('Name', 'Ann Lee');
|
|
await p.fill('Email', 'ann@example.com');
|
|
await p.shot('04-new-user');
|
|
await p.click('Create user');
|
|
await p.waitFor(`document.body.innerText.includes('Temporary password')`, 'temp password dialog');
|
|
const temp = await p.evaluate(`document.querySelector('dialog[open] code').textContent`);
|
|
assert.match(temp, /^[A-Za-z0-9]{16}$/);
|
|
await p.shot('05-temp-password');
|
|
await p.click('Done');
|
|
await p.waitFor(`document.body.innerText.includes('ann@example.com')`, 'new user in table');
|
|
|
|
// edit
|
|
await p.click('Edit Ann Lee');
|
|
await p.waitFor(`!!document.querySelector('dialog[open]')`, 'edit drawer');
|
|
await p.fill('Name', 'Ann B. Lee');
|
|
await p.fill('Role', 'admin');
|
|
await p.click('Save');
|
|
await p.waitFor(`document.body.innerText.includes('Ann B. Lee')`, 'edited name in table');
|
|
|
|
// delete
|
|
await p.click('Delete Ann B. Lee');
|
|
await p.waitFor(
|
|
`document.body.innerText.includes('Delete Ann B. Lee?') || !!document.querySelector('dialog[open]')`,
|
|
'confirm',
|
|
);
|
|
await p.shot('06-confirm-delete');
|
|
await p.click('Delete');
|
|
await p.waitFor(`!document.body.innerText.includes('ann@example.com')`, 'user removed');
|
|
await p.shot('07-after-delete');
|
|
|
|
// log out and back in with the new password
|
|
await p.click('Sign out');
|
|
await p.waitFor(`location.pathname === '/login'`, 'signed out');
|
|
await p.fill('Email', email);
|
|
await p.fill('Password', NEW_PW);
|
|
await p.click('Sign in');
|
|
await p.waitFor(`location.pathname === '/admin'`, 'signed back in');
|
|
await sleep(200);
|
|
|
|
assert.deepEqual(p.consoleErrors, [], 'no console errors');
|
|
console.log('M1-T4 browser flow: PASS');
|
|
} catch (e) {
|
|
await p.shot('FAILED').catch(() => {});
|
|
console.error(
|
|
'FAIL:',
|
|
e.message,
|
|
'\npath:',
|
|
await p.path().catch(() => '?'),
|
|
'\nconsole:',
|
|
p.consoleErrors,
|
|
);
|
|
process.exitCode = 1;
|
|
} finally {
|
|
await p.close();
|
|
}
|