integrations/voipms/client.ts: voipmsApi(url, creds, method, params) per 06 §5.1 (GET rest.php, 15 s timeout, status "success" or an error code). The API password is only in the query string: never logged, never in an error. Known codes get a message for people (invalid_credentials, ip_not_enabled, ...). getServers and getDids parse forgivingly; no_did is an empty list. Routes: GET /api/trunks/:id/voipms-servers, GET and POST /api/trunks/:id/import-dids. Import adds only numbers voip.ms lists and that aren't added yet, label from the description, destination hangup. 400 without API credentials, 502 provider_error with voip.ms's reason. VOIPMS_API_URL overrides the endpoint for tests. Web: the trunk form's server list comes from voip.ms once the trunk has API access, falling back to the built-in list with the reason. "Import numbers from voip.ms" sits in the trunk form's API section and the setup wizard's numbers step. Saving a new API password asks voip.ms again (found in e2e: the old error stayed). The response shapes are from the 06 spec, not a real account: voip.ms's API docs need a login. Marked TODO(spec) and (verify) in 06. Tests use a fake rest.php; e2e m9-t1 too (trunk created off, so nothing registers to the real voip.ms). Task: M9-T1
154 lines
6.2 KiB
JavaScript
154 lines
6.2 KiB
JavaScript
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
// M9-T1 in a browser, against a fake voip.ms API on 127.0.0.1:18090 (start the api with
|
|
// VOIPMS_API_URL=http://127.0.0.1:18090/api/v1/rest.php). A trunk with API access gets its POP
|
|
// list from voip.ms, a wrong API password shows voip.ms's reason, and "Import numbers from
|
|
// voip.ms" adds the picked numbers, not routed yet. The API password never reaches the api log.
|
|
// Runs after m1-t4.mjs.
|
|
// Usage: node scripts/e2e/m9-t1.mjs <baseUrl> <adminEmail> <adminPassword> <shotsDir> [<apiLog>]
|
|
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
import { createServer } from 'node:http';
|
|
import { launch } from './cdp.mjs';
|
|
|
|
const [base, email, pw, shotsDir, apiLog] = process.argv.slice(2);
|
|
const API_PW = 'Vms-api-pw-7731';
|
|
|
|
// The fake rest.php.
|
|
const seen = [];
|
|
const fake = createServer((req, res) => {
|
|
const q = new URL(req.url, 'http://x').searchParams;
|
|
seen.push(q.get('method'));
|
|
const ok = q.get('api_username') === 'owner@example.com' && q.get('api_password') === API_PW;
|
|
const answers = {
|
|
getServersInfo: {
|
|
status: 'success',
|
|
servers: [
|
|
{ server_name: 'Chicago 4', server_hostname: 'chicago4.voip.ms' },
|
|
{ server_name: 'Dallas 2', server_hostname: 'dallas2.voip.ms' },
|
|
],
|
|
},
|
|
getDIDsInfo: {
|
|
status: 'success',
|
|
dids: [
|
|
{ did: '3125550144', description: 'Front desk', sms_enabled: '1' },
|
|
{ did: '3125550145', description: 'Fax', sms_enabled: '0' },
|
|
],
|
|
},
|
|
};
|
|
res.setHeader('content-type', 'application/json');
|
|
res.end(JSON.stringify(ok ? answers[q.get('method')] : { status: 'invalid_credentials' }));
|
|
});
|
|
await new Promise((r) => fake.listen(18090, '127.0.0.1', r));
|
|
|
|
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
|
|
.getSetCookie()
|
|
.map((c) => c.split(';')[0])
|
|
.join('; ');
|
|
const made = await (
|
|
await fetch(`${base}/api/trunks`, {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json', 'x-requested-with': 'selfphoneng', cookie },
|
|
body: JSON.stringify({
|
|
name: 'voip.ms import',
|
|
enabled: false, // never register to the real voip.ms from a test
|
|
serverHost: 'chicago1.voip.ms',
|
|
username: '123456_m9import',
|
|
password: 'sub-pw',
|
|
voipmsApiUsername: 'owner@example.com',
|
|
voipmsApiPassword: 'wrong-pw-1',
|
|
}),
|
|
})
|
|
).json();
|
|
assert.ok(made.id, `trunk: ${JSON.stringify(made)}`);
|
|
|
|
const p = await launch({ shotsDir });
|
|
const openApi = `(() => { const d = [...document.querySelectorAll('dialog[open] details')].find(d => d.innerText.includes('voip.ms API')); d.open = true; return true; })()`;
|
|
const datalist = `[...document.querySelectorAll('#voipms-servers option')].map(o => o.value).join()`;
|
|
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'`);
|
|
|
|
// Wrong API password: the built-in server list and voip.ms's reason.
|
|
await p.click('Trunks');
|
|
await p.waitFor(`document.body.innerText.includes('123456_m9import@')`, 'trunk listed');
|
|
await p.evaluate(`(() => {
|
|
const li = [...document.querySelectorAll('li')].find(l => l.innerText.includes('123456_m9import@'));
|
|
[...li.querySelectorAll('button')].find(b => b.textContent === 'Edit').click();
|
|
return true;
|
|
})()`);
|
|
await p.waitFor(`!!document.querySelector('dialog[open]')`);
|
|
await p.waitFor(
|
|
`document.querySelector('dialog[open]').innerText.includes('rejected the API username or password')`,
|
|
'server list error shown',
|
|
);
|
|
assert.match(await p.evaluate(datalist), /chicago1\.voip\.ms/);
|
|
console.log('wrong API password: the form says voip.ms rejected it, built-in server list');
|
|
|
|
// The right one: the account's servers.
|
|
await p.evaluate(openApi);
|
|
await p.fill('API password', API_PW, 'dialog[open]');
|
|
await p.click('Save', 'dialog[open]');
|
|
await p.waitFor(`${datalist} === 'chicago4.voip.ms,dallas2.voip.ms'`, 'servers from voip.ms');
|
|
console.log('API password saved: the server list comes from voip.ms (chicago4, dallas2)');
|
|
|
|
// Import one of the two numbers.
|
|
await p.evaluate(openApi);
|
|
await p.click('Import numbers from voip.ms', 'dialog[open]');
|
|
await p.waitFor(
|
|
`document.querySelector('dialog[open]').innerText.includes('Front desk')`,
|
|
'numbers listed',
|
|
);
|
|
await p.evaluate(`(() => {
|
|
const l = [...document.querySelectorAll('dialog[open] label')].find(l => l.textContent.includes('Front desk'));
|
|
l.querySelector('input').click();
|
|
return true;
|
|
})()`);
|
|
await p.click('Import 1 number', 'dialog[open]');
|
|
await p.waitFor(
|
|
`document.querySelector('dialog[open]').innerText.includes('Added 1.')`,
|
|
'imported',
|
|
);
|
|
await p.waitFor(
|
|
`document.querySelector('dialog[open]').innerText.includes('(added)')`,
|
|
'marked added',
|
|
);
|
|
await p.evaluate(
|
|
`document.querySelector('dialog[open] fieldset legend')?.scrollIntoView({ block: 'start' }) ?? true`,
|
|
);
|
|
await p.shot('m9-t1-01-import');
|
|
console.log('imported (312) 555-0144 "Front desk"; the list marks it added');
|
|
|
|
await p.goto(`${base}/admin/numbers`);
|
|
await p.waitFor(`document.body.innerText.includes('(312) 555-0144')`, 'on Phone numbers');
|
|
const text = await p.text();
|
|
assert.match(text, /Front desk/);
|
|
assert.doesNotMatch(text, /555-0145/);
|
|
await p.shot('m9-t1-02-numbers');
|
|
console.log('Phone numbers lists it with its label; the other number was not imported');
|
|
|
|
assert.ok(seen.includes('getServersInfo') && seen.includes('getDIDsInfo'));
|
|
if (apiLog) {
|
|
const log = readFileSync(apiLog, 'utf8');
|
|
assert.ok(!log.includes(API_PW) && !log.includes('wrong-pw-1'), 'API password in the log');
|
|
console.log('the api log never contains the API password');
|
|
}
|
|
console.log('M9-T1 (voip.ms API, import): PASS');
|
|
} catch (e) {
|
|
await p.shot('m9-t1-fail').catch(() => {});
|
|
console.log(`FAIL: ${e.message}`);
|
|
console.log(`browser errors: ${JSON.stringify(p.consoleErrors)}`);
|
|
process.exitCode = 1;
|
|
} finally {
|
|
await p.close();
|
|
fake.close();
|
|
}
|