Files
upstream-jitsi-meet/tests/prosody/mod_system_chat_message_spec.js
upstream-mirror 1d2bfcac0c
Some checks failed
Prosody Plugin Tests / test (push) Failing after 10m11s
Close stale issues and PRs / stale (push) Failing after 1s
snapshot of https://github.com/jitsi/jitsi-meet.git @ fd1749dd28f4138aa80234fc7f311b19aaffa0e7 (2026-09-15)
2026-09-15 15:56:32 +05:00

337 lines
12 KiB
JavaScript

import assert from 'assert';
import { mintAsapToken, mintSystemToken } from './helpers/jwt.js';
import { sendSystemChatMessage } from './helpers/test_observer.js';
import { createXmppClient, joinWithFocus } from './helpers/xmpp_client.js';
const CONFERENCE = 'conference.localhost';
let _roomCounter = 0;
const room = () => `system-chat-${++_roomCounter}@${CONFERENCE}`;
// ─── Helpers ─────────────────────────────────────────────────────────────────
/**
* Creates a room with a focus participant.
*
* @returns {Promise<{roomJid: string, focus: object}>}
*/
async function createRoom() {
const roomJid = room();
const focus = await joinWithFocus(roomJid);
return { roomJid,
focus };
}
/**
* Disconnects all provided clients.
*
* @param {...object} clients - Clients to disconnect.
* @returns {Promise<void>}
*/
async function disconnectAll(...clients) {
await Promise.all(clients.map(c => c.disconnect()));
}
// ─── Tests ────────────────────────────────────────────────────────────────────
describe('mod_system_chat_message', () => {
// ── Authentication ───────────────────────────────────────────────────────
//
// mod_system_chat_message uses prosody_password_public_key_repo_url as its
// key server — a separate key pair from login tokens (asap_key_server).
// Auth failures return 401.
describe('authentication', () => {
it('returns 401 when Authorization header is absent', async () => {
const { roomJid, focus } = await createRoom();
try {
const { status } = await sendSystemChatMessage(
roomJid, [ focus.jid ], 'hello', null, { omitAuth: true });
assert.strictEqual(status, 401);
} finally {
await disconnectAll(focus);
}
});
it('returns 401 when a login token is used instead of a system token', async () => {
const { roomJid, focus } = await createRoom();
try {
const loginToken = mintAsapToken({ room: '*' });
const { status } = await sendSystemChatMessage(
roomJid, [ focus.jid ], 'hello', loginToken);
assert.strictEqual(status, 401,
'login token must be rejected by the system key server');
} finally {
await disconnectAll(focus);
}
});
it('returns 401 when token is expired', async () => {
const { roomJid, focus } = await createRoom();
try {
const token = mintSystemToken({}, { expired: true });
const { status } = await sendSystemChatMessage(
roomJid, [ focus.jid ], 'hello', token);
assert.strictEqual(status, 401);
} finally {
await disconnectAll(focus);
}
});
});
// ── Parameter validation ─────────────────────────────────────────────────
describe('parameter validation', () => {
it('returns 400 when Content-Type is not application/json', async () => {
const token = mintSystemToken();
const res = await fetch(
'http://localhost:5280/send-system-chat-message',
{
method: 'POST',
headers: {
'Content-Type': 'text/plain',
Authorization: `Bearer ${token}`
},
body: '{"room":"r@conference.localhost","connectionJIDs":[],"message":"hi"}'
}
);
assert.strictEqual(res.status, 400);
});
it('returns 400 when body is empty', async () => {
const token = mintSystemToken();
const res = await fetch(
'http://localhost:5280/send-system-chat-message',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: ''
}
);
assert.strictEqual(res.status, 400);
});
it('returns 400 when message is missing', async () => {
const { roomJid, focus } = await createRoom();
try {
const token = mintSystemToken();
const res = await fetch(
'http://localhost:5280/send-system-chat-message',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({ room: roomJid,
connectionJIDs: [ focus.jid ] })
}
);
assert.strictEqual(res.status, 400);
} finally {
await disconnectAll(focus);
}
});
it('returns 400 when connectionJIDs is missing', async () => {
const { roomJid, focus } = await createRoom();
try {
const token = mintSystemToken();
const res = await fetch(
'http://localhost:5280/send-system-chat-message',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({ room: roomJid,
message: 'hi' })
}
);
assert.strictEqual(res.status, 400);
} finally {
await disconnectAll(focus);
}
});
it('returns 400 when room is missing', async () => {
const token = mintSystemToken();
const res = await fetch(
'http://localhost:5280/send-system-chat-message',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({ connectionJIDs: [],
message: 'hi' })
}
);
assert.strictEqual(res.status, 400);
});
});
// ── Room lookup ──────────────────────────────────────────────────────────
describe('room lookup', () => {
it('returns 404 when room does not exist', async () => {
const token = mintSystemToken();
const { status } = await sendSystemChatMessage(
`nonexistent@${CONFERENCE}`, [], 'hi', token);
assert.strictEqual(status, 404);
});
});
// ── Message delivery ─────────────────────────────────────────────────────
describe('message delivery', () => {
// Filter that matches only system_chat_message payloads, skipping
// metadata broadcasts that arrive on the same json-message namespace.
const isSystemChat = m => {
const el = m.getChild('json-message', 'http://jitsi.org/jitmeet');
if (!el) {
return false;
}
try {
return JSON.parse(el.text()).type === 'system_chat_message';
} catch {
return false;
}
};
it('returns 200 and delivers the message to the recipient', async () => {
const { roomJid, focus } = await createRoom();
const user = await createXmppClient();
await user.joinRoom(roomJid);
try {
const token = mintSystemToken();
const text = 'Hello from system';
const { status } = await sendSystemChatMessage(
roomJid, [ user.jid ], text, token);
assert.strictEqual(status, 200);
const msg = await user.waitForMessage(isSystemChat);
const jsonMessage = msg.getChild('json-message', 'http://jitsi.org/jitmeet');
assert.ok(jsonMessage, 'message must contain a json-message element');
const payload = JSON.parse(jsonMessage.text());
assert.strictEqual(payload.type, 'system_chat_message');
assert.strictEqual(payload.message, text);
} finally {
await disconnectAll(focus, user);
}
});
it('includes displayName in the payload when provided', async () => {
const { roomJid, focus } = await createRoom();
const user = await createXmppClient();
await user.joinRoom(roomJid);
try {
const token = mintSystemToken();
const { status } = await sendSystemChatMessage(
roomJid, [ user.jid ], 'hi', token, { displayName: 'System' });
assert.strictEqual(status, 200);
const msg = await user.waitForMessage(isSystemChat);
const payload = JSON.parse(
msg.getChild('json-message', 'http://jitsi.org/jitmeet').text());
assert.strictEqual(payload.displayName, 'System');
} finally {
await disconnectAll(focus, user);
}
});
it('returns 200 when connectionJIDs contains a JID not in the room (no occupancy validation)', async () => {
// mod_system_chat_message routes stanzas to every JID in
// connectionJIDs without verifying they are room occupants.
// This test documents the current behaviour: the call succeeds
// (200) even for non-occupant JIDs.
const { roomJid, focus } = await createRoom();
try {
const token = mintSystemToken();
const nonOccupant = 'nobody@localhost/ghost-resource';
const { status } = await sendSystemChatMessage(
roomJid, [ nonOccupant ], 'hello', token);
assert.strictEqual(status, 200,
'module must not crash when connectionJIDs contains a non-occupant JID');
} finally {
await disconnectAll(focus);
}
});
it('delivers to multiple recipients', async () => {
const { roomJid, focus } = await createRoom();
const alice = await createXmppClient();
const bob = await createXmppClient();
await alice.joinRoom(roomJid);
await bob.joinRoom(roomJid);
try {
const token = mintSystemToken();
const { status } = await sendSystemChatMessage(
roomJid, [ alice.jid, bob.jid ], 'broadcast', token);
assert.strictEqual(status, 200);
const [ msgA, msgB ] = await Promise.all([
alice.waitForMessage(isSystemChat),
bob.waitForMessage(isSystemChat)
]);
for (const msg of [ msgA, msgB ]) {
const payload = JSON.parse(
msg.getChild('json-message', 'http://jitsi.org/jitmeet').text());
assert.strictEqual(payload.message, 'broadcast');
}
} finally {
await disconnectAll(focus, alice, bob);
}
});
});
});