services/voicemail.ts rescans a mailbox on Asterisk's MessageWaiting event (and every mailbox on AMI connect) instead of watching files, upserts voicemail_messages by msg_id, marks vanished ones deleted, sends voicemail.new to the owner only, and emails the wav. Delete-after-email uses AMI VoicemailRemove, and only when the audio was attached. Asterisk 24 has VoicemailMove/VoicemailRemove, so 04 §5.6 drops the renumbering plan. New VOICEMAIL_DIR setting; e2e m4-t2 checks the email arrives within 10 s with a playable attachment (SMTP sink shared with m3-t4). Task: M4-T2
159 lines
6.6 KiB
JavaScript
159 lines
6.6 KiB
JavaScript
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
// M3-T4 in a browser: dashboard warnings, Settings → Email and Emergency, and the test button
|
|
// (email arrives, the notify phone rings, the red banner shows). Nothing dials 911.
|
|
// Run after m3-t1..t3 on the m2-live setup. Needs the api started with SMTP reachable at
|
|
// 127.0.0.1:2525, which this script provides (a tiny SMTP sink).
|
|
// Usage: node scripts/e2e/m3-t4.mjs <baseUrl> <adminEmail> <adminPassword> <shotsDir>
|
|
import assert from 'node:assert/strict';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { launch } from './cdp.mjs';
|
|
import { smtpSink } from './smtp-sink.mjs';
|
|
|
|
const NET = 'sp-live';
|
|
const AST = 'sp-live-ast';
|
|
const PHONE = 'sp-live-phone101';
|
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
const docker = (...a) => {
|
|
const r = spawnSync('docker', a, { encoding: 'utf8' });
|
|
return { ok: r.status === 0, out: `${r.stdout ?? ''}${r.stderr ?? ''}` };
|
|
};
|
|
const cli = (cmd) => docker('exec', AST, 'asterisk', '-rx', cmd).out;
|
|
const [base, email, pw, shotsDir] = process.argv.slice(2);
|
|
|
|
const { mails, server: smtp } = await smtpSink();
|
|
|
|
const login = await fetch(`${base}/api/auth/login`, {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json', 'x-requested-with': 'selfphoneng' },
|
|
body: JSON.stringify({ email, password: pw }),
|
|
});
|
|
const cookie = login.headers.get('set-cookie').split(';')[0];
|
|
const get = async (path) => (await fetch(`${base}${path}`, { headers: { cookie } })).json();
|
|
|
|
// A softphone registered as 101 (Dan, from m3-t2), to be the "ring these phones" target.
|
|
const dan = (await get('/api/extensions')).items.find((e) => e.number === '101');
|
|
const creds = await get(`/api/extensions/${dan.id}/sip-credentials`);
|
|
const dir = mkdtempSync(join(tmpdir(), 'sp-phone101-'));
|
|
mkdirSync(join(dir, 'p'));
|
|
writeFileSync(
|
|
join(dir, 'p', 'config'),
|
|
'sip_listen 0.0.0.0:5070\nmodule_path /usr/lib/baresip/modules\nmodule g711.so\nmodule ausine.so\nmodule aufile.so\nmodule menu.so\nmodule account.so\naudio_source ausine,440\nausrc_srate 48000\nauplay_srate 48000\naudio_player aufile,/phone/p/heard.wav\naudio_alert aufile,/dev/null\n',
|
|
);
|
|
writeFileSync(
|
|
join(dir, 'p', 'accounts'),
|
|
`<sip:101@${AST};transport=udp>;auth_pass=${creds.password};answermode=auto;audio_codecs=PCMU\n`,
|
|
);
|
|
docker('rm', '-f', PHONE);
|
|
assert.ok(
|
|
docker(
|
|
'run',
|
|
'-d',
|
|
'--rm',
|
|
'--name',
|
|
PHONE,
|
|
'--network',
|
|
NET,
|
|
'-v',
|
|
`${dir}:/phone`,
|
|
'debian:trixie-slim',
|
|
'sh',
|
|
'-c',
|
|
'apt-get update -qq >/dev/null && apt-get install -y -qq --no-install-recommends baresip-core >/dev/null 2>&1 && baresip -f /phone/p > /phone/p/log 2>&1',
|
|
).ok,
|
|
);
|
|
|
|
const p = await launch({ shotsDir });
|
|
try {
|
|
await p.goto(`${base}/login`);
|
|
await p.waitFor(`!!document.querySelector('input[type=email]')`);
|
|
await p.fill('Email', email);
|
|
await p.fill('Password', pw);
|
|
await p.click('Sign in');
|
|
await p.waitFor(`location.pathname === '/admin'`);
|
|
await p.waitFor(
|
|
`document.body.innerText.includes('No emergency (E911) caller ID is set') && document.body.innerText.includes('Email is not set up')`,
|
|
'dashboard warnings',
|
|
);
|
|
await p.shot('m3-t4-01-warnings');
|
|
console.log('dashboard: E911 caller ID and email warnings');
|
|
|
|
// Email
|
|
await p.click('Settings');
|
|
await p.waitFor(`document.body.innerText.includes('SMTP server')`);
|
|
await p.fill('SMTP server', '127.0.0.1');
|
|
await p.fill('Port', '2525');
|
|
await p.fill('From', 'Phone system <pbx@example.com>');
|
|
await p.click('Save');
|
|
await p.waitFor(`document.body.innerText.includes('Saved.')`, 'smtp saved');
|
|
await p.fill('Send a test email to', 'admin@example.com');
|
|
await p.click('Send test');
|
|
await p.waitFor(`document.body.innerText.includes('Sent. Check the inbox')`, 'test email', 15000);
|
|
assert.ok(
|
|
mails.some((m) => m.includes('Subject: SelfPhoneNG test email')),
|
|
'test email arrived',
|
|
);
|
|
console.log('email: test message received by the SMTP sink');
|
|
|
|
// Emergency
|
|
await p.click('Emergency');
|
|
await p.waitFor(`document.body.innerText.includes('Default emergency caller ID')`);
|
|
const did = (await get('/api/dids')).items[0];
|
|
await p.waitFor(`!!document.querySelector('option[value="${did.id}"]')`, 'phone numbers loaded');
|
|
await p.fill('Default emergency caller ID', did.id);
|
|
await p.fill('Email these people', 'security@example.com');
|
|
await p.evaluate(
|
|
`[...document.querySelectorAll('label')].find(l => l.textContent.includes('101 Dan Doe')).querySelector('input').click(), true`,
|
|
);
|
|
await p.click('Save');
|
|
await p.waitFor(`document.body.innerText.includes('Saved.')`, 'emergency saved');
|
|
|
|
for (let i = 0; i < 120 && !/101\/sip:.*Avail/.test(cli('pjsip show contacts')); i++)
|
|
await sleep(1000);
|
|
assert.match(cli('pjsip show contacts'), /101\/sip:.*Avail/, 'phone 101 registered');
|
|
const before = mails.length;
|
|
await p.click('Test notifications');
|
|
await p.waitFor(`document.body.innerText.includes('Rang 101: ok')`, 'test result', 20000);
|
|
await p.waitFor(
|
|
`document.body.innerText.includes('TEST of the 911 alerts (Administrator)')`,
|
|
'red banner',
|
|
);
|
|
let rang = '';
|
|
for (let i = 0; i < 30 && !/sp-emergency-alert/.test(rang); i++) {
|
|
await sleep(300);
|
|
rang = cli('core show channels concise');
|
|
}
|
|
assert.match(rang, /^PJSIP\/101-.*sp-emergency-alert/m, '101 answered the alert ring-out');
|
|
for (let i = 0; i < 30 && mails.length === before; i++) await sleep(200);
|
|
const alert = mails.slice(before).find((m) => /Subject: TEST EMERGENCY: 911 dialed/.test(m));
|
|
assert.ok(alert, 'alert email arrived');
|
|
assert.ok(
|
|
!/to=911|Dial\(PJSIP\/911/.test(cli('core show channels concise')),
|
|
'nothing dialed 911',
|
|
);
|
|
await p.shot('m3-t4-02-test-notifications');
|
|
console.log('test button: email + ring-out on 101 + red banner, nothing dialed');
|
|
|
|
// The dashboard now asks to confirm the E911 address of that number.
|
|
await p.click('Dismiss');
|
|
await p.click('Dashboard');
|
|
await p.waitFor(
|
|
`document.body.innerText.includes('Confirm the emergency caller ID number has an E911 address')`,
|
|
'address warning',
|
|
);
|
|
assert.ok(!(await p.evaluate('document.body.innerText')).includes('Email is not set up'));
|
|
await p.shot('m3-t4-03-dashboard');
|
|
assert.deepEqual(p.consoleErrors, [], 'no console errors');
|
|
console.log('M3-T4 (UI): PASS');
|
|
} catch (e) {
|
|
await p.shot('m3-t4-FAILED').catch(() => {});
|
|
console.error('FAIL:', e.message, '\nconsole:', p.consoleErrors);
|
|
process.exitCode = 1;
|
|
} finally {
|
|
await p.close();
|
|
docker('rm', '-f', PHONE);
|
|
smtp.close();
|
|
}
|