numbers: any free internal number of 1 to 6 digits; ranges only suggest

Owner wanted the first user on 1. Numbering ranges now choose the Next free suggestion
(Settings → Numbering), the setup wizard takes a number per person, and a one-digit number
can't also be a menu key of a digital receptionist with direct dialing. Dialplan patterns that
take an internal number match one digit (_X!); golden case short-numbers proves it in Asterisk.
This commit is contained in:
iamdoubz committed 2026-10-21 13:17:25 -05:00
1 parent 4143f6d064
commit aa6681da03
42 files changed
+932 -137

No files matched your search

+8
View File
@@ -141,6 +141,14 @@ async function check(name) {
if (!shownQ.startsWith(`${q} has`) || listed !== members)
problems.push(`queue ${q}: expected ${members} members, got: ${shownQ.trim()}`);
}
// A case can list [number, context] pairs Asterisk must match (short-numbers: one-digit
// numbers, which a pattern like _X. needs two digits for).
const snap = JSON.parse(readFileSync(join(GOLDEN, name, 'snapshot.json'), 'utf8'));
for (const [exten, ctx] of snap.asteriskMatches ?? []) {
const d = cli(`dialplan show ${exten}@${ctx}`).out;
if (!/-= \d+ extensions? \(/.test(d) || /There is no existence/.test(d))
problems.push(`${exten} doesn't match in ${ctx}`);
}
// Asterisk colors its log: strip the escape codes before looking for ERROR[.
// eslint-disable-next-line no-control-regex -- matching ANSI escapes is the point
const log = docker('logs', NAME).out.replace(/\x1b\[[0-9;]*m/g, '');
+1 -1
View File
@@ -62,7 +62,7 @@ describe.skipIf(!hasTestDb)('/api/conferences', () => {
it('checks the number and the PINs', async () => {
const bad = (body: object) =>
req('POST', '/api/conferences', { number: '801', name: 'X', ...body });
expect((await bad({ number: '700' })).statusCode).toBe(400); // parking, not 800–849
expect((await bad({ number: '700' })).statusCode).toBe(409); // the parking lot
expect((await bad({ number: '800' })).statusCode).toBe(409);
expect((await bad({ pin: '12' })).statusCode).toBe(400);
expect((await bad({ pin: '12a4' })).statusCode).toBe(400);
+6 -3
View File
@@ -72,9 +72,12 @@ describe.skipIf(!hasTestDb)('extensions API', () => {
const make = (number: string, extra = {}) =>
req('POST', '/api/extensions', { number, name: 'X', ...extra });
expect((await make('211')).json().error.message).toMatch(/reserved/);
expect((await make('600')).json().error.message).toMatch(/from 100 to 499/);
expect((await make('12')).statusCode).toBe(400);
expect((await make('710')).statusCode).toBe(400); // outside the users range anyway
expect((await make('1234567')).statusCode).toBe(400); // 1 to 6 digits
expect((await make('710')).json().error.message).toMatch(/parking/);
// Any free number, in or out of the users range (owner, 2026-10-21): "1" for the first user.
const one = await make('1');
expect(one.statusCode, one.body).toBe(201);
expect((await req('DELETE', `/api/extensions/${one.json().id}`)).statusCode).toBe(200);
const dup = await make('101');
expect(dup.statusCode).toBe(409);
expect(dup.json().error.message).toMatch(/extension "Dan Doe"/);
+18 -1
View File
@@ -81,7 +81,24 @@ describe.skipIf(!hasTestDb)('/api/ivrs', () => {
const post = (body: object) => req('POST', '/api/ivrs', { ...base(), number: '601', ...body });
const missing = '00000000-0000-4000-8000-000000000999';
expect((await post({ number: '600' })).statusCode).toBe(409);
expect((await post({ number: '500' })).statusCode).toBe(400); // ring-group range
expect((await post({ number: '1234567' })).statusCode).toBe(400);
// A one-digit number and a menu key (direct dialing on) can't be the same, either way round.
const one = await req('POST', '/api/extensions', { number: '1', name: 'X' });
expect(one.statusCode).toBe(409);
expect(one.json().error.message).toMatch(/menu key of the digital receptionist "Main menu"/);
const four = await req('POST', '/api/extensions', { number: '4', name: 'Four' });
expect(four.statusCode, four.body).toBe(201);
const clash = await post({ options: [{ digit: '4', destination: { type: 'hangup' } }] });
expect(clash.statusCode).toBe(409);
expect(clash.json().error.message).toMatch(/Key 4 is also the number of extension "Four"/);
const noDial = await post({
allowDirectDial: false,
number: '602',
options: [{ digit: '4', destination: { type: 'hangup' } }],
});
expect(noDial.statusCode).toBe(201);
await req('DELETE', `/api/ivrs/${noDial.json().id}`);
await req('DELETE', `/api/extensions/${four.json().id}`);
expect((await post({ greetingSoundId: missing })).statusCode).toBe(400);
const twice = await post({
options: [
+1 -1
View File
@@ -53,7 +53,7 @@ describe.skipIf(!hasTestDb)('/api/paging-groups', () => {
it('checks the number and the members', async () => {
const bad = (body: object) =>
req('POST', '/api/paging-groups', { number: '851', name: 'X', ...body });
expect((await bad({ number: '800' })).statusCode).toBe(400); // conference range
expect((await bad({ number: '1234567' })).statusCode).toBe(400); // 1 to 6 digits
expect((await bad({ number: '850' })).statusCode).toBe(409);
expect((await bad({ memberIds: [ext['101'], ext['101']] })).statusCode).toBe(400);
expect((await bad({ memberIds: ['00000000-0000-4000-8000-000000000000'] })).statusCode).toBe(
+1 -1
View File
@@ -99,7 +99,7 @@ describe.skipIf(!hasTestDb)('/api/queues', () => {
timeoutDestination: { type: 'hangup' },
...body,
});
expect((await bad({ number: '500' })).statusCode).toBe(400); // outside 550–599
expect((await bad({ number: '1234567' })).statusCode).toBe(400); // 1 to 6 digits
expect((await bad({ number: '550' })).statusCode).toBe(409);
const twice = { extensionId: ext['101'], penalty: 0 };
expect((await bad({ members: [twice, twice] })).statusCode).toBe(400);
+1 -1
View File
@@ -64,7 +64,7 @@ describe.skipIf(!hasTestDb)('/api/ring-groups', () => {
noAnswerDestination: { type: 'hangup' },
...body,
});
expect((await bad({ number: '101' })).statusCode).toBe(400); // outside 500–549
expect((await bad({ number: '101' })).statusCode).toBe(409); // extension 101 has it
expect((await bad({ number: '500' })).statusCode).toBe(409);
expect((await bad({ memberIds: [ext['101'], ext['101']] })).statusCode).toBe(400);
expect((await bad({ memberIds: ['00000000-0000-4000-8000-000000000999'] })).statusCode).toBe(