generator: PJSIP transports, extension endpoints and rtp config
Golden cases empty, one-user, webrtc-disabled and hostile-names; each also renders identically from shuffled input. pnpm test:asterisk boots the asterisk image with every golden case and fails on a missing endpoint or context or any ERROR log line (checked against a deliberately broken case). On 24.0.0-rc2: 101 and 101-web load with every set_var. Task: M2-T3
This commit is contained in:
1 parent
a5de247948
commit
06ac99b9f4
44 files changed
+1040
-12
No files matched your search
@@ -0,0 +1 @@
|
||||
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
|
||||
@@ -0,0 +1 @@
|
||||
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
|
||||
@@ -0,0 +1 @@
|
||||
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
|
||||
@@ -0,0 +1 @@
|
||||
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
|
||||
@@ -0,0 +1,27 @@
|
||||
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
|
||||
[global]
|
||||
type = global
|
||||
user_agent = SelfPhoneNG
|
||||
endpoint_identifier_order = ip,username,anonymous
|
||||
keep_alive_interval = 90
|
||||
max_initial_qualify_time = 4
|
||||
|
||||
[system]
|
||||
type = system
|
||||
timer_t1 = 500
|
||||
timer_b = 32000
|
||||
|
||||
[transport-udp]
|
||||
type = transport
|
||||
protocol = udp
|
||||
bind = 0.0.0.0:5060
|
||||
|
||||
[transport-tcp]
|
||||
type = transport
|
||||
protocol = tcp
|
||||
bind = 0.0.0.0:5060
|
||||
|
||||
[transport-ws]
|
||||
type = transport
|
||||
protocol = ws
|
||||
bind = 0.0.0.0
|
||||
@@ -0,0 +1 @@
|
||||
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
|
||||
@@ -0,0 +1,2 @@
|
||||
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
|
||||
icesupport = yes
|
||||
@@ -0,0 +1 @@
|
||||
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1 @@
|
||||
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
|
||||
+1
@@ -0,0 +1 @@
|
||||
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
|
||||
@@ -0,0 +1 @@
|
||||
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
|
||||
@@ -0,0 +1 @@
|
||||
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
|
||||
@@ -0,0 +1,237 @@
|
||||
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
|
||||
[global]
|
||||
type = global
|
||||
user_agent = SelfPhoneNG
|
||||
endpoint_identifier_order = ip,username,anonymous
|
||||
keep_alive_interval = 90
|
||||
max_initial_qualify_time = 4
|
||||
|
||||
[system]
|
||||
type = system
|
||||
timer_t1 = 500
|
||||
timer_b = 32000
|
||||
|
||||
[transport-udp]
|
||||
type = transport
|
||||
protocol = udp
|
||||
bind = 0.0.0.0:5060
|
||||
|
||||
[transport-tcp]
|
||||
type = transport
|
||||
protocol = tcp
|
||||
bind = 0.0.0.0:5060
|
||||
|
||||
[transport-ws]
|
||||
type = transport
|
||||
protocol = ws
|
||||
bind = 0.0.0.0
|
||||
|
||||
; ── extension 103 (Dan Systemrm -rf /eviltype = endpoint) ──
|
||||
[103]
|
||||
type = endpoint
|
||||
context = sp-from-ext
|
||||
disallow = all
|
||||
allow = g722,ulaw,alaw
|
||||
auth = 103-auth
|
||||
aors = 103
|
||||
direct_media = no
|
||||
rtp_symmetric = yes
|
||||
force_rport = yes
|
||||
rewrite_contact = yes
|
||||
send_pai = yes
|
||||
callerid = "Dan Systemrm -rf /eviltype = endpoint" <103>
|
||||
mailboxes = 103@default
|
||||
dtmf_mode = rfc4733
|
||||
allow_subscribe = yes
|
||||
language = en
|
||||
moh_suggest = default
|
||||
tone_zone = us
|
||||
set_var = SP_EXT=103
|
||||
set_var = SP_CLASS=2
|
||||
set_var = SP_RECMODE=never
|
||||
set_var = __SP_DIRECTION=internal
|
||||
|
||||
[103-auth]
|
||||
type = auth
|
||||
auth_type = userpass
|
||||
username = 103
|
||||
password = Hostile00000000000000001
|
||||
|
||||
[103]
|
||||
type = aor
|
||||
max_contacts = 5
|
||||
remove_existing = yes
|
||||
qualify_frequency = 30
|
||||
mailboxes = 103@default
|
||||
|
||||
[103-web]
|
||||
type = endpoint
|
||||
context = sp-from-ext
|
||||
webrtc = yes
|
||||
disallow = all
|
||||
allow = g722,ulaw
|
||||
auth = 103-web-auth
|
||||
aors = 103-web
|
||||
callerid = "Dan Systemrm -rf /eviltype = endpoint" <103>
|
||||
mailboxes = 103@default
|
||||
dtmf_mode = rfc4733
|
||||
allow_subscribe = yes
|
||||
language = en
|
||||
moh_suggest = default
|
||||
tone_zone = us
|
||||
set_var = SP_EXT=103
|
||||
set_var = SP_CLASS=2
|
||||
set_var = SP_RECMODE=never
|
||||
set_var = __SP_DIRECTION=internal
|
||||
|
||||
[103-web-auth]
|
||||
type = auth
|
||||
auth_type = userpass
|
||||
username = 103-web
|
||||
password = Hostile00000000000000001
|
||||
|
||||
[103-web]
|
||||
type = aor
|
||||
max_contacts = 3
|
||||
remove_existing = yes
|
||||
qualify_frequency = 0
|
||||
|
||||
; ── extension 104 (104) ──
|
||||
[104]
|
||||
type = endpoint
|
||||
context = sp-from-ext
|
||||
disallow = all
|
||||
allow = g722,ulaw,alaw
|
||||
auth = 104-auth
|
||||
aors = 104
|
||||
direct_media = no
|
||||
rtp_symmetric = yes
|
||||
force_rport = yes
|
||||
rewrite_contact = yes
|
||||
send_pai = yes
|
||||
callerid = "104" <104>
|
||||
mailboxes = 104@default
|
||||
dtmf_mode = rfc4733
|
||||
allow_subscribe = yes
|
||||
language = en
|
||||
moh_suggest = default
|
||||
tone_zone = us
|
||||
set_var = SP_EXT=104
|
||||
set_var = SP_CLASS=2
|
||||
set_var = SP_RECMODE=never
|
||||
set_var = __SP_DIRECTION=internal
|
||||
|
||||
[104-auth]
|
||||
type = auth
|
||||
auth_type = userpass
|
||||
username = 104
|
||||
password = Hostile00000000000000002
|
||||
|
||||
[104]
|
||||
type = aor
|
||||
max_contacts = 5
|
||||
remove_existing = yes
|
||||
qualify_frequency = 30
|
||||
mailboxes = 104@default
|
||||
|
||||
[104-web]
|
||||
type = endpoint
|
||||
context = sp-from-ext
|
||||
webrtc = yes
|
||||
disallow = all
|
||||
allow = g722,ulaw
|
||||
auth = 104-web-auth
|
||||
aors = 104-web
|
||||
callerid = "104" <104>
|
||||
mailboxes = 104@default
|
||||
dtmf_mode = rfc4733
|
||||
allow_subscribe = yes
|
||||
language = en
|
||||
moh_suggest = default
|
||||
tone_zone = us
|
||||
set_var = SP_EXT=104
|
||||
set_var = SP_CLASS=2
|
||||
set_var = SP_RECMODE=never
|
||||
set_var = __SP_DIRECTION=internal
|
||||
|
||||
[104-web-auth]
|
||||
type = auth
|
||||
auth_type = userpass
|
||||
username = 104-web
|
||||
password = Hostile00000000000000002
|
||||
|
||||
[104-web]
|
||||
type = aor
|
||||
max_contacts = 3
|
||||
remove_existing = yes
|
||||
qualify_frequency = 0
|
||||
|
||||
; ── extension 105 (Zoë Ångström 李 SHELLid evil) ──
|
||||
[105]
|
||||
type = endpoint
|
||||
context = sp-from-ext
|
||||
disallow = all
|
||||
allow = g722,ulaw,alaw
|
||||
auth = 105-auth
|
||||
aors = 105
|
||||
direct_media = no
|
||||
rtp_symmetric = yes
|
||||
force_rport = yes
|
||||
rewrite_contact = yes
|
||||
send_pai = yes
|
||||
callerid = "Zoë Ångström 李 SHELLid evil" <105>
|
||||
mailboxes = 105@default
|
||||
dtmf_mode = rfc4733
|
||||
allow_subscribe = yes
|
||||
language = en
|
||||
moh_suggest = default
|
||||
tone_zone = us
|
||||
set_var = SP_EXT=105
|
||||
set_var = SP_CLASS=2
|
||||
set_var = SP_RECMODE=never
|
||||
set_var = __SP_DIRECTION=internal
|
||||
|
||||
[105-auth]
|
||||
type = auth
|
||||
auth_type = userpass
|
||||
username = 105
|
||||
password = Hostile00000000000000003
|
||||
|
||||
[105]
|
||||
type = aor
|
||||
max_contacts = 5
|
||||
remove_existing = yes
|
||||
qualify_frequency = 30
|
||||
mailboxes = 105@default
|
||||
|
||||
[105-web]
|
||||
type = endpoint
|
||||
context = sp-from-ext
|
||||
webrtc = yes
|
||||
disallow = all
|
||||
allow = g722,ulaw
|
||||
auth = 105-web-auth
|
||||
aors = 105-web
|
||||
callerid = "Zoë Ångström 李 SHELLid evil" <105>
|
||||
mailboxes = 105@default
|
||||
dtmf_mode = rfc4733
|
||||
allow_subscribe = yes
|
||||
language = en
|
||||
moh_suggest = default
|
||||
tone_zone = us
|
||||
set_var = SP_EXT=105
|
||||
set_var = SP_CLASS=2
|
||||
set_var = SP_RECMODE=never
|
||||
set_var = __SP_DIRECTION=internal
|
||||
|
||||
[105-web-auth]
|
||||
type = auth
|
||||
auth_type = userpass
|
||||
username = 105-web
|
||||
password = Hostile00000000000000003
|
||||
|
||||
[105-web]
|
||||
type = aor
|
||||
max_contacts = 3
|
||||
remove_existing = yes
|
||||
qualify_frequency = 0
|
||||
@@ -0,0 +1 @@
|
||||
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
|
||||
@@ -0,0 +1,2 @@
|
||||
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
|
||||
icesupport = yes
|
||||
@@ -0,0 +1 @@
|
||||
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"extensions": [
|
||||
{
|
||||
"number": "103",
|
||||
"name": "Dan\"); System(rm -rf /)\n[evil]\ntype = endpoint",
|
||||
"sipPassword": "Hostile00000000000000001"
|
||||
},
|
||||
{
|
||||
"number": "104",
|
||||
"name": "();,\"'\\$<>|",
|
||||
"sipPassword": "Hostile00000000000000002"
|
||||
},
|
||||
{
|
||||
"number": "105",
|
||||
"name": "Zoë Ångström 李 ${SHELL(id)} \u202Eevil\u200B",
|
||||
"sipPassword": "Hostile00000000000000003"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
|
||||
+1
@@ -0,0 +1 @@
|
||||
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
|
||||
@@ -0,0 +1 @@
|
||||
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
|
||||
@@ -0,0 +1 @@
|
||||
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
|
||||
@@ -0,0 +1,115 @@
|
||||
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
|
||||
[global]
|
||||
type = global
|
||||
user_agent = SelfPhoneNG
|
||||
endpoint_identifier_order = ip,username,anonymous
|
||||
keep_alive_interval = 90
|
||||
max_initial_qualify_time = 4
|
||||
|
||||
[system]
|
||||
type = system
|
||||
timer_t1 = 500
|
||||
timer_b = 32000
|
||||
|
||||
[transport-udp]
|
||||
type = transport
|
||||
protocol = udp
|
||||
bind = 0.0.0.0:5060
|
||||
external_media_address = 203.0.113.10
|
||||
external_signaling_address = 203.0.113.10
|
||||
local_net = 192.168.0.0/16
|
||||
local_net = 10.0.0.0/8
|
||||
local_net = 172.16.0.0/12
|
||||
|
||||
[transport-tcp]
|
||||
type = transport
|
||||
protocol = tcp
|
||||
bind = 0.0.0.0:5060
|
||||
external_media_address = 203.0.113.10
|
||||
external_signaling_address = 203.0.113.10
|
||||
local_net = 192.168.0.0/16
|
||||
local_net = 10.0.0.0/8
|
||||
local_net = 172.16.0.0/12
|
||||
|
||||
[transport-ws]
|
||||
type = transport
|
||||
protocol = ws
|
||||
bind = 0.0.0.0
|
||||
|
||||
; ── extension 101 (Dan Doe) ──
|
||||
[101]
|
||||
type = endpoint
|
||||
context = sp-from-ext
|
||||
disallow = all
|
||||
allow = g722,ulaw,alaw
|
||||
auth = 101-auth
|
||||
aors = 101
|
||||
direct_media = no
|
||||
rtp_symmetric = yes
|
||||
force_rport = yes
|
||||
rewrite_contact = yes
|
||||
send_pai = yes
|
||||
callerid = "Dan Doe" <101>
|
||||
mailboxes = 101@default
|
||||
dtmf_mode = rfc4733
|
||||
allow_subscribe = yes
|
||||
named_call_group = sales
|
||||
named_pickup_group = sales
|
||||
language = en
|
||||
moh_suggest = default
|
||||
tone_zone = us
|
||||
set_var = SP_EXT=101
|
||||
set_var = SP_CLASS=2
|
||||
set_var = SP_OUTCID=13125551234
|
||||
set_var = SP_E911CID=13125551234
|
||||
set_var = SP_RECMODE=never
|
||||
set_var = __SP_DIRECTION=internal
|
||||
|
||||
[101-auth]
|
||||
type = auth
|
||||
auth_type = userpass
|
||||
username = 101
|
||||
password = Ab3dEf9hIj2kLm4nOp6qRs8t
|
||||
|
||||
[101]
|
||||
type = aor
|
||||
max_contacts = 5
|
||||
remove_existing = yes
|
||||
qualify_frequency = 30
|
||||
mailboxes = 101@default
|
||||
|
||||
[101-web]
|
||||
type = endpoint
|
||||
context = sp-from-ext
|
||||
webrtc = yes
|
||||
disallow = all
|
||||
allow = g722,ulaw
|
||||
auth = 101-web-auth
|
||||
aors = 101-web
|
||||
callerid = "Dan Doe" <101>
|
||||
mailboxes = 101@default
|
||||
dtmf_mode = rfc4733
|
||||
allow_subscribe = yes
|
||||
named_call_group = sales
|
||||
named_pickup_group = sales
|
||||
language = en
|
||||
moh_suggest = default
|
||||
tone_zone = us
|
||||
set_var = SP_EXT=101
|
||||
set_var = SP_CLASS=2
|
||||
set_var = SP_OUTCID=13125551234
|
||||
set_var = SP_E911CID=13125551234
|
||||
set_var = SP_RECMODE=never
|
||||
set_var = __SP_DIRECTION=internal
|
||||
|
||||
[101-web-auth]
|
||||
type = auth
|
||||
auth_type = userpass
|
||||
username = 101-web
|
||||
password = Ab3dEf9hIj2kLm4nOp6qRs8t
|
||||
|
||||
[101-web]
|
||||
type = aor
|
||||
max_contacts = 3
|
||||
remove_existing = yes
|
||||
qualify_frequency = 0
|
||||
@@ -0,0 +1 @@
|
||||
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
|
||||
@@ -0,0 +1,6 @@
|
||||
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
|
||||
icesupport = yes
|
||||
stunaddr = stun.example.org:3478
|
||||
|
||||
[ice_host_candidates]
|
||||
192.168.1.20 => 203.0.113.10
|
||||
@@ -0,0 +1 @@
|
||||
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"settings": {
|
||||
"network.publicIp": "203.0.113.10",
|
||||
"network.lanIp": "192.168.1.20",
|
||||
"network.stunServer": "stun.example.org:3478"
|
||||
},
|
||||
"extensions": [
|
||||
{
|
||||
"number": "101",
|
||||
"name": "Dan Doe",
|
||||
"sipPassword": "Ab3dEf9hIj2kLm4nOp6qRs8t",
|
||||
"pickupGroup": "sales",
|
||||
"outboundCallerId": "13125551234",
|
||||
"emergencyCallerId": "13125551234",
|
||||
"isAdmin": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
|
||||
+1
@@ -0,0 +1 @@
|
||||
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
|
||||
@@ -0,0 +1 @@
|
||||
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
|
||||
+1
@@ -0,0 +1 @@
|
||||
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
|
||||
@@ -0,0 +1,96 @@
|
||||
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
|
||||
[global]
|
||||
type = global
|
||||
user_agent = SelfPhoneNG
|
||||
endpoint_identifier_order = ip,username,anonymous
|
||||
keep_alive_interval = 90
|
||||
max_initial_qualify_time = 4
|
||||
|
||||
[system]
|
||||
type = system
|
||||
timer_t1 = 500
|
||||
timer_b = 32000
|
||||
|
||||
[transport-udp]
|
||||
type = transport
|
||||
protocol = udp
|
||||
bind = 0.0.0.0:5070
|
||||
|
||||
[transport-ws]
|
||||
type = transport
|
||||
protocol = ws
|
||||
bind = 0.0.0.0
|
||||
|
||||
; ── extension 99 (Lobby phone) ──
|
||||
[99]
|
||||
type = endpoint
|
||||
context = sp-from-ext
|
||||
disallow = all
|
||||
allow = g722,ulaw,alaw
|
||||
auth = 99-auth
|
||||
aors = 99
|
||||
direct_media = no
|
||||
rtp_symmetric = yes
|
||||
force_rport = yes
|
||||
rewrite_contact = yes
|
||||
send_pai = yes
|
||||
callerid = "Lobby phone" <99>
|
||||
mailboxes = 99@default
|
||||
dtmf_mode = rfc4733
|
||||
allow_subscribe = yes
|
||||
language = fr
|
||||
moh_suggest = default
|
||||
tone_zone = us
|
||||
set_var = SP_EXT=99
|
||||
set_var = SP_CLASS=0
|
||||
set_var = SP_RECMODE=never
|
||||
set_var = __SP_DIRECTION=internal
|
||||
|
||||
[99-auth]
|
||||
type = auth
|
||||
auth_type = userpass
|
||||
username = 99
|
||||
password = Lobby00000000000000000AB
|
||||
|
||||
[99]
|
||||
type = aor
|
||||
max_contacts = 5
|
||||
remove_existing = yes
|
||||
qualify_frequency = 30
|
||||
mailboxes = 99@default
|
||||
|
||||
; ── extension 102 (Ann Lee) ──
|
||||
[102]
|
||||
type = endpoint
|
||||
context = sp-from-ext
|
||||
disallow = all
|
||||
allow = g722,ulaw,alaw
|
||||
auth = 102-auth
|
||||
aors = 102
|
||||
direct_media = no
|
||||
rtp_symmetric = yes
|
||||
force_rport = yes
|
||||
rewrite_contact = yes
|
||||
send_pai = yes
|
||||
callerid = "Ann Lee" <102>
|
||||
dtmf_mode = rfc4733
|
||||
allow_subscribe = yes
|
||||
language = en
|
||||
moh_suggest = default
|
||||
tone_zone = us
|
||||
set_var = SP_EXT=102
|
||||
set_var = SP_CLASS=1
|
||||
set_var = SP_RECMODE=all
|
||||
set_var = __SP_DIRECTION=internal
|
||||
|
||||
[102-auth]
|
||||
type = auth
|
||||
auth_type = userpass
|
||||
username = 102
|
||||
password = Zz9yXx8wVv7uTt6sRr5qPp4o
|
||||
|
||||
[102]
|
||||
type = aor
|
||||
max_contacts = 2
|
||||
remove_existing = yes
|
||||
qualify_frequency = 30
|
||||
@@ -0,0 +1 @@
|
||||
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
|
||||
@@ -0,0 +1,2 @@
|
||||
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
|
||||
icesupport = yes
|
||||
@@ -0,0 +1 @@
|
||||
; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"settings": { "sip.tcpEnabled": false, "sip.udpPort": 5070 },
|
||||
"extensions": [
|
||||
{
|
||||
"number": "102",
|
||||
"name": "Ann Lee",
|
||||
"sipPassword": "Zz9yXx8wVv7uTt6sRr5qPp4o",
|
||||
"webrtcEnabled": false,
|
||||
"voicemailEnabled": false,
|
||||
"maxContacts": 2,
|
||||
"outboundClass": "local",
|
||||
"recordMode": "all"
|
||||
},
|
||||
{
|
||||
"number": "99",
|
||||
"name": "Lobby phone",
|
||||
"sipPassword": "Lobby00000000000000000AB",
|
||||
"webrtcEnabled": false,
|
||||
"outboundClass": "internal",
|
||||
"language": "fr"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Golden tests (03 §12.1–12.2): render(snapshot.json) must equal expected/*.conf byte for byte.
|
||||
// `UPDATE_GOLDEN=1 pnpm test` rewrites the expected files; a human reads the diff before committing.
|
||||
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { SettingsSchemas, type SettingKey } from '@selfphoneng/shared';
|
||||
import { GENERATED_FILES, render } from './index.ts';
|
||||
import type { Snapshot } from './load.ts';
|
||||
|
||||
const ROOT = fileURLToPath(new URL('./__golden__', import.meta.url));
|
||||
const UPDATE = process.env.UPDATE_GOLDEN === '1';
|
||||
|
||||
/** snapshot.json holds only what differs from the defaults, to keep cases readable. */
|
||||
export function loadCase(name: string): Snapshot {
|
||||
const raw = JSON.parse(readFileSync(join(ROOT, name, 'snapshot.json'), 'utf8')) as {
|
||||
today?: string;
|
||||
settings?: Record<string, unknown>;
|
||||
extensions?: Partial<Snapshot['extensions'][number]>[];
|
||||
};
|
||||
const settings = Object.fromEntries(
|
||||
(Object.keys(SettingsSchemas) as SettingKey[])
|
||||
.filter((k) => k !== 'sms.webhookToken')
|
||||
.map((k) => [k, SettingsSchemas[k].parse(raw.settings?.[k])]),
|
||||
) as Snapshot['settings'];
|
||||
return {
|
||||
today: raw.today ?? '2026-10-17',
|
||||
settings,
|
||||
extensions: (raw.extensions ?? []).map((e, i) => ({
|
||||
id: `00000000-0000-4000-8000-${String(i).padStart(12, '0')}`,
|
||||
number: '101',
|
||||
name: 'User',
|
||||
sipPassword: 'TestPassword0000000000AB',
|
||||
webrtcEnabled: true,
|
||||
maxContacts: 5,
|
||||
ringSeconds: 25,
|
||||
voicemailEnabled: true,
|
||||
outboundClass: 'national',
|
||||
outboundCallerId: null,
|
||||
emergencyCallerId: null,
|
||||
recordMode: 'never',
|
||||
pickupGroup: null,
|
||||
language: 'en',
|
||||
mohClass: 'default',
|
||||
isAdmin: false,
|
||||
...e,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
const cases = readdirSync(ROOT).filter((d) => existsSync(join(ROOT, d, 'snapshot.json')));
|
||||
|
||||
describe('golden', () => {
|
||||
it('has the required cases', () => {
|
||||
// 03 §12.1; the rest arrive with their features (M3, M6, M8).
|
||||
expect(cases).toEqual(
|
||||
expect.arrayContaining(['empty', 'one-user', 'webrtc-disabled', 'hostile-names']),
|
||||
);
|
||||
});
|
||||
|
||||
describe.each(cases)('%s', (name) => {
|
||||
const snap = loadCase(name);
|
||||
const files = render(snap);
|
||||
const dir = join(ROOT, name, 'expected');
|
||||
if (UPDATE) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
for (const f of GENERATED_FILES) writeFileSync(join(dir, f), files[f]);
|
||||
}
|
||||
it.each(GENERATED_FILES)('%s', (f) => {
|
||||
expect(files[f]).toBe(readFileSync(join(dir, f), 'utf8'));
|
||||
});
|
||||
|
||||
it('is deterministic: shuffled input order gives identical output', () => {
|
||||
const shuffled = { ...snap, extensions: [...snap.extensions].reverse() };
|
||||
expect(render(shuffled)).toEqual(files);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,9 @@ import type { Db } from '../db/client.ts';
|
||||
import { generatorRuns } from '../db/schema.ts';
|
||||
import { getSetting, setSetting } from '../services/settings.ts';
|
||||
import { loadSnapshot, type Snapshot } from './load.ts';
|
||||
import { renderPjsip, renderRtp, renderTransports } from './pjsip.ts';
|
||||
|
||||
export { renderTransports };
|
||||
|
||||
export const HEADER = '; GENERATED BY SelfPhoneNG — DO NOT EDIT. Changes will be overwritten.\n';
|
||||
|
||||
@@ -29,7 +32,10 @@ export const GENERATED_FILES = Object.keys(RELOADS) as GeneratedFileName[];
|
||||
export type RenderedFiles = Record<GeneratedFileName, string>;
|
||||
|
||||
// Each file's body; tasks fill these in (M2-T3 pjsip/rtp, M2-T4 dialplan, ...).
|
||||
const BODIES: Partial<Record<GeneratedFileName, (snap: Snapshot) => string>> = {};
|
||||
const BODIES: Partial<Record<GeneratedFileName, (snap: Snapshot) => string>> = {
|
||||
'pjsip_selfphoneng.conf': renderPjsip,
|
||||
'rtp_selfphoneng.conf': renderRtp,
|
||||
};
|
||||
|
||||
/** PURE: no I/O, no clock, no randomness. Same snapshot → byte-identical files. */
|
||||
export function render(snap: Snapshot): RenderedFiles {
|
||||
@@ -38,11 +44,6 @@ export function render(snap: Snapshot): RenderedFiles {
|
||||
) as RenderedFiles;
|
||||
}
|
||||
|
||||
/** The transport block: [global], [system] and every type=transport section (03 §3). */
|
||||
export function renderTransports(_snap: Snapshot): string {
|
||||
return ''; // M2-T3
|
||||
}
|
||||
|
||||
export type ApplyResult = {
|
||||
ok: boolean;
|
||||
error: string | null;
|
||||
|
||||
@@ -1,12 +1,36 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Everything render() needs, loaded in one go (03 §1). render() itself does no I/O.
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { alias } from 'drizzle-orm/pg-core';
|
||||
import type { Db } from '../db/client.ts';
|
||||
import { dids, extensions, users } from '../db/schema.ts';
|
||||
import { decrypt } from '../crypto/secrets.ts';
|
||||
import { getAllSettings } from '../services/settings.ts';
|
||||
|
||||
export type SnapExtension = {
|
||||
id: string;
|
||||
number: string;
|
||||
name: string;
|
||||
sipPassword: string; // decrypted
|
||||
webrtcEnabled: boolean;
|
||||
maxContacts: number;
|
||||
ringSeconds: number;
|
||||
voicemailEnabled: boolean;
|
||||
outboundClass: 'internal' | 'local' | 'national' | 'international';
|
||||
outboundCallerId: string | null; // DID number
|
||||
emergencyCallerId: string | null; // DID number
|
||||
recordMode: 'never' | 'inbound' | 'outbound' | 'all';
|
||||
pickupGroup: string | null;
|
||||
language: string;
|
||||
mohClass: string;
|
||||
isAdmin: boolean; // the extension's user is an admin (time-condition override codes)
|
||||
};
|
||||
|
||||
export type Snapshot = {
|
||||
/** "YYYY-MM-DD" in general.timezone; render() never reads the clock. */
|
||||
today: string;
|
||||
settings: Awaited<ReturnType<typeof getAllSettings>>;
|
||||
extensions: SnapExtension[];
|
||||
};
|
||||
|
||||
export function todayIn(timezone: string, now = new Date()): string {
|
||||
@@ -16,5 +40,44 @@ export function todayIn(timezone: string, now = new Date()): string {
|
||||
|
||||
export async function loadSnapshot(db: Db, now = new Date()): Promise<Snapshot> {
|
||||
const settings = await getAllSettings(db);
|
||||
return { today: todayIn(settings['general.timezone'], now), settings };
|
||||
const outDid = alias(dids, 'out_did');
|
||||
const e911Did = alias(dids, 'e911_did');
|
||||
const rows = await db
|
||||
.select({
|
||||
ext: extensions,
|
||||
role: users.role,
|
||||
outCid: outDid.number,
|
||||
e911Cid: e911Did.number,
|
||||
})
|
||||
.from(extensions)
|
||||
.leftJoin(users, eq(users.id, extensions.userId))
|
||||
.leftJoin(outDid, eq(outDid.id, extensions.outboundCallerIdDidId))
|
||||
.leftJoin(e911Did, eq(e911Did.id, extensions.emergencyCallerIdDidId));
|
||||
|
||||
return {
|
||||
today: todayIn(settings['general.timezone'], now),
|
||||
settings,
|
||||
extensions: rows.map(({ ext, role, outCid, e911Cid }) => ({
|
||||
id: ext.id,
|
||||
number: ext.number,
|
||||
name: ext.name,
|
||||
sipPassword: decrypt(ext.sipPasswordEnc),
|
||||
webrtcEnabled: ext.webrtcEnabled,
|
||||
maxContacts: ext.maxContacts,
|
||||
ringSeconds: ext.ringSeconds,
|
||||
voicemailEnabled: ext.voicemailEnabled,
|
||||
outboundClass: ext.outboundClass,
|
||||
outboundCallerId: outCid,
|
||||
emergencyCallerId: e911Cid,
|
||||
recordMode: ext.recordMode,
|
||||
pickupGroup: ext.pickupGroup,
|
||||
language: ext.language,
|
||||
mohClass: ext.mohClass,
|
||||
isAdmin: role === 'admin',
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/** Numeric order for internal numbers ("99" < "100"), then text as a tiebreak. */
|
||||
export const byNumber = (a: { number: string }, b: { number: string }) =>
|
||||
Number(a.number) - Number(b.number) || a.number.localeCompare(b.number);
|
||||
@@ -0,0 +1,173 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// pjsip_selfphoneng.conf (03 §5) and rtp_selfphoneng.conf (03 §4).
|
||||
import { byNumber, type SnapExtension, type Snapshot } from './load.ts';
|
||||
import * as s from './sanitize.ts';
|
||||
|
||||
const CLASS_NUMBER = { internal: 0, local: 1, national: 2, international: 3 } as const;
|
||||
|
||||
const section = (title: string, lines: (string | false | null | undefined)[]) =>
|
||||
[`[${title}]`, ...lines.filter((l): l is string => typeof l === 'string')].join('\n');
|
||||
|
||||
/** "stun.example.org:3478" → sanitized host[:port]. */
|
||||
function hostPort(v: string): string {
|
||||
const [h, p, ...rest] = v.split(':');
|
||||
if (rest.length) throw new s.SanitizeError(`host:port: invalid value ${JSON.stringify(v)}`);
|
||||
return p === undefined ? s.host(h!) : `${s.host(h!)}:${s.digits(p)}`;
|
||||
}
|
||||
|
||||
function natLines(snap: Snapshot): string[] {
|
||||
const ip = snap.settings['network.publicIp'];
|
||||
if (!ip) return [];
|
||||
return [
|
||||
`external_media_address = ${s.ipv4(ip)}`,
|
||||
`external_signaling_address = ${s.ipv4(ip)}`,
|
||||
...snap.settings['network.localNets'].map((n) => `local_net = ${s.cidr(n)}`),
|
||||
];
|
||||
}
|
||||
|
||||
/** [global], [system] and the transports: hashed to detect changes that need a restart. */
|
||||
export function renderTransports(snap: Snapshot): string {
|
||||
const st = snap.settings;
|
||||
const port = s.digits(String(st['sip.udpPort']));
|
||||
const nat = natLines(snap);
|
||||
return [
|
||||
section('global', [
|
||||
'type = global',
|
||||
'user_agent = SelfPhoneNG',
|
||||
'endpoint_identifier_order = ip,username,anonymous',
|
||||
'keep_alive_interval = 90',
|
||||
'max_initial_qualify_time = 4',
|
||||
]),
|
||||
section('system', ['type = system', 'timer_t1 = 500', 'timer_b = 32000']),
|
||||
section('transport-udp', [
|
||||
'type = transport',
|
||||
'protocol = udp',
|
||||
`bind = 0.0.0.0:${port}`,
|
||||
...nat,
|
||||
]),
|
||||
st['sip.tcpEnabled'] &&
|
||||
section('transport-tcp', [
|
||||
'type = transport',
|
||||
'protocol = tcp',
|
||||
`bind = 0.0.0.0:${port}`,
|
||||
...nat,
|
||||
]),
|
||||
st['sip.tlsEnabled'] &&
|
||||
section('transport-tls', [
|
||||
'type = transport',
|
||||
'protocol = tls',
|
||||
`bind = 0.0.0.0:${s.digits(String(st['sip.tlsPort']))}`,
|
||||
'cert_file = /var/lib/selfphoneng/tls/fullchain.pem',
|
||||
'priv_key_file = /var/lib/selfphoneng/tls/privkey.pem',
|
||||
'method = tlsv1_2',
|
||||
...nat,
|
||||
]),
|
||||
// The bind address is ignored: browsers arrive through Asterisk's HTTP server (01 §3.5).
|
||||
section('transport-ws', ['type = transport', 'protocol = ws', 'bind = 0.0.0.0']),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n\n');
|
||||
}
|
||||
|
||||
function extensionSections(e: SnapExtension): string {
|
||||
const num = s.digits(e.number);
|
||||
const name = s.name(e.name) || num;
|
||||
const groups = e.pickupGroup ? s.ident(e.pickupGroup) : null;
|
||||
const mailbox = e.voicemailEnabled ? `mailboxes = ${num}@default` : null;
|
||||
const vars = [
|
||||
`set_var = SP_EXT=${num}`,
|
||||
`set_var = SP_CLASS=${CLASS_NUMBER[e.outboundClass]}`,
|
||||
e.outboundCallerId && `set_var = SP_OUTCID=${s.digits(e.outboundCallerId)}`,
|
||||
e.emergencyCallerId && `set_var = SP_E911CID=${s.digits(e.emergencyCallerId)}`,
|
||||
`set_var = SP_RECMODE=${e.recordMode}`,
|
||||
'set_var = __SP_DIRECTION=internal',
|
||||
];
|
||||
const common = [
|
||||
`callerid = "${name}" <${num}>`,
|
||||
mailbox,
|
||||
'dtmf_mode = rfc4733',
|
||||
'allow_subscribe = yes',
|
||||
groups && `named_call_group = ${groups}`,
|
||||
groups && `named_pickup_group = ${groups}`,
|
||||
`language = ${s.ident(e.language)}`,
|
||||
`moh_suggest = ${s.ident(e.mohClass)}`,
|
||||
'tone_zone = us',
|
||||
...vars,
|
||||
];
|
||||
const password = s.secret(e.sipPassword);
|
||||
const out = [
|
||||
`; ── extension ${num} (${name}) ──
|
||||
` +
|
||||
section(num, [
|
||||
'type = endpoint',
|
||||
'context = sp-from-ext',
|
||||
'disallow = all',
|
||||
'allow = g722,ulaw,alaw',
|
||||
`auth = ${num}-auth`,
|
||||
`aors = ${num}`,
|
||||
'direct_media = no',
|
||||
'rtp_symmetric = yes',
|
||||
'force_rport = yes',
|
||||
'rewrite_contact = yes',
|
||||
'send_pai = yes',
|
||||
...common,
|
||||
]),
|
||||
section(`${num}-auth`, [
|
||||
'type = auth',
|
||||
'auth_type = userpass',
|
||||
`username = ${num}`,
|
||||
`password = ${password}`,
|
||||
]),
|
||||
section(num, [
|
||||
'type = aor',
|
||||
`max_contacts = ${s.digits(String(e.maxContacts))}`,
|
||||
'remove_existing = yes',
|
||||
'qualify_frequency = 30',
|
||||
mailbox,
|
||||
]),
|
||||
];
|
||||
if (e.webrtcEnabled)
|
||||
out.push(
|
||||
section(`${num}-web`, [
|
||||
'type = endpoint',
|
||||
'context = sp-from-ext',
|
||||
'webrtc = yes', // ICE, AVPF, DTLS-SRTP, rtcp-mux
|
||||
'disallow = all',
|
||||
'allow = g722,ulaw', // no opus: no open source transcoder in Asterisk (AGENTS rule 4)
|
||||
`auth = ${num}-web-auth`,
|
||||
`aors = ${num}-web`,
|
||||
...common,
|
||||
]),
|
||||
section(`${num}-web-auth`, [
|
||||
'type = auth',
|
||||
'auth_type = userpass',
|
||||
`username = ${num}-web`,
|
||||
`password = ${password}`,
|
||||
]),
|
||||
section(`${num}-web`, [
|
||||
'type = aor',
|
||||
'max_contacts = 3',
|
||||
'remove_existing = yes',
|
||||
'qualify_frequency = 0', // browsers don't answer OPTIONS reliably
|
||||
]),
|
||||
);
|
||||
return out.join('\n\n');
|
||||
}
|
||||
|
||||
export function renderPjsip(snap: Snapshot): string {
|
||||
const exts = [...snap.extensions].sort(byNumber).map(extensionSections);
|
||||
return [renderTransports(snap), ...exts].join('\n\n') + '\n';
|
||||
}
|
||||
|
||||
export function renderRtp(snap: Snapshot): string {
|
||||
const st = snap.settings;
|
||||
const lines = ['icesupport = yes'];
|
||||
if (st['network.stunServer']) lines.push(`stunaddr = ${hostPort(st['network.stunServer'])}`);
|
||||
if (st['network.lanIp'] && st['network.publicIp'])
|
||||
lines.push(
|
||||
'',
|
||||
'[ice_host_candidates]',
|
||||
`${s.ipv4(st['network.lanIp'])} => ${s.ipv4(st['network.publicIp'])}`,
|
||||
);
|
||||
return lines.join('\n') + '\n';
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Asterisk load test (03 §12.3): boot the asterisk image with each golden case's generated files and
|
||||
// fail if Asterisk logs ERROR while loading, or if an endpoint/context we generated is missing.
|
||||
// Needs Docker and a built image (ASTERISK_IMAGE, default "selfphoneng-asterisk").
|
||||
// Usage: pnpm test:asterisk [case ...]
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { cpSync, existsSync, mkdtempSync, readdirSync, readFileSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const GOLDEN = fileURLToPath(new URL('../../src/generator/__golden__', import.meta.url));
|
||||
const IMAGE = process.env.ASTERISK_IMAGE ?? 'selfphoneng-asterisk';
|
||||
const NAME = `sp-golden-${process.pid}`;
|
||||
|
||||
const docker = (...args) => {
|
||||
const r = spawnSync('docker', args, { encoding: 'utf8' });
|
||||
return { ok: r.status === 0, out: `${r.stdout ?? ''}${r.stderr ?? ''}` };
|
||||
};
|
||||
const cli = (cmd) => docker('exec', NAME, 'asterisk', '-rx', cmd);
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
/** What the generated files promise Asterisk will have. */
|
||||
function expectations(dir) {
|
||||
const pjsip = readFileSync(join(dir, 'pjsip_selfphoneng.conf'), 'utf8');
|
||||
const endpoints = [...pjsip.matchAll(/^\[([^\]]+)\]\ntype = endpoint$/gm)].map((m) => m[1]);
|
||||
const ext = readFileSync(join(dir, 'extensions_selfphoneng.conf'), 'utf8');
|
||||
const contexts = [...ext.matchAll(/^\[([^\]]+)\]$/gm)].map((m) => m[1]);
|
||||
return { endpoints, contexts };
|
||||
}
|
||||
|
||||
async function check(name) {
|
||||
const tmp = mkdtempSync(join(tmpdir(), `sp-golden-${name}-`));
|
||||
cpSync(join(GOLDEN, name, 'expected'), tmp, { recursive: true });
|
||||
const problems = [];
|
||||
const run = docker(
|
||||
'run',
|
||||
'-d',
|
||||
'--rm',
|
||||
'--name',
|
||||
NAME,
|
||||
'-e',
|
||||
'AMI_SECRET=golden-test-secret',
|
||||
'-v',
|
||||
`${tmp}:/etc/asterisk/selfphoneng`,
|
||||
IMAGE,
|
||||
);
|
||||
if (!run.ok) throw new Error(`docker run failed: ${run.out}`);
|
||||
try {
|
||||
let booted = false;
|
||||
for (let i = 0; i < 60 && !booted; i++) {
|
||||
await sleep(500);
|
||||
booted = cli('core waitfullybooted').ok;
|
||||
}
|
||||
if (!booted) problems.push('Asterisk did not finish booting');
|
||||
await sleep(500);
|
||||
|
||||
const { endpoints, contexts } = expectations(tmp);
|
||||
const shown = cli('pjsip show endpoints').out;
|
||||
for (const e of endpoints)
|
||||
if (!new RegExp(`Endpoint:\\s+${e}[\\s/]`).test(shown))
|
||||
problems.push(`endpoint ${e} not loaded`);
|
||||
for (const c of contexts) {
|
||||
const d = cli(`dialplan show ${c}`).out;
|
||||
if (!d.includes(`'${c}'`)) problems.push(`context ${c} not loaded`);
|
||||
}
|
||||
// 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, '');
|
||||
const errors = log.split('\n').filter((l) => /ERROR\[/.test(l));
|
||||
problems.push(...errors.map((l) => `log: ${l.trim()}`));
|
||||
return { name, problems, endpoints: endpoints.length, contexts: contexts.length };
|
||||
} finally {
|
||||
docker('rm', '-f', NAME);
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
const cases = process.argv.slice(2).length
|
||||
? process.argv.slice(2)
|
||||
: readdirSync(GOLDEN).filter((d) => existsSync(join(GOLDEN, d, 'expected')));
|
||||
let failed = 0;
|
||||
for (const c of cases) {
|
||||
const r = await check(c);
|
||||
const status = r.problems.length ? 'FAIL' : 'ok ';
|
||||
console.log(`${status} ${c} (${r.endpoints} endpoints, ${r.contexts} contexts)`);
|
||||
for (const p of r.problems) console.log(` ${p}`);
|
||||
if (r.problems.length) failed++;
|
||||
}
|
||||
process.exit(failed ? 1 : 0);
|
||||
@@ -74,7 +74,9 @@ describe.skipIf(!hasTestDb)('generator apply', () => {
|
||||
]);
|
||||
const names = readdirSync(dir).sort();
|
||||
expect(names).toEqual(['.prev', ...GENERATED_FILES].sort()); // no leftover .tmp files
|
||||
expect(readFileSync(join(dir, 'pjsip_selfphoneng.conf'), 'utf8')).toBe(HEADER);
|
||||
expect(readFileSync(join(dir, 'pjsip_selfphoneng.conf'), 'utf8')).toBe(
|
||||
render(await loadSnapshot(db))['pjsip_selfphoneng.conf'],
|
||||
);
|
||||
expect(await getSetting(db, 'system.restartRequired')).toBe(false);
|
||||
});
|
||||
|
||||
@@ -195,3 +197,47 @@ describe.skipIf(!hasTestDb)('generator apply', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!hasTestDb)('loadSnapshot', () => {
|
||||
it('decrypts SIP passwords and resolves caller ID DIDs and admin flags', async () => {
|
||||
const { db, drop } = await createTestDb();
|
||||
try {
|
||||
const { setSecretKey, encrypt } = await import('../src/crypto/secrets.ts');
|
||||
setSecretKey(Buffer.alloc(32, 3).toString('base64'));
|
||||
const s = await import('../src/db/schema.ts');
|
||||
const [u] = await db
|
||||
.insert(s.users)
|
||||
.values({ email: 'a@example.com', displayName: 'A', passwordHash: 'x', role: 'admin' })
|
||||
.returning();
|
||||
const [t] = await db
|
||||
.insert(s.trunks)
|
||||
.values({ name: 't', serverHost: 'h.example' })
|
||||
.returning();
|
||||
const [d] = await db
|
||||
.insert(s.dids)
|
||||
.values({ number: '13125551234', trunkId: t!.id, destination: { type: 'hangup' } })
|
||||
.returning();
|
||||
await db.insert(s.extensions).values({
|
||||
number: '101',
|
||||
userId: u!.id,
|
||||
name: 'Dan',
|
||||
sipPasswordEnc: encrypt('Ab3dEf9hIj2kLm4nOp6qRs8t'),
|
||||
voicemailPinEnc: encrypt('482913'),
|
||||
outboundCallerIdDidId: d!.id,
|
||||
});
|
||||
const snap = await loadSnapshot(db, new Date('2026-10-17T12:00:00Z'));
|
||||
expect(snap.today).toBe('2026-10-17');
|
||||
expect(snap.extensions).toEqual([
|
||||
expect.objectContaining({
|
||||
number: '101',
|
||||
sipPassword: 'Ab3dEf9hIj2kLm4nOp6qRs8t',
|
||||
outboundCallerId: '13125551234',
|
||||
emergencyCallerId: null,
|
||||
isAdmin: true,
|
||||
}),
|
||||
]);
|
||||
} finally {
|
||||
await drop();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -36,9 +36,11 @@ Rules for `render()`:
|
||||
4. If nothing changed and not `force`: record a `generator_runs` row and return.
|
||||
5. Copy the current files to `/etc/asterisk/selfphoneng/.prev/` (keep one previous version).
|
||||
6. For each changed file, write `<name>.tmp` then `rename()` it over the old one (atomic).
|
||||
7. Work out reload commands from the changed files (§3). If the PJSIP
|
||||
7. Work out reload commands from the changed files (§3), or from every file when `force`. If the PJSIP
|
||||
transport sections changed, don't reload PJSIP. Set
|
||||
`system.restartRequired = true` instead (see `01-infrastructure.md` §4.3).
|
||||
`system.restartRequired = true` instead (see `01-infrastructure.md` §4.3). Exception: the very first
|
||||
apply (no stored `system.transportHash`) reloads PJSIP normally, since it only adds transports to an
|
||||
Asterisk that booted with none.
|
||||
8. Send each command as AMI `Action: Command`. If AMI is down, mark the run
|
||||
`ok=false, error='ami-disconnected'`. The AMI reconnect handler calls `apply({force:true})` later.
|
||||
9. After `queue reload`: re-send every paused queue member (`QueuePause`), because a reload resets pause state.
|
||||
@@ -197,7 +199,8 @@ set_var = SP_CLASS=2 ; internal=0 local=1 national=2 international
|
||||
set_var = SP_OUTCID=13125551234 ; omit if outbound_caller_id_did_id null
|
||||
set_var = SP_E911CID=13125551234 ; omit if emergency_caller_id_did_id null
|
||||
set_var = SP_RECMODE=never
|
||||
set_var = __SP_DIRECTION=internal ; verify inheritance prefix works in set_var (AGENTS rule 10)
|
||||
set_var = __SP_DIRECTION=internal ; loads as variable "__SP_DIRECTION" (checked on 24.0.0-rc2); inheritance to
|
||||
; dialed channels still to be proven by a call test (M2-T6)
|
||||
|
||||
[101-auth]
|
||||
type = auth
|
||||
|
||||
+2
-1
@@ -14,7 +14,8 @@
|
||||
"test": "vitest run",
|
||||
"lint": "eslint . && prettier --check .",
|
||||
"format": "prettier --write .",
|
||||
"typecheck": "pnpm -r typecheck"
|
||||
"typecheck": "pnpm -r typecheck",
|
||||
"test:asterisk": "node apps/api/test/asterisk/load-golden.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
|
||||
Reference in new issue
Block a user