Files
2026-10-21 19:10:02 -05:00

295 lines
21 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Browser checks
Dependency-free scripts that drive a headless Chromium (Edge/Chrome) over the DevTools
protocol. They need an api serving the built web UI and a fresh database.
```sh
pnpm --filter @selfphoneng/web build && mkdir -p /tmp/sp && cp -r apps/web/dist /tmp/sp/public
# fresh DB, then from /tmp/sp:
DATABASE_URL=postgres://… INITIAL_ADMIN_EMAIL=admin@example.com INITIAL_ADMIN_PASSWORD=initial-admin-pw \
node --env-file=<repo>/.env --conditions=source <repo>/apps/api/src/index.ts
BROWSER=/usr/bin/chromium node scripts/e2e/m1-t4.mjs http://127.0.0.1:8080 admin@example.com initial-admin-pw /tmp/shots
node scripts/e2e/mobile.mjs http://127.0.0.1:8080 admin@example.com correct-horse-battery-staple /tmp/shots
```
`m1-t4.mjs` is the M1-T4 acceptance flow: forced password change, then create/edit/delete a user.
## M2 live loop (any Docker host, no host networking)
`m2-live.mjs` runs the whole chain on a private Docker network: a test Asterisk with AMI published
on `127.0.0.1:15038`, the api writing config into it, two baresip phones, and the dashboard in a
browser. It checks that a new extension is live in Asterisk within 2 s, both phones show online,
and the dots turn red during a call.
```sh
node scripts/e2e/m2-live.mjs up /tmp/sp-live-conf
# start the api with AMI_PORT=15038 AMI_SECRET=m2-live-secret ASTERISK_CONFIG_DIR=/tmp/sp-live-conf
node scripts/e2e/m1-t4.mjs http://127.0.0.1:8080 admin@example.com initial-admin-pw /tmp/shots
node scripts/e2e/m2-live.mjs check http://127.0.0.1:8080 admin@example.com correct-horse-battery-staple /tmp/shots
node scripts/e2e/m2-live.mjs down
```
`m3-t1.mjs` runs on the same setup (after `m1-t4.mjs`): it starts a fake voip.ms (a second Asterisk
that accepts one sub-account) on the m2-live network, creates the trunk in the UI and waits for
**Registered**, then presses Test. `m3-t2.mjs` (after `m3-t1.mjs`) adds a phone number routed to a user in the UI and
checks `sp-inbound` in Asterisk, and that the extension can no longer be deleted. `m3-t3.mjs` (after
both) checks the default outbound rules, the test box, a user's calling permission, and that
reordering changes the include order in Asterisk. `m3-t4.mjs` (after all three) runs a tiny SMTP sink on
127.0.0.1:2525 and a baresip phone as 101, then checks the dashboard warnings, Settings → Email and
Emergency, and the Test notifications button (email arrives, 101 rings, red banner). Nothing dials 911. The fake provider from `m3-t1.mjs` stays up until `m2-live.mjs down`: a container that took
over its IP would be identified as the trunk.
`pnpm test:asterisk` (golden-file load test, `call-test.ts` with 30 s two-way audio between two
baresip phones, and `trunk-test.ts` with registration against a fake provider) needs no api at all.
`m3-t5.mjs` is the M3-T5 acceptance and needs its own fresh database: it signs in with the
initial password, walks all ten wizard steps against a fake voip.ms, then proves an inbound call
to the DID rings the new user's softphone and an outbound call leaves with the DID as caller ID.
`m4-t2.mjs` is the M4-T2 acceptance (no browser): start m2-live with a voicemail dir
(`m2-live.mjs up <confDir> <vmDir>`) and the api with `VOICEMAIL_DIR=<vmDir>`. It runs the SMTP
sink (`smtp-sink.mjs`, shared with `m3-t4.mjs`), adds a user and extension 150 with voicemail,
leaves a message from a Local channel, and checks the email arrives within 10 s with a PCM 8 kHz
wav attached. The voicemail itself (MWI on a phone, `*97`) is covered by `pnpm test:asterisk`.
`m4-t3.mjs <baseUrl> <adminEmail> <adminPassword> <vmDir> <shotsDir>` is the M4-T3 acceptance on the
same setup: it adds a user with extension 160 and a baresip phone with the mwi module, leaves
messages, then as that user in the browser plays one (marks it read) and deletes another, each
time checking the phone's MWI goes off within 5 s. It records the "no answer" greeting with a
fake microphone and checks a caller hears it.
`m5-t1.mjs <baseUrl> <adminEmail> <adminPassword> <shotsDir>` is the M5-T1 check in Docker: start
the api with `WEBRTC_WS_URL=ws://127.0.0.1:18088/ws`. `m2-live.mjs up` publishes Asterisk's HTTP
server on 127.0.0.1:18088 and RTP 10000–10019/udp. The script sets LAN IP = the container and public
IP = 127.0.0.1 (so ICE offers 127.0.0.1), restarts telephony, then signs in as a new user, enables
the softphone with a fake microphone, and checks two-way audio to a baresip phone (171) in both
directions.
`m5-t2.mjs` (after `m5-t1.mjs`, same user) starts two baresip phones (172, 173) through
`phones.mjs` and checks the softphone features: the incoming call dialog, DTMF, hold (music on hold
at the other end), call waiting, blind and attended transfer, and "Use here" moving the softphone to
a second tab (`cdp.mjs` `newTab()`).
`m5-t3.mjs` (after `m5-t2.mjs`) is the M5-T3 acceptance. It watches People in one browser while 173
calls 172 and measures the time from Asterisk's hint changing to the dot changing (must be < 1 s).
Then a second browser, as a new user with desk phone 174 and the softphone off, clicks **Call** on
172: 174 must ring first (Asterisk channel order), then the two are connected.
`m6-t1.mjs` needs m2-live started with a sounds dir (`m2-live.mjs up <confDir> <vmDir> <soundsDir>`)
and the api with `SOUNDS_DIR=<soundsDir>`. It drops a generated wav on **Sounds & music**, uploads it,
checks that Asterisk can play `custom/<id>`, renames it, and deletes it, which is refused while it's
the recording notice. `cdp.mjs` `fill()`/`click()` take an optional selector to search within.
`m6-t2.mjs` (after `m1-t4.mjs`) creates ring group 500 in the UI with members 190 (never answers)
and 191 (auto-answer), and checks that a call from 192 rings both and is answered by 191. It then
switches the group to "one at a time" with 5 s each and checks that 190 rings alone before 191.
`m6-t3.mjs` (sounds dir like `m6-t1.mjs`) builds digital receptionist 600 in the UI. It uploads the
greeting through the form's SoundPicker, sends key 1 to 193 and key 2 to 194, and then a Local
channel sending DTMF presses 1, 2 and a wrong key (5): 193 or 194 rings, or "not a valid choice"
plays and then the greeting again.
`m6-t4.mjs` builds office hours in the UI. It paints Saturday 10–11 on the grid and checks that
Force closed / Force open route a call to 196 / 195 and set or clear AstDB `SP/TCOVR/<id>`. `*321`
and `*331` from the admin's own phone (197) must reach the database and the page, `*311` from a
non-admin phone must get "no service", and the 2026 US federal holiday import must reach the
dialplan.
`m6-t7.mjs` (after `m1-t4.mjs`) checks the feature codes from a real phone (198, belonging to user
`fwd@example.com`):
- `*78`/`*79` set DND (the database, AstDB and the page follow, and calls meanwhile go to
voicemail).
- `*72199`/`*73` forward everything to 199. A `*72` to an outside number this user may not call is
refused, removed from AstDB, and shown as a toast.
- `/me/settings` saves a no-answer forward.
- `*43` echo test and `*65` say my extension.
- `**200` and `*8` pick up a call ringing at 200.
`m6-check.mjs` is the M6 milestone check (after `m1-t4.mjs`). It builds DID → office hours → menu
(1 → a ring group of 221 and 222, 2 → 223; closed → 220's voicemail) and calls the trunk entry with
a caller ID over AMI. It checks the closed path; `*31N` from an admin's phone (220) forcing open;
the menu keys; and a number blocked on **Blocked numbers** being rejected (cause 21) while other
callers still get through.
`m7-t2.mjs` (after `m1-t4.mjs`, with the Asterisk image rebuilt for `cdr_manager.conf`) makes real
calls: 231 → 230 answered, then 231 → 230 while 230 is on DND (voicemail). It waits for them to
reach `calls` through the AMI `Cdr` events. It then checks the admin Call history (status filter,
expanded legs, CSV export), and as user `hist@example.com` (230): the missed call on Home,
`/me/calls`, and **Call back**, which rings 230's desk phone first and then 231.
`m7-t3.mjs` needs m2-live with sounds and recordings dirs (`m2-live.mjs up <conf> <vm> <sounds>
<recs>`) and the api with `SOUNDS_DIR` and `RECORDINGS_DIR`. It picks the recording notice under
Settings → Recording and ticks "Record every call" on a phone number in the UI. A caller then
reaches 240 (AMI Originate into the trunk entry, `ami.mjs`): the notice plays, MixMonitor records,
and the call's player in Call history serves the wav. The recording is also on the Recordings page.
`m7-t4.mjs`: seven auto-answer phones (260-266) and three concurrent calls (`channel originate`).
The dashboard's "Calls now" list must show exactly the channels in `core show channels concise`.
Then the admin hangs up one call (confirm dialog) and transfers another's caller to 266; the list
matches Asterisk after each step.
`m8-t1.mjs`: three auto-answer phones (agents 280 and 281, overflow 282). It builds queue 550 in
the UI (wait 20 s, then 282; callers wait even with no agent logged in), pauses 280 with `*45550`
from the phone and 281 with the queue page's button, and puts three callers in line (AMI Originate
of `Local/550@sp-from-ext`). The queue page and the dashboard tile show them live; unpausing 281
connects the longest waiting caller, a second caller goes to 282 after 20 s, a third hangs up.
`*45550` logs 280 back in, `*45551` (not an agent) plays `ss-noservice`, and the stats count
3 calls: 1 answered, 1 hung up, 1 timed out.
`m8-t2.mjs`: three auto-answer phones (290 parks, 291 belongs to a user, 292 picks up). A caller
reaches 290, which blind-transfers it to 700 (AMI BlindTransfer, what a phone's transfer key does):
it lands on 701 and the slot's hint goes InUse. The dashboard and the user's Home list it, the live
call shows "Parked", and **Pick up 701** on Home rings 291, which dials 701 and talks to the caller.
A second park takes 702 (`findslot = next`) and 292 picks it up by dialing 702.
`m8-t3.mjs` needs m2-live with a recordings dir. It builds room 800 in the UI (PIN 4826, admin PIN
917355, at most 3, recorded; the form refuses an admin PIN equal to the PIN). Callers are Local
channels typing with `SendDTMF`: a wrong PIN plays `conf-invalidpin` and asks again, the admin PIN
joins with flags `AM` in `confbridge list`, a 4th caller is turned away, and the recordings appear
on the Recordings page. Room 801 has no PIN; desk phones 300 and 301 dial in.
`m8-t4.mjs`: phones 320 and 321 answer manually but honor the auto-answer headers
(`sip_autoanswer=yes`, like a Yealink or Grandstream); 322 auto-answers and does the paging. It
builds paging group 850 in the UI. An ordinary call to 320 keeps ringing; a page from 322 is
answered by 320 and 321 by themselves (the INVITE carries `Call-Info: …;answer-after=0` and
`Alert-Info`, seen with `pjsip set logger on`), muted (`m` in `confbridge list`), and 322 (busy,
a member too) isn't paged. `*80320` is two-way; `*80999` plays `ss-noservice`.
`m8-t5.mjs` needs m2-live with a moh dir (`m2-live.mjs up` 5th argument, the api with `MOH_DIR`).
It makes the set `sales-jazz` on **Sounds & music** (refusing a bad name), uploads a tone into it,
and checks `moh show files`, a queue caller hearing it ("Started music on hold, class
'sales-jazz'"), a user's `moh_suggest`, delete refused while used, and the default music once the
set is empty.
`m8-check.mjs` is the M8 milestone check (after `m1-t4.mjs`): phones 340–343, a DID to queue 552 (550 is m8-t1's)
(agents 340 and 341, 30 s, then 342). 340 pauses with `*45552` and 341 from the api, three callers
come in from the trunk entry and wait; 341 comes back, gets caller 1, parks them on 701 (AMI
BlindTransfer to 700), 343 dials 701 and picks up; 341 takes caller 2 and caller 3 rings 342 after
30 s. The stats count 3 offered, 2 answered, 1 timed out.
`m9-t1.mjs` starts 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`; the optional 5th argument is the api log).
With a wrong API password the trunk form shows voip.ms's reason and the built-in server list; with
the right one the list comes from `getServersInfo`. **Import numbers from voip.ms** imports one of
two numbers with its description as the label, the list marks it added, Phone numbers shows it,
and the api log never contains the API password. The trunk is created turned off, so nothing
registers to the real voip.ms.
`m10-t1.mjs` (after `m1-t4.mjs`) adds a Yealink on **Phones** (a short MAC is refused), reads the
URL, user name and password from the drawer, and checks the preview masks the passwords. Then,
as the phone: `/prov/<mac>.cfg` answers 401 without credentials and the file with them, and a
baresip phone given only the account from that file registers. The list then shows when it
fetched its settings.
`m10-t2.mjs` (after `m1-t4.mjs`) gives 360 BLF keys for 361 and the parking slot 701 and a
Yealink. From the phone's side: the config names the keys, clears the rest, has Central time, and
its phonebook URL (with the phone's credentials in it) returns the extensions. Asterisk has hints
for 361 and 701. With the company time zone set to Asia/Tokyo, the Phones page warns that phones
show UTC (the zone is put back after).
`m10-t3.mjs` (after `m1-t4.mjs`) as user `lin@example.com` (370): **My settings → Set up a phone
app** shows the Linphone QR and its link. Fetching the link with no other credentials (as Linphone
does) returns the account XML; a changed token gets 404; and a baresip phone given only the user
name and password from the XML registers. Scanning the QR with the Linphone app is left to a person.
`m10-t4.mjs` (after `m1-t4.mjs`): 380 calls 381; the admin, whose extension is 382 (desk phone),
presses **Listen**, **Whisper** and **Barge** on the dashboard. Each time 382 rings, answers and
runs `ChanSpy(PJSIP/380-…, qE / qEw / qEB)`; hanging up the call ends the barge leg too.
`m11-t1.mjs` (after `m1-t4.mjs`; needs the Asterisk image with `sp-vm-pincheck`; 5th argument: the
generated config dir) clicks through admin pages and fails on any Content-Security-Policy
violation; turns the phone ACL on with the phones' Docker network unlisted (390 can't register:
"Not match Endpoint ACL") and then listed (it registers); and changes 391's voicemail PIN from the
phone with `*98` and DTMF: `1111` is refused by `sp-vm-pincheck`, `7391` is stored by the api and
survives a later config write.
`m11-t1-totp.mjs` (after `m1-t4.mjs`): the admin turns two-step sign-in on from **My account**
(a wrong code refused, 10 recovery codes shown), signs out and back in with password then a code,
then with a recovery code (9 left), and turns it off with the password. Codes come from
`apps/api/src/auth/totp.ts` (node runs it directly).
`m11-t2.mjs <baseUrl> <adminEmail> <adminPassword> <tlsDir> <shotsDir>` (after `m1-t4.mjs`; start
the api with `TLS_DIR=<tlsDir>`, an empty folder) makes a throwaway CA with the image's openssl and
starts a fake voip.ms that only speaks TLS (`sp-live-tlsprov`, certificate for its IP). Settings →
Security first says there is no certificate, then shows the PBX's; the admin turns TLS on and
telephony restarts. A TLS + SRTP trunk must not register while the CA is untrusted (`verify_server`
logs "certificate is untrusted"), then registers once it is; a call carries `RTP/SAVP` with an
`a=crypto` key and audio both ways; a renewed PBX certificate is served after
`module reload res_pjsip.so` (what `scripts/tls-cert.sh` runs) while that call stays up.
`m11-t3.mjs` (no api, no m2-live; needs the images from `docker compose build`) is the M11-T3
acceptance on two throwaway servers: privileged `docker:28-dind` containers, each running the real
compose stack with the images loaded from this host. Server A gets data (a new admin password,
user Dan with 201, a trunk with a password, voicemail, a sound, music on hold, a recording) and
**Back up now** (0600, no generated config, no recordings, the trunk password only encrypted in
`db.sql`). Server B, a fresh install with the same `.env`, refuses the backup under another
`APP_SECRET` and on a "no", then restores it: the admin signs in with A's password, 201 and the
trunk are back, the trunk password decrypts into Asterisk's config, the files match byte for byte.
`KEEP=1` leaves both servers running.
`m11-t4.mjs` (m2-live started with a voicemail dir, after `m1-t4.mjs`; stop your api first: the
script starts and restarts it, with the api's environment taken from its own) is the M11-T4
acceptance, with mail going to a local SMTP sink on 127.0.0.1:2525. Settings → Network turns on
the public IP watch and Settings → Recording keeps voicemail 30 days. A real voicemail, made 40
days old in the database, is removed through Asterisk (files gone) when the api restarts. The
watch finds the real public IP (needs internet), saves it, asks for a restart and emails the
admins. A trunk to 192.0.2.1 (nothing answers) gets exactly one "is down" email after 5 minutes,
and a disk under 10% free gets a "Disk almost full" email. About 8 minutes.
`m11-t5.mjs` (m2-live with a voicemail dir, after `m1-t4.mjs`) is the M11-T5 acceptance. An API
token made in Settings → API tokens adds extension 201 from a script (no cookie, no CSRF header),
can't manage tokens, and is refused once revoked in the browser. A webhook added in Settings →
Webhooks to a receiver on this host is refused as a private address until that's allowed, then
the test arrives signed. A real call to 201 (Local channel, voicemail answers) delivers
`call.started`, `call.ended` and `voicemail.new`, all with valid signatures; the receiver fails
the first one with a 500 and it comes again a minute later. About 2 minutes.
`m11-t6.mjs` (m2-live with a voicemail dir, after `m1-t4.mjs`) is the M11-T6 acceptance. Three real
calls: one answered by phone 260, one to 261's voicemail and one through queue 560 that 260
answers. `/api/reports/summary` agrees with call history (today's bars, missed, this hour in the
heatmap) and `/api/reports/queues` shows 560 with 1 offered, 1 answered, 100% in time. The Reports
page shows the same at 1280 px and 390 px, without scrolling sideways on the phone. About 3
minutes.
`m11-t7.mjs` (m2-live, after `m1-t4.mjs`) checks the admin guide (`docs/admin-guide/`). A baresip
phone signs in to extension 270 with the wrong password; fail2ban's stock `asterisk` filter
(`fail2ban-regex`, Debian's fail2ban) matches the resulting `security.log` lines with the phone's
IP, and CrowdSec's `crowdsecurity/asterisk` collection parses them (`cscli explain`). It then takes
the guide's screenshots into the given folder (run it with `docs/admin-guide/img`) and checks every
relative link, image and `#anchor` in the guide. About 4 minutes.
`m11-system.mjs` (m2-live with sounds and recordings dirs, api with `RECORDINGS_DIR`, after `m1-t4.mjs`)
checks the owner's follow-ups to M11-T7. The user drawer's Calling → "Record calls" set to "Calls
from outside" records a real inbound call to 241 (MixMonitor, a recording in call history); set back
to "Never", the next call isn't recorded. A public IP change on Settings → Network raises the
dashboard's restart warning, whose link opens System ("Restart: Needed", the command); restarting
the Asterisk container brings it to "Running" / "Not needed", the new IP is in the transport and the
warning is gone. System information shows the IP and ports, the event log and its CSV have the restart
(restart needed, Asterisk down, up), and the page doesn't scroll sideways at 390 px. Before signing
in, the login page's link opens About, whose "Code v<version>" downloads `selfphone.tar.gz` byte for
byte as built: the runner builds the api image and copies its `VERSION` and archive into the api's
working folder (5th argument). About 6 minutes.
`m11-diagnostics.mjs <baseUrl> <adminEmail> <adminPassword> <shotsDir>` (after `m1-t4.mjs`, on a
database without `network.probeUrl`) checks System → Diagnostics: the server's checks show, the
network tests wait for a probe, then it sets one in Settings → Network (a probe it starts on
127.0.0.1:18090, so the results also warn that it's inside the network) and runs a quick and a
complete test. Asterisk isn't needed.
`numbers-music-reports.mjs <baseUrl> <adminEmail> <adminPassword> <shotsDir>` (after `m1-t4.mjs`;
`PG_CONTAINER` and `PG_DB` name the Postgres container and database, for the calls it inserts)
checks the owner's 2026-10-21 features: Settings → Numbering (users from 1, Next free suggests 1),
the music-on-hold play order (move up, Shuffle), and the Reports totals (answered, talk time,
outside minutes in and out, the voip.ms balance tile). Needs ffmpeg for the uploads; no Asterisk.
`ai-settings.mjs <baseUrl> <adminEmail> <adminPassword> <shotsDir>` (after `m1-t4.mjs`, spec 10)
runs a fake OpenAI-style server on 127.0.0.1:18001 and checks Settings → AI: Test for speech to
text and text to speech (the voice plays), Save with voicemail transcripts on, and an address off
the local network refused.
`call-transcripts.mjs <baseUrl> <adminEmail> <adminPassword> <shotsDir>` (after `m1-t4.mjs`, with
`PG_CONTAINER` and `PG_DB` like `numbers-music-reports.mjs`) puts a recording with a transcript and
summary in the database and checks the Recordings page: the summary, why another has none, Show
transcript in order, and search by what was said.