1. REST 404 "Unknown host: prosody": the tester reaches prosody by service name (http://prosody:5280), so aiohttp sends Host: prosody, which Prosody rejects as an unknown vhost. The auto host-header heuristic only overrides for IP/localhost. Set REST_HOST_HEADER= example.com explicitly in the tester env so HTTP routing lands on the example.com VirtualHost that serves mod_http_rest /rest. 2. healthcheck.sh not found: the static test_04_infra checks resolve ../config/healthcheck.sh (= /config/healthcheck.sh) but the tester image only ships /tests. Mount ./config:/config:ro so the path resolves inside the container. 3. telnet non-loopback banner: read only 256 bytes, capturing just the ASCII-art top and never the literal "Prosody" text. Bump to 1024 to match test_04_infra.test_telnet_banner, which passes with the larger read. Part-of: <http://gitlab.vnc.biz/uxf/vnctalk-prosody/-/merge_requests/3>
VNCtalk Prosody Verification Test Suite
This directory contains an integration-test suite that verifies whether a running XMPP server conforms to the VNCtalk Prosody behaviour requirements.
Prerequisites
You need Python 3.10+ with the venv module.
1. Create the virtualenv
Create it inside the repo (recommended) so it is isolated and reproducible:
cd /path/to/vnctalk-prosody
python3 -m venv tests/venv
Or use a hidden .venv at repo root:
python3 -m venv .venv
2. Activate the virtualenv
Linux / macOS:
source tests/venv/bin/activate
Windows (PowerShell):
tests/venv/Scripts/Activate.ps1
Windows (cmd.exe):
tests/venv/Scripts/activate.bat
3. Install dependencies
pip install -r tests/requirements.txt
(or manually: pip install slixmpp pytest pytest-asyncio aiohttp lxml psycopg2-binary asyncpg)
4. Verify the installation
pytest tests/ --collect-only -c tests/pytest.ini
You should see ~86 tests collected. If the collection fails with an asyncio error, make sure you are passing -c tests/pytest.ini (it sets asyncio_mode = auto).
Configuration
Tests are configured via environment variables or CLI options.
Environment variables
| Variable | Default | Description |
|---|---|---|
XMPP_HOST |
localhost |
C2S hostname |
XMPP_PORT |
5222 |
C2S port |
XMPP_JID |
(empty) | Test account JID (bare or full) |
XMPP_PASSWORD |
(empty) | Test account password |
XMPP_JID2 |
(empty) | Second distinct account for affiliation tests |
XMPP_PASSWORD2 |
(empty) | Second account password |
XMPP_DOMAIN |
example.com |
XMPP domain |
BOSH_URL |
(empty) | BOSH endpoint URL |
WS_URL |
(empty) | WebSocket endpoint URL |
REST_URL |
(empty) | mod_http_rest URL |
REST_USER |
(empty) | HTTP Basic Auth user for /rest (reverse proxy credential) |
REST_PASSWORD |
(empty) | HTTP Basic Auth password for /rest (reverse proxy credential) |
REST_HOST_HEADER |
auto |
Host header for /rest: auto sends the XMPP domain only for IP/localhost URLs (direct prosody needs it for vhost routing; ingress hostnames reject a mismatching Host, e.g. 431), none disables, anything else is sent verbatim |
VERIFY_SSL |
(unset) | Verify TLS certificates (default: False for self-signed dev certs) |
MUC_DOMAIN |
(empty) | MUC component domain |
ADMIN_TELNET_HOST |
127.0.0.1 |
Admin telnet host |
ADMIN_TELNET_PORT |
5582 |
Admin telnet port |
PG_HOST |
(empty) | PostgreSQL host |
PG_PORT |
5432 |
PostgreSQL port |
PG_DB |
prosody |
PostgreSQL database name |
PG_USER |
(empty) | PostgreSQL user |
PG_PASSWORD |
(empty) | PostgreSQL password |
MOCK_URL |
(empty) | Base URL of the test mock service (HTTP side-effect assertions) |
SMACKS_HIBERNATION_TIME |
300 |
Server's smacks_hibernation_time (lower it, e.g. 10, so SMACKS expiry tests run fast) |
CLI options
All env vars can be overridden on the command line:
pytest tests/ \
--xmpp-host=192.168.1.10 \
--xmpp-jid=user@example.com \
--xmpp-password=secret \
--xmpp-jid2=user2@example.com \
--xmpp-password2=secret2 \
--xmpp-domain=example.com \
--muc-domain=conference.example.com \
--bosh-url=http://192.168.1.10:5280/http-bind \
--ws-url=ws://192.168.1.10:5280/xmpp-websocket \
--rest-url=http://192.168.1.10:5280/rest \
--rest-user=restuser \
--rest-password=restsecret \
--pg-host=192.168.1.10 --pg-user=prosody --pg-password=secret --pg-db=prosody
# Add --verify-ssl if your server has a real (not self-signed) certificate
Running
The test suite includes a pytest.ini with asyncio_mode = auto. Run from the repo root or point to the config:
Run all tests
pytest tests/ -v -c tests/pytest.ini
Skip live-connection tests (dry-run infrastructure checks only)
pytest tests/ -v --skip-live -c tests/pytest.ini
Run a specific test file
pytest tests/test_01_core.py -v -c tests/pytest.ini
Running with docker-compose (recommended)
The repo ships a self-contained docker-compose.yml that brings up everything
the suite needs — no external auth backend, FCM proxy, file-share, or DB:
| Service | Image / build | Purpose |
|---|---|---|
postgres |
postgres:15-alpine |
Prosody's mod_storage_sql backend (published 5434:5432 on the host) |
mocks |
./tests/mocks |
Single aiohttp app standing in for auth/FCM/delfile/avatar/file-share, with a /__requests capture API (published 8092:8080) |
prosody |
. (this Dockerfile) |
The server under test, built from this repo (5222, 5280, 5582) |
tester |
./tests |
Runs pytest inside the network; opt-in via the ci profile |
Authentication is delegated to the mock (mod_auth_http_async → http://mocks:8080/auth),
so the three test accounts live entirely in the mock's AUTH_USERS env var —
there is no Prosody user-provisioning step. user1@example.com / user2@example.com
are the two distinct accounts; admin@example.com is a server admin (for telnet
and admin-only paths).
A. One-shot: build + run the whole suite in the tester container
make test
# equivalent to:
# docker compose up -d --build postgres mocks prosody
# docker compose run --rm tester pytest . -v -c pytest.ini
The tester service is gated behind the ci profile, so it only starts when
explicitly invoked (it is not brought up by docker compose up).
B. Start the stack, then run pytest from the host
This is more convenient for iterating on a single test file:
make up # docker compose up -d --build postgres mocks prosody
# wait for prosody to become healthy:
docker compose ps # prosody should show "healthy"
Then point the suite at the published host ports:
export XMPP_HOST=localhost XMPP_PORT=5222
export XMPP_DOMAIN=example.com MUC_DOMAIN=conference.example.com
export XMPP_JID=user1@example.com XMPP_PASSWORD=pass1
export XMPP_JID2=user2@example.com XMPP_PASSWORD2=pass2
export REST_URL=http://localhost:5280/rest
export BOSH_URL=http://localhost:5280/http-bind
export WS_URL=ws://localhost:5280/xmpp-websocket
export ADMIN_TELNET_HOST=localhost ADMIN_TELNET_PORT=5582
export PG_HOST=localhost PG_PORT=5434 PG_DB=prosody PG_USER=prosody PG_PASSWORD=prosody
export MOCK_URL=http://localhost:8092
export SMACKS_HIBERNATION_TIME=10 # matches compose; lets SMACKS expiry tests run fast
pytest tests/ -v -c tests/pytest.ini
Host-port note: the compose file publishes
5434:5432(PG) and8092:8080(mocks) because5433/8090are often already taken on developer machines. Adjust theports:mappings indocker-compose.ymlif you need different ones.
C. Inspecting side-effects and logs
The mock keeps an in-memory log of every request it receives, queryable for deterministic side-effect assertions:
# list captured requests (optionally filtered by path prefix)
curl -s 'http://localhost:8092/__requests?path=/fcm/notify' | python3 -m json.tool
# reset the capture log between manual runs
curl -X DELETE http://localhost:8092/__requests
Prosody logs to its log file inside the container:
docker compose exec prosody tail -f /var/log/prosody/prosody.log
# or: make logs
Tearing down
make down # docker compose down
docker compose down -v # also remove the pgdata volume
Importing an existing database
To run the suite against a snapshot of a real VNCtalk Prosody database (e.g. to
reproduce a production data condition), load the dump into the compose postgres
service before starting Prosody.
1. Start postgres only
docker compose up -d --build postgres
# it exposes PG on localhost:5434
2. Restore a dump into the prosody database
The compose postgres is pre-provisioned with an empty prosody database owned
by the prosody user (password prosody). Restore a plain SQL or custom-format
dump straight into it:
# plain SQL dump
psql -h localhost -p 5434 -U prosody -d prosody -f /path/to/prosody.sql
# or, from a pg_dump custom file:
pg_restore -h localhost -p 5434 -U prosody -d prosody --no-owner --clean --if-exists /path/to/prosody.dump
If your dump targets a different DB/user, recreate them instead and point the
compose prosody service env (prosodyDBname/prosodyDBuser/prosodyDBpass)
at those — but the simplest path is to restore into the pre-created prosody
database. If sql_manage_tables = true (it is, in the template) Prosody will
create any missing tables on startup, so a partial dump is fine.
3. Drop the captured auth, then bring up the rest
The imported DB may contain real fcmtoken / private / vcard rows that
would skew side-effect assertions. To get a clean run, either wipe the relevant
stores or use fresh test accounts. Then start Prosody and the mocks:
docker compose up -d --build mocks prosody
4. Run the suite
Use the host-port flow from B above (PG_HOST=localhost PG_PORT=5434 ...).
Because authentication = "http_async", the imported users' passwords are not
used — the mock auth backend decides logins, so keep using user1/user2 (or
extend AUTH_USERS in docker-compose.yml to match imported JIDs).
Restoring into a fresh volume
If the pgdata volume already has data you want to replace:
docker compose down -v # remove the volume
docker compose up -d postgres # recreate empty
psql -h localhost -p 5434 -U prosody -d prosody -f /path/to/prosody.sql
Running against a local Prosody container (without compose)
If you want to run Prosody standalone (e.g. the legacy test.sh flow, or a
one-off docker run against an external auth backend and DB), build the image
and publish the ports, then point the suite at them.
1. Build and run the image
docker build -t vnctalk-prosody:test .
docker run -d --name prosody-test \
-e prosodyDomain=example.com \
-e prosodyDBhost=172.17.0.1 \
-e prosodyDBname=prosody \
-e prosodyDBuser=prosody \
-e prosodyDBpass=prosody \
-e hybridaAuthUrl=http://192.168.23.3:80/ \
-e fcmApiKey=asdfghjkl \
-e fcm_api_url=http://fcmp:80/notify \
-e del_api_url=http://fcmp:80/delete \
-e fileShareBaseUrl=https://phpfile.example.com/share.php/ \
-e fileShareSecret=g3h31m \
-e avatarUploadUrl=http://avatar.example.com/ \
-e avatarUploadUser=avatar -e avatarUploadPass=avpw \
-e DEFAULT_JITSI_CONFERENCE=conference.jitsi.test \
-p 5222:5222 -p 5280:5280 -p 5582:5582 \
vnctalk-prosody:test
Env-var names must match the template placeholders (
fcm_api_url,del_api_url, snake_case). The legacytest.shpassed camelCasefcmApiUrl/fcmDelUrl, whichenvsubstsilently leaves unsubstituted — seetest-improvement.md §1.2.run-tests.sh/docker-compose.ymluse the correct names.
2. Expose the test ports
The suite needs C2S (5222), HTTP/BOSH/WS/REST (5280), and admin telnet
(5582). Map them to the host as above. For DB-assertion tests, also make your
PostgreSQL reachable (PG_HOST/PG_PORT).
3. Run the suite
export XMPP_HOST=127.0.0.1 XMPP_PORT=5222
export XMPP_DOMAIN=example.com MUC_DOMAIN=conference.example.com
export XMPP_JID=admin@example.com # an account your auth backend accepts
export XMPP_PASSWORD=<from-your-auth-backend>
export XMPP_JID2=user2@example.com XMPP_PASSWORD2=<...> # distinct account
export REST_URL=http://127.0.0.1:5280/rest
export BOSH_URL=http://127.0.0.1:5280/http-bind
export WS_URL=ws://127.0.0.1:5280/xmpp-websocket
export ADMIN_TELNET_HOST=127.0.0.1 ADMIN_TELNET_PORT=5582
export PG_HOST=127.0.0.1 PG_PORT=5432 PG_DB=prosody PG_USER=prosody PG_PASSWORD=prosody
pytest tests/ -v -c tests/pytest.ini
Without MOCK_URL, the HTTP side-effect tests (test_09) and the
http_upload slot PUT assertion (test_12) are skipped — they need the
capture mock. To exercise them against a standalone container, run the mock
separately and set MOCK_URL:
docker run -d --name mocks -p 8092:8080 \
-e AUTH_USERS="admin@example.com:adminpw,user2@example.com:pw2" \
$(docker build -q tests/mocks)
export MOCK_URL=http://localhost:8092
and point the container's hybridaAuthUrl/fcm_api_url/del_api_url/
avatarUploadUrl/fileShareBaseUrl at the mock's host:port.
Test Coverage
| File | What it checks |
|---|---|
test_01_core.py |
C2S auth, disco features (MAM, carbons), BOSH/WebSocket reachability, REST injection, REST→MAM roundtrip, REST→carbons roundtrip |
test_02_muc.py |
MUC disco, rooms hidden by default, MUC MAM content verification, stanza-id stripping, auto-member on invite, unregister IQ affiliation removal, vdata disco, mod_vcard_muc room vCard get/set |
test_03_vnctalk.py |
vCard fallback, avatar upload trigger, timestamp stamps, receipts, broadcast component, open_host_store smoke test, E2E hints, MUC data broadcast, mod_vnc_muc_hook notification to a non-joined affiliate |
test_04_infra.py |
Admin telnet banner, healthcheck exit-code contract, port reachability |
test_05_patches.py |
Patch behavioral contracts: unique IQ bare-JID error, hidden-by-default negative test, large stanza truncation, self-unavailable routing, offline affiliate broadcast |
test_06_postgres.py |
PostgreSQL schema verification: table existence, MAM/MUC log column checks, REST store-user inversion, mod_vnc_track_kicks real kick-row assertion |
test_07_module_load.py |
Zero-coverage module load detection (smoke hooks for vnc_lastactivity, delfile, remotemucstore, remotemucinvite, track_kicks) |
test_08_image_patches.py |
Patch-application CI test — docker-exec grep of every patched upstream file (MANUAL §4.1) |
test_09_http_sideeffects.py |
HTTP side-effects via mock capture: mod_vnc_fcm (1:1 + MUC), mod_vnc_delfile, mod_vnc_vcard_avatar, mod_vnc_receipts archive |
test_10_smacks.py |
Upstream mod_smacks parity: enable/ack, resume-replay, hibernation-expiry (offline variant xfail until the fork is replaced) |
test_11_smokes.py |
Low-risk smokes: filter_chatstates, idlecompat, http_altconnect, webpresence, admin-telnet non-loopback bind |
test_12_image_runtime.py |
mod_http_upload_external slot handshake + signed-URL PUT, healthcheck exit-2 execution, no-residual-${...} config check |
MANUAL_TESTS.md |
Step-by-step procedures for contracts that require DB inspection, federation, or container internals |
Notes
- Tests that require a live XMPP account are skipped automatically if
--xmpp-jidor--xmpp-passwordare empty (or if--skip-liveis used). - Two distinct accounts (
--xmpp-jidand--xmpp-jid2) are strongly recommended. Tests that share the same bare JID between both clients are markedxfailrather than silently passing. - MUC tests create temporary rooms and attempt to destroy them after each test.
- The REST injection test now performs a roundtrip: inject via REST, query MAM, assert the message appears.
- If the
/restendpoint is protected by a reverse proxy with HTTP Basic Auth, provide--rest-userand--rest-password(orREST_USER/REST_PASSWORDenv vars). TheRESTInjectorhelper sends aBasicAuthorization header when these are configured. TheHost: <xmpp domain>override needed for prosody vhost routing is only sent for direct (IP/localhost) REST URLs; ingress-fronted hostnames rewrite Host themselves and reject a mismatching override (observed as an empty 431 reply). Tune withREST_HOST_HEADER/--rest-host-header(auto/none/verbatim value). - PostgreSQL tests are skipped unless
--pg-hostand--pg-userare provided. With the compose stack PG is always present (published on5434). test_08andtest_12's execution tests rundocker compose execagainst the runningprosodycontainer; they skip when the compose stack is not up.- The healthcheck test now executes
healthcheck.shinside the container with a tampered cert and asserts exit 2 (replacing the earlier source-grep check), plus a no-residual-${...}config-rendering assertion. test_10(SMACKS) needs a lowsmacks_hibernation_time(setSMACKS_HIBERNATION_TIME=10in compose); the expiry/offline tests skip if the configured value is too large to wait for.- The
VNCXmppClienthonorsXMPP_HOST/XMPP_PORTdirectly (slixmpp otherwise does SRV/A resolution on the JID domain, which breaks against a local stack whose domain doesn't resolve to it).