# 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](https://testcontainers.com/), 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. ```js 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 `Host` header. A module on the MUC component > (`conference.localhost`) would only be reachable at `Host: 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 through > `module: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=` | Return live room state: `{ jid, hidden, occupant_count }` | ```js 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](https://lunarmodules.github.io/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 ```bash 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: 1. `test:lua` — busted unit tests for pure Lua code (skipped gracefully if busted is not installed) 2. `test:integration` — Mocha integration tests against a Docker Prosody instance 3. `test:report` — generates `allure-report/` from both test suites To run a single spec file with container debug output: ```bash DEBUG=testcontainers,testcontainers:containers npm run test:one -- ``` To open the report after a run: ```bash npx allure open allure-report ``` On macOS with Colima the Docker socket is not at the default path. Export it before running: ```bash 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: ```bash 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.