import assert from 'assert'; import { createTestContext } from './helpers/test_context.js'; import { setRoomMaxOccupants } from './helpers/test_observer.js'; import { isAvailablePresence } from './helpers/xmpp_utils.js'; const CONFERENCE = 'conference.localhost'; // Each test uses a unique room name to avoid cross-test state. let _roomCounter = 0; const room = () => `max-occupants-${++_roomCounter}@${CONFERENCE}`; describe('mod_muc_max_occupants', () => { // Prosody config sets muc_max_occupants = 2. let ctx; beforeEach(() => { ctx = createTestContext(); }); afterEach(() => ctx.cleanup()); it('allows join when room is empty', async () => { const r = room(); await ctx.connectFocus(r); const c = await ctx.connect(); const presence = await c.joinRoom(r); assert.ok(isAvailablePresence(presence)); }); it('allows join when room has one occupant (under limit)', async () => { const r = room(); await ctx.connectFocus(r); const c1 = await ctx.connect(); const c2 = await ctx.connect(); await c1.joinRoom(r); const presence = await c2.joinRoom(r); assert.ok(isAvailablePresence(presence)); }); it('blocks join when room is at the limit', async () => { const r = room(); await ctx.connectFocus(r); const c1 = await ctx.connect(); const c2 = await ctx.connect(); const c3 = await ctx.connect(); await c1.joinRoom(r); await c2.joinRoom(r); const presence = await c3.joinRoom(r); assert.equal(presence.attrs.type, 'error'); assert.ok( presence.getChild('error')?.getChild('service-unavailable'), 'expected in error stanza' ); }); it('health check room bypasses the occupant limit', async () => { // mod_muc_meeting_id restricts health-check rooms to focus (jicofo) only, // so we test the occupant-limit bypass with focus clients. // is_healthcheck_room() matches rooms starting with '__jicofo-health-check'. const healthRoom = `__jicofo-health-check-test@${CONFERENCE}`; await ctx.connectFocus(healthRoom); // A second focus join would conflict on nick; the meaningful assertion is // that the first focus client joined without hitting a limit error. assert.ok(true, 'focus joined health-check room without occupant-limit error'); }); describe('whitelist', () => { // Prosody config sets muc_access_whitelist = { "whitelist.localhost", "auth.localhost" }. // Clients created with domain:'whitelist.localhost' get JIDs on that domain // and are treated as whitelisted by mod_muc_max_occupants. it('whitelisted user can join a room that is at the limit', async () => { const r = room(); await ctx.connectFocus(r); const c1 = await ctx.connect(); const c2 = await ctx.connect(); const wl = await ctx.connectWhitelisted(); await c1.joinRoom(r); await c2.joinRoom(r); // room now at limit (2 non-whitelisted) const presence = await wl.joinRoom(r); assert.ok(isAvailablePresence(presence), 'whitelisted user must bypass the occupant limit'); }); it('whitelisted occupants do not count against the limit for non-whitelisted users', async () => { const r = room(); await ctx.connectFocus(r); const wl1 = await ctx.connectWhitelisted(); const wl2 = await ctx.connectWhitelisted(); const c1 = await ctx.connect(); const c2 = await ctx.connect(); const c3 = await ctx.connect(); // Two whitelisted users join — they bypass the check and are not // counted when a non-whitelisted user evaluates available slots. await wl1.joinRoom(r); await wl2.joinRoom(r); // Non-whitelisted users: only they count against the limit of 2. const p1 = await c1.joinRoom(r); assert.ok(isAvailablePresence(p1), '1st non-whitelisted user must be allowed (0 counted occupants so far)'); const p2 = await c2.joinRoom(r); assert.ok(isAvailablePresence(p2), '2nd non-whitelisted user must be allowed (1 counted occupant)'); const p3 = await c3.joinRoom(r); assert.equal(p3.attrs.type, 'error', '3rd non-whitelisted user must be blocked (2 counted occupants = limit reached)'); assert.ok( p3.getChild('error')?.getChild('service-unavailable'), 'expected in error stanza' ); }); }); describe('per-room limit', () => { // room._data.max_occupants overrides the global muc_max_occupants (2) // for an individual room. The HTTP helper sets it after room creation. it('per-room limit higher than global allows more occupants', async () => { const r = room(); await ctx.connectFocus(r); const c1 = await ctx.connect(); await c1.joinRoom(r); // Override the global limit of 2 with a per-room limit of 4. await setRoomMaxOccupants(r, 4); const c2 = await ctx.connect(); const c3 = await ctx.connect(); const c4 = await ctx.connect(); const c5 = await ctx.connect(); const p2 = await c2.joinRoom(r); assert.ok(isAvailablePresence(p2), 'user 2 should join (limit 4)'); const p3 = await c3.joinRoom(r); assert.ok(isAvailablePresence(p3), 'user 3 should join (global limit 2 would block, per-room limit 4 allows)'); const p4 = await c4.joinRoom(r); assert.ok(isAvailablePresence(p4), 'user 4 should join (limit 4)'); const p5 = await c5.joinRoom(r); assert.equal(p5.attrs.type, 'error', 'user 5 must be blocked (at per-room limit 4)'); assert.ok( p5.getChild('error')?.getChild('service-unavailable'), 'expected in error stanza' ); }); it('per-room limit lower than global restricts the room further', async () => { const r = room(); await ctx.connectFocus(r); const c1 = await ctx.connect(); await c1.joinRoom(r); // Override the global limit of 2 with a stricter per-room limit of 1. await setRoomMaxOccupants(r, 1); const c2 = await ctx.connect(); const presence = await c2.joinRoom(r); assert.equal(presence.attrs.type, 'error', 'user 2 must be blocked by the per-room limit of 1'); assert.ok( presence.getChild('error')?.getChild('service-unavailable'), 'expected in error stanza' ); }); }); });