import assert from 'assert'; import { mintAsapToken } from './helpers/jwt.js'; import { setHideDisplayNameForGuests, setRoomMaxOccupants } from './helpers/test_observer.js'; import { createXmppClient, joinWithFocus } from './helpers/xmpp_client.js'; const CONFERENCE = 'conference.localhost'; const NICK_NS = 'http://jabber.org/protocol/nick'; const DISPLAY_NAME_NS = 'http://jitsi.org/protocol/display-name'; let _counter = 0; const nextRoom = () => `displayname-${++_counter}@${CONFERENCE}`; // ─── Helpers ────────────────────────────────────────────────────────────────── /** * Connects a client on the main VirtualHost with a given token context. * Always passes ?room= so that session.jitsi_web_query_room is set — * required by the hideDisplayNameForGuests outbound filter. * * @param {string} roomJid * @param {object} [contextUser] JWT context.user claims (e.g. { name: 'Alice' }) * @param {object} [features] JWT context.features claims */ function connect(roomJid, { contextUser, features } = {}) { const roomName = roomJid.split('@')[0]; const context = {}; if (contextUser !== undefined) { context.user = contextUser; } if (features !== undefined) { context.features = features; } const token = mintAsapToken({ room: roomName, context }); return createXmppClient({ params: { room: roomName, token } }); } const connectModerator = roomJid => connect(roomJid, { contextUser: { moderator: true } }); /** * Waits for a non-error, non-unavailable presence from a specific occupant nick. * Skips any presences already queued from earlier occupants. * * @param {object} client observer client * @param {string} roomJid * @param {string} nick MUC nick of the occupant to wait for */ function waitForOtherPresence(client, roomJid, nick) { return client.waitForPresence( s => s.attrs.from === `${roomJid}/${nick}` && !s.attrs.type, 5000 ); } // ─── name-readonly ──────────────────────────────────────────────────────────── describe('mod_muc_displayname — name-readonly', () => { const clients = []; afterEach(async () => { await Promise.all(clients.map(c => c.disconnect())); clients.length = 0; }); it('enforces token name: observer sees token name even when sender sent a different nick', async () => { const r = nextRoom(); const focus = await joinWithFocus(r); const observer = await createXmppClient({ params: { room: r.split('@')[0] } }); clients.push(focus, observer); await observer.joinRoom(r); // Alice connects with name-readonly=true and name="Alice" in the token. const alice = await connect(r, { contextUser: { name: 'Alice' }, features: { 'name-readonly': true } }); clients.push(alice); // Alice joins with a different display name — the inbound filter must replace it. await alice.joinRoom(r, undefined, { displayName: 'WrongName' }); // The observer sees Alice's join presence; nick must be "Alice" (from token). const presence = await waitForOtherPresence(observer, r, alice.nick); const nick = presence.getChild('nick', NICK_NS); assert.ok(nick, 'presence must contain a element'); assert.strictEqual(nick.getText(), 'Alice', 'nick must be the token name, not the client-supplied name'); }); it('strips nick when name-readonly=true and token has no name', async () => { const r = nextRoom(); const focus = await joinWithFocus(r); const observer = await createXmppClient({ params: { room: r.split('@')[0] } }); clients.push(focus, observer); await observer.joinRoom(r); // No context.user.name — name-readonly with no name means nick is stripped. const alice = await connect(r, { features: { 'name-readonly': true } }); clients.push(alice); await alice.joinRoom(r, undefined, { displayName: 'SomeName' }); const presence = await waitForOtherPresence(observer, r, alice.nick); assert.ok( !presence.getChild('nick', NICK_NS), 'nick element must be absent when name-readonly and token has no name' ); }); it('passes nick unchanged when name-readonly is not set', async () => { const r = nextRoom(); const focus = await joinWithFocus(r); const observer = await createXmppClient({ params: { room: r.split('@')[0] } }); clients.push(focus, observer); await observer.joinRoom(r); // No name-readonly feature — client's own nick must be preserved. const alice = await connect(r, { contextUser: { name: 'Alice' } }); clients.push(alice); await alice.joinRoom(r, undefined, { displayName: 'ChosenName' }); const presence = await waitForOtherPresence(observer, r, alice.nick); const nick = presence.getChild('nick', NICK_NS); assert.ok(nick, 'presence must contain a element'); assert.strictEqual(nick.getText(), 'ChosenName', 'nick must be the client-supplied name when name-readonly is not set'); }); }); // ─── hideDisplayNameForGuests ───────────────────────────────────────────────── describe('mod_muc_displayname — hideDisplayNameForGuests', () => { const clients = []; afterEach(async () => { await Promise.all(clients.map(c => c.disconnect())); clients.length = 0; }); it('non-moderator recipient receives identity-stripped presence', async () => { const r = nextRoom(); const roomName = r.split('@')[0]; const focus = await joinWithFocus(r); await setRoomMaxOccupants(r, 10); // Non-moderator guest — must receive filtered presences. const guest = await createXmppClient({ params: { room: roomName } }); // Moderator — must receive full presences. const mod = await connectModerator(r); clients.push(focus, guest, mod); await guest.joinRoom(r); await mod.joinRoom(r); await setHideDisplayNameForGuests(r, true); // Alice joins with a display name after the flag is set. const alice = await connect(r, { contextUser: { name: 'Alice' } }); clients.push(alice); await alice.joinRoom(r, undefined, { displayName: 'Alice' }); // Guest sees Alice's join presence — identity must be stripped. const guestPresence = await waitForOtherPresence(guest, r, alice.nick); assert.ok( !guestPresence.getChild('nick', NICK_NS), 'non-moderator must not see when hideDisplayNameForGuests is true' ); }); it('moderator recipient receives full presence with identity intact', async () => { const r = nextRoom(); const roomName = r.split('@')[0]; const focus = await joinWithFocus(r); await setRoomMaxOccupants(r, 10); const guest = await createXmppClient({ params: { room: roomName } }); const mod = await connectModerator(r); clients.push(focus, guest, mod); await guest.joinRoom(r); await mod.joinRoom(r); await setHideDisplayNameForGuests(r, true); const alice = await connect(r, { contextUser: { name: 'Alice' } }); clients.push(alice); await alice.joinRoom(r, undefined, { displayName: 'Alice' }); // Drain guest's copy; moderator's copy is what we assert on. await waitForOtherPresence(guest, r, alice.nick); // Mod sees Alice's join presence — identity must be intact. const modPresence = await waitForOtherPresence(mod, r, alice.nick); const nick = modPresence.getChild('nick', NICK_NS); assert.ok(nick, 'moderator must see when hideDisplayNameForGuests is true'); assert.strictEqual(nick.getText(), 'Alice'); }); it('non-moderator receives full presence when hideDisplayNameForGuests is false', async () => { const r = nextRoom(); const roomName = r.split('@')[0]; const focus = await joinWithFocus(r); const guest = await createXmppClient({ params: { room: roomName } }); clients.push(focus, guest); await guest.joinRoom(r); // Flag is not set — guest must receive full presence. const alice = await connect(r, { contextUser: { name: 'Alice' } }); clients.push(alice); await alice.joinRoom(r, undefined, { displayName: 'Alice' }); const presence = await waitForOtherPresence(guest, r, alice.nick); const nick = presence.getChild('nick', NICK_NS); assert.ok(nick, 'guest must see when hideDisplayNameForGuests is false'); assert.strictEqual(nick.getText(), 'Alice'); }); }); // ─── message display-name handling ─────────────────────────────────────────── describe('mod_muc_displayname — message display-name handling', () => { const clients = []; afterEach(async () => { await Promise.all(clients.map(c => c.disconnect())); clients.length = 0; }); it('live broadcast strips and from groupchat messages', async () => { const r = nextRoom(); const roomName = r.split('@')[0]; const focus = await joinWithFocus(r); // Alice is the sender; Bob observes live delivery. const alice = await connect(r, { contextUser: { name: 'Alice' } }); const bob = await createXmppClient({ params: { room: roomName } }); clients.push(focus, alice, bob); await alice.joinRoom(r, undefined, { displayName: 'Alice' }); await bob.joinRoom(r); await alice.sendGroupchat(r, 'hello'); // Bob receives the live broadcast; it must not contain nick or display-name. const msg = await bob.waitForMessage( s => s.attrs.type === 'groupchat' && s.getChildText('body') === 'hello' ); assert.ok(!msg.getChild('nick', NICK_NS), 'live message must not contain '); assert.ok(!msg.getChild('display-name', DISPLAY_NAME_NS), 'live message must not contain '); }); it('history message contains injected from token name', async () => { const r = nextRoom(); const focus = await joinWithFocus(r); // Alice has a token name; her message must be stored in history with display-name. const alice = await connect(r, { contextUser: { name: 'Alice', id: 'alice-id' } }); clients.push(focus, alice); await alice.joinRoom(r); await alice.sendGroupchat(r, 'history-msg'); // Bob joins after the message — he receives it as history (with ). const bob = await createXmppClient({ params: { room: r.split('@')[0] } }); clients.push(bob); await bob.joinRoom(r); const historyMsg = await bob.waitForMessage( s => s.attrs.type === 'groupchat' && s.getChildText('body') === 'history-msg' && s.getChild('delay') !== undefined ); const displayName = historyMsg.getChild('display-name', DISPLAY_NAME_NS); assert.ok(displayName, 'history message must contain '); assert.strictEqual(displayName.getText(), 'Alice'); assert.strictEqual(displayName.attrs.source, 'token'); }); it('history message contains from nick for anonymous sender', async () => { const r = nextRoom(); const focus = await joinWithFocus(r); // Anonymous sender — display name comes from their element, not token. const anon = await createXmppClient({ params: { room: r.split('@')[0] } }); clients.push(focus, anon); await anon.joinRoom(r, undefined, { displayName: 'GuestUser' }); await anon.sendGroupchat(r, 'anon-msg'); const bob = await createXmppClient({ params: { room: r.split('@')[0] } }); clients.push(bob); await bob.joinRoom(r); const historyMsg = await bob.waitForMessage( s => s.attrs.type === 'groupchat' && s.getChildText('body') === 'anon-msg' && s.getChild('delay') !== undefined ); const displayName = historyMsg.getChild('display-name', DISPLAY_NAME_NS); assert.ok(displayName, 'history message must contain '); assert.strictEqual(displayName.getText(), 'GuestUser'); assert.strictEqual(displayName.attrs.source, 'guest'); }); });