8.3 KiB
Prosody Plugin Integration Tests
Integration tests for the Jitsi Prosody plugins. Tests run against a real Prosody 13 instance inside Docker and connect to it over XMPP WebSocket and HTTP.
Architecture and Lifecycle
One Docker container runs for the entire test suite (not per-test). The container is started
by the Mocha global setup hook (setup.js) using testcontainers,
and torn down after all tests complete.
npm test
└─ Mocha (setup.js beforeAll)
└─ Docker container: Prosody 13
└─ HTTP :5280 ← test clients connect via WebSocket (/xmpp-websocket)
← test assertions query HTTP endpoints here
Test clients are anonymous XMPP connections (@xmpp/client). Rooms are created by joining
them; they are automatically destroyed by Prosody when all occupants disconnect.
Because rooms live only as long as their occupants, tests are effectively isolated as long as
each test disconnects its clients in a finally block. Room names are unique per test (counter
suffix) so there is no cross-test interference.
Assertion Strategies
Two complementary mechanisms are available for asserting internal Prosody state.
Option I — prosodyctl shell (black-box runtime control)
helpers/prosody_shell.js runs Lua expressions inside the live Prosody process via
prosodyctl shell (executed through docker exec). Use this when you need to:
- Load or unload a module at runtime to test behaviour with/without it.
- Execute arbitrary Lua to inspect or mutate server state.
import { prosodyShell } from './helpers/prosody_shell.js';
await prosodyShell('module:unload("muc_hide_all", "conference.localhost")');
// ... run test ...
await prosodyShell('module:load("muc_hide_all", "conference.localhost")');
Option II — mod_test_observer HTTP endpoints (event log + room state)
mod_test_observer.lua (loaded on the MUC component) hooks MUC events and stores them in a
Lua shared table. mod_test_observer_http.lua (loaded on the main VirtualHost) serves that
shared state over plain HTTP on port 5280.
Why two modules? Prosody routes HTTP by
Hostheader. A module on the MUC component (conference.localhost) would only be reachable atHost: conference.localhost, which is not resolvable from the host machine. Loading the HTTP server on the main VirtualHost (localhost) makes port 5280 directly reachable. The two modules share data throughmodule:shared().
helpers/test_observer.js wraps the three endpoints:
| Endpoint | Purpose |
|---|---|
GET /test-observer/events |
Return all MUC events fired since the last clear |
DELETE /test-observer/events |
Clear the event log (call in beforeEach) |
GET /test-observer/rooms?jid=<room-jid> |
Return live room state: { jid, hidden, occupant_count } |
import { clearEvents, getEvents, getRoomState } from './helpers/test_observer.js';
beforeEach(() => clearEvents());
// Check fired events:
const events = await getEvents();
assert.ok(events.map(e => e.event).includes('muc-room-pre-create'));
// Check internal room state:
const state = await getRoomState('room@conference.localhost');
assert.equal(state.hidden, true);
Test Placement Guidelines
Pure Lua code (no Prosody API needed) — test in lua/ using
busted. These run without Docker and
are fast. A good fit for library files (e.g. token/jwk.lib.lua) and any
module logic that can be extracted into plain functions.
Code that requires Prosody stubs or live server behaviour — test via the
Docker/JS path (Mocha + *_spec.js). This covers anything that calls
module:hook, module:shared, prosody.hosts, MUC APIs, etc.
Prefer integration over isolation. When adding a new module under test,
load it on the main VirtualHost or MUC component in
docker/prosody.cfg.lua rather than testing it in isolation with stubs.
This exercises real module interactions (hooks fire in the correct order,
shared state is visible across modules) and catches interoperability bugs that
unit tests miss.
Directory Structure
tests/prosody/
├── README.md this file
├── setup.js Mocha root hooks: start/stop Docker container
├── .mocharc.cjs Mocha configuration (spec glob, timeout, reporters)
├── mocha-reporter-config.json Multi-reporter config: spec + allure-mocha
├── package.json Node dependencies (mocha, testcontainers, @xmpp/client, allure)
│
├── docker/
│ ├── Dockerfile Prosody 13 image with lua-cjson + Jitsi plugins
│ ├── docker-compose.yml Exposes :5222 (XMPP) and :5280 (HTTP)
│ ├── prosody.cfg.lua Top-level config: Includes the cfg/ fragments below
│ └── cfg/ Config fragments (see prosody.cfg.lua header)
│ ├── global.cfg.lua Global settings: no TLS, anonymous auth, HTTP on :5280
│ ├── virtualhosts.cfg.lua All VirtualHost blocks
│ ├── components.cfg.lua All Component blocks except conference.localhost
│ └── conference.cfg.lua conference.localhost (separate so extending images can re-declare it)
│
├── helpers/
│ ├── container.js Holds the testcontainers container reference (set in setup.js)
│ ├── xmpp_client.js Creates anonymous XMPP clients with joinRoom / sendDiscoInfo
│ ├── prosody_shell.js Runs prosodyctl shell commands inside the container (Option I)
│ └── test_observer.js HTTP client for mod_test_observer endpoints (Option II)
│
├── lua/
│ └── jwk_spec.lua busted unit tests for token/jwk.lib.lua (no Prosody needed)
│
├── mod_conference_duration_spec.js Tests for mod_conference_duration
├── mod_muc_filter_access_spec.js Tests for mod_muc_filter_access
├── mod_muc_hide_all_spec.js Tests for mod_muc_hide_all
├── mod_muc_max_occupants_spec.js Tests for mod_muc_max_occupants
├── mod_muc_meeting_id_spec.js Tests for mod_muc_meeting_id
└── mod_muc_size_spec.js Tests for mod_muc_size
The plugin sources under test (mod_muc_hide_all.lua, mod_muc_max_occupants.lua, etc.) live
in resources/prosody-plugins/ and are copied into the Docker image at build time. The test
observer modules (mod_test_observer.lua, mod_test_observer_http.lua) are test-only and
should never be loaded in production.
Running
cd tests/prosody
npm install # first time only
npm test # runs Lua unit tests, integration tests, and generates Allure report
npm test runs three steps in sequence:
test:lua— busted unit tests for pure Lua code (skipped gracefully if busted is not installed)test:integration— Mocha integration tests against a Docker Prosody instancetest:report— generatesallure-report/from both test suites
To run a single spec file with container debug output:
DEBUG=testcontainers,testcontainers:containers npm run test:one -- <spec-file>
To open the report after a run:
npx allure open allure-report
On macOS with Colima the Docker socket is not at the default path. Export it before running:
export DOCKER_HOST=unix:///Users/$(whoami)/.colima/default/docker.sock
export TESTCONTAINERS_RYUK_DISABLED=true # Ryuk reaper fails under Colima
npm test
Extending from another repo
Repos with private Prosody plugins (e.g. jitsi-meet-branding) can reuse this setup by building on top of the test image:
npm run build:test-image # builds jitsi-meet-prosody-test:latest
The extending image starts FROM jitsi-meet-prosody-test:latest, copies its
plugins into an additional plugin path, and replaces
/etc/prosody/prosody.cfg.lua with a config that Includes the baked-in
/etc/prosody/cfg/global.cfg.lua, virtualhosts.cfg.lua and
components.cfg.lua fragments, then re-declares conference.localhost
(based on cfg/conference.cfg.lua) with the extra modules appended.
Prosody Component blocks cannot be extended in place, which is why
conference.localhost lives in its own fragment that extending configs
replace wholesale.