Files
Stefan-Sanger 35161b9b6d fix: resolve 3 in-container test failures in CI compose run
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>
2026-07-15 17:59:12 +02:00
..

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

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_asynchttp://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) and 8092:8080 (mocks) because 5433/8090 are often already taken on developer machines. Adjust the ports: mappings in docker-compose.yml if 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 legacy test.sh passed camelCase fcmApiUrl/fcmDelUrl, which envsubst silently leaves unsubstituted — see test-improvement.md §1.2. run-tests.sh / docker-compose.yml use 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-jid or --xmpp-password are empty (or if --skip-live is used).
  • Two distinct accounts (--xmpp-jid and --xmpp-jid2) are strongly recommended. Tests that share the same bare JID between both clients are marked xfail rather 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 /rest endpoint is protected by a reverse proxy with HTTP Basic Auth, provide --rest-user and --rest-password (or REST_USER / REST_PASSWORD env vars). The RESTInjector helper sends a Basic Authorization header when these are configured. The Host: <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 with REST_HOST_HEADER/--rest-host-header (auto/none/verbatim value).
  • PostgreSQL tests are skipped unless --pg-host and --pg-user are provided. With the compose stack PG is always present (published on 5434).
  • test_08 and test_12's execution tests run docker compose exec against the running prosody container; they skip when the compose stack is not up.
  • The healthcheck test now executes healthcheck.sh inside 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 low smacks_hibernation_time (set SMACKS_HIBERNATION_TIME=10 in compose); the expiry/offline tests skip if the configured value is too large to wait for.
  • The VNCXmppClient honors XMPP_HOST/XMPP_PORT directly (slixmpp otherwise does SRV/A resolution on the JID domain, which breaks against a local stack whose domain doesn't resolve to it).