Files
jp b8c60605fd Update all documentation to current MySQL + React SPA + vm_api.py architecture
README, docs/{ARCHITECTURE,CONFIGURATION,INSTALL,OPERATIONS,SECURITY,TESTING}
and CHANGELOG were stale: they described the original SQLite + CardDAV +
zero-JS vm_web portal. Brought them in line with the actual code:
- MySQL (PyMySQL) as the default store; SQLite fallback only
- vm_contacts resolves from the MySQL contacts table (file/google/carddav removed)
- React SPA frontend + vm_api.py JSON API (:8098); vm_web.py now serves /audio/
- Apache vhost proxies /api/ -> :8098, /audio/ -> :8099, serves the SPA
- CSP relaxed to script-src 'self' 'unsafe-inline' (UI is now JavaScript)
- Added CHANGELOG 1.2.0 entry for the React/vm_api frontend work
2026-08-13 20:20:39 +01:00

5.4 KiB

Testing

How to verify each part of the system. Do the mailcmd tests as the asterisk user, not as yourself — that's where permission bugs hide.


0. Pre-checks

df -h /var                                  # must not be full (Postfix 452)
grep '^mailcmd' /etc/asterisk/voicemail.conf
sudo systemctl is-active vm-api vm-portal
curl -s http://127.0.0.1:8098/api/healthz
curl -s http://127.0.0.1:8099/healthz

1. Unit tests (no sudo, no asterisk)

cd /home/jp/Work/asterisk-voicemail
./venv/bin/python tests/test_telegram.py     # routing, caption clipping, opus transcode
./venv/bin/python tests/test_contacts.py     # vCard/CSV parse, digit-normalised match

test_telegram.py asserts: per-mailbox → one chat; comma list → N chats; group id negative; unmapped → default; enabled=no → no route; caption clipped to 1017 chars on a word boundary when over 1024; a 374 KB wav → 64 KB OggS opus.

test_contacts.py asserts: +447****0123, 07700900123, 447700900123 all match one contact (last-9-digit key); multi-TEL cards; Google CSV ::: split.


2. Build a fake voicemail and run the real pipeline

# needs ffmpeg; uses test_vm.wav shipped in the repo (or any wav)
python3 tests/make_test_mail.py test_vm.wav you@example.com > /tmp/t.eml
sudo chmod 644 /tmp/t.eml
sudo -u asterisk /opt/vm-transcribe/venv/bin/python3 \
     /opt/vm-transcribe/vm_mailcmd.py < /tmp/t.eml
sudo tail -5 /var/log/asterisk/vm_mailcmd.log

Expect transcribed … chars then sent enriched notification to you@example.com. Then confirm real delivery:

sudo grep "to=<you@example.com>" /var/log/mail.log | tail -1   # status=sent

Fail-safe: a message with no audio

printf 'From: a@b\nTo: you@example.com\nSubject: no audio\n\nbody\n' > /tmp/n.eml
sudo chmod 644 /tmp/n.eml
sudo -u asterisk /opt/vm-transcribe/venv/bin/python3 \
     /opt/vm-transcribe/vm_mailcmd.py < /tmp/n.eml
# log must show: "no audio attachment; relaying original"

Fail-safe: a thrown error relays the original

Temporarily point VM_DB at an unreadable path (or MySQL offline); the script should fall back to relaying the original Asterisk message and log relaying original, mailcmd error.


3. Telegram (needs a real token)

sudo /opt/vm-transcribe/venv/bin/python3 /opt/vm-transcribe/vm_tg_setup.py ids
# each recipient must have messaged the bot once (/start) to appear
sudo /opt/vm-transcribe/venv/bin/python3 /opt/vm-transcribe/vm_tg_setup.py test 1001
# sends a real voice note + summary; exits non-zero on any failure

Offline, the routing logic is covered by tests/test_telegram.py so you don't need a token to verify the config parser.


4. Contacts (MySQL)

# resolve a caller ID against the MySQL contacts table
sudo -u asterisk /opt/vm-transcribe/venv/bin/python3 \
     /opt/vm-transcribe/vm_contacts.py '<07700900123>'
# prints (name, email) or the raw caller id

# load an export
sudo -u asterisk /opt/vm-transcribe/venv/bin/python3 \
     /opt/vm-transcribe/vm_import_contacts.py /path/to/contacts.vcf

5. JSON API — auth, authz, playback, delete

Run the API locally (or against the live service) and exercise it with curl. Use VM_INSECURE_COOKIE=1 only for plain-HTTP local tests so the Secure cookie can be set.

B=http://127.0.0.1:8098

# login ok
curl -s -c /tmp/j -o /dev/null -w '%{redirect_url}\n' -d 'mailbox=7940&pin=5159' $B/api/login

# wrong pin rejected
curl -s -o /dev/null -w '%{http_code}\n' -d 'mailbox=7940&pin=1111' $B/api/login

# authenticated message list
curl -s -b /tmp/j $B/api/messages | head -c 400

# contacts CRUD
curl -s -b /tmp/j $B/api/contacts | head -c 400

Authorization (IDOR) — must 404

A session for mailbox A must never reach mailbox B's data. With a cookie for mailbox 7940, hitting /api/messages/{id-owned-by-1001} (or its audio/delete variants) must return 404, never serve or delete the other mailbox's message. Add this assertion whenever you change vm_store or vm_api.


6. Portal over the real URL (TLS + proxy)

R="--resolve vm.txt3.net:443:ORIGIN.IP"
curl -s $R https://vm.txt3.net/api/healthz
curl -s $R -o /dev/null -w '%{http_code}\n' https://vm.txt3.net/login
curl -s $R -D- -o /dev/null https://vm.txt3.net/login | grep -iE 'strict-transport|content-security|x-frame'
# http -> https redirect
curl -s $R -o /dev/null -w '%{redirect_url}\n' http://vm.txt3.net/

The React SPA is served as static files; all data flows through /api/. Confirm the SPA fallback returns index.html for arbitrary client-side routes:

curl -s $R -o /dev/null -w '%{http_code}\n' https://vm.txt3.net/some/client/route

Behind a CDN, --resolve to the origin IP; otherwise you are testing the CDN, not your server.


7. Backfill

sudo -u asterisk /opt/vm-transcribe/venv/bin/python3 \
     /opt/vm-transcribe/vm_import.py --dry-run --limit 5
# preview: lists 5 messages with caller + date, no writes

sudo -u asterisk /opt/vm-transcribe/venv/bin/python3 \
     /opt/vm-transcribe/vm_import.py --limit 3
# real: 3 imported, others skipped; re-running is a no-op (idempotent)

Verify they appear via the API/messages endpoint and are playable (§5). Expect a no_speech count — 44-byte WAVs are hung-up calls, not failures.

CDR history import:

sudo -u asterisk /opt/vm-transcribe/venv/bin/python3 /opt/vm-transcribe/vm_backfill_cdr.py