Files
upstream-jitsi-meet/tests/prosody/helpers/test_observer.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

497 lines
16 KiB
JavaScript

const BASE = 'http://localhost:5280/test-observer';
const ACCESS_MANAGER_URL = `${BASE}/access-manager`;
const END_MEETING_URL = 'http://localhost:5280/end-meeting';
const KICK_PARTICIPANT_URL = 'http://localhost:5280/kick-participant';
const SYSTEM_CHAT_URL = 'http://localhost:5280/send-system-chat-message';
const JIGASI_INVITE_URL = 'http://localhost:5280/invite-jigasi';
/**
* Configures the mock access manager endpoint (served by mod_test_observer_http).
* mod_muc_auth_ban calls this endpoint for VPaaS sessions
* (jitsi_web_query_prefix starting with "vpaas-magic-cookie-").
*
* Call this before connecting the client under test.
* Call with { access: true, status: 200 } (the defaults) to reset between tests.
*
* @param {object} [opts]
* @param {boolean} [opts.access=true] true → allow, false → ban
* @param {number} [opts.status=200] HTTP status code to return.
* Non-200 values simulate HTTP errors;
* mod_muc_auth_ban fails open on errors.
* @param {boolean} [opts.nonJson=false] When true, return a 200 with a non-JSON
* body. Tests the nil-check bug:
* json.decode returns nil and the module
* must not crash on r['access'].
*/
export async function setAccessManagerResponse({ access = true, status = 200, nonJson = false } = {}) {
const res = await fetch(ACCESS_MANAGER_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
access,
status,
// eslint-disable-next-line camelcase
non_json: nonJson
})
});
if (res.status !== 204) {
throw new Error(`setAccessManagerResponse failed: ${res.status} ${await res.text()}`);
}
}
/**
* Returns all MUC events recorded by mod_test_observer since the last clear.
* @returns {Promise<Array<{event: string, room?: string, occupant?: string, timestamp: number}>>}
*/
export async function getEvents() {
const res = await fetch(`${BASE}/events`);
if (!res.ok) {
throw new Error(`GET /events failed: ${res.status}`);
}
return res.json();
}
/**
* Clears the recorded event list.
*/
export async function clearEvents() {
const res = await fetch(`${BASE}/events`, { method: 'DELETE' });
if (res.status !== 204) {
throw new Error(`DELETE /events failed: ${res.status}`);
}
}
/**
* Sets room._data.hideDisplayNameForGuests for mod_muc_displayname tests.
* The room must already exist (at least one occupant).
*
* @param {string} roomJid e.g. 'room@conference.localhost'
* @param {boolean} hidden true to enable filtering; false to disable
*/
export async function setHideDisplayNameForGuests(roomJid, hidden) {
const res = await fetch(`${BASE}/rooms/hide-display-name`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jid: roomJid,
hidden })
});
if (!res.ok) {
throw new Error(`setHideDisplayNameForGuests failed: ${res.status} ${await res.text()}`);
}
}
/**
* Sets room._data.breakout_rooms_active for mod_muc_cleanup_backend_services tests.
* When active is true, the module skips the destroy-timer logic even if only
* backend services remain. The room must already exist (at least one occupant).
*
* @param {string} roomJid e.g. 'room@conference.localhost'
* @param {boolean} active true to simulate active breakout rooms; false to clear
*/
export async function setBreakoutRoomsActive(roomJid, active) {
const res = await fetch(`${BASE}/rooms/breakout-rooms-active`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jid: roomJid,
active })
});
if (!res.ok) {
throw new Error(`setBreakoutRoomsActive failed: ${res.status} ${await res.text()}`);
}
}
/**
* Sets the per-room max_occupants limit for an existing room.
* The room must already exist (at least one occupant).
* This overrides the global muc_max_occupants for this room only.
*
* @param {string} roomJid e.g. 'room@conference.localhost'
* @param {number} max new occupant limit
*/
export async function setRoomMaxOccupants(roomJid, max) {
const res = await fetch(`${BASE}/rooms/max-occupants`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jid: roomJid,
// eslint-disable-next-line camelcase
max_occupants: max
})
});
if (!res.ok) {
throw new Error(`setRoomMaxOccupants failed: ${res.status} ${await res.text()}`);
}
}
/**
* Returns the live jitsi_meet_context_features for a session.
* Unlike session-info (which snapshots at resource-bind), this reads the current
* value so it reflects any updates made after join (e.g. by mod_jitsi_permissions).
*
* @param {string} fullJid e.g. 'abc123@localhost/res1'
* @returns {Promise<object|null>} feature map, or null when none are set
*/
export async function getSessionFeatures(fullJid) {
const res = await fetch(`${BASE}/sessions/features?jid=${encodeURIComponent(fullJid)}`);
if (!res.ok) {
throw new Error(`getSessionFeatures failed: ${res.status} ${await res.text()}`);
}
return (await res.json()).features;
}
/**
* Sets jitsi_meet_context_user and jitsi_meet_context_features on an active c2s
* session identified by full JID. Allows tests to simulate JWT token context
* without running a real token-auth module.
*
* @param {string} fullJid e.g. 'abc123@localhost/res1'
* @param {string} userId value for jitsi_meet_context_user.id
* @param {object} features key/value feature flags, e.g. { flip: true }
*/
export async function setSessionContext(fullJid, userId, features = {}) {
const res = await fetch(`${BASE}/sessions/context`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jid: fullJid,
// eslint-disable-next-line camelcase
user_id: userId,
features })
});
if (!res.ok) {
throw new Error(`setSessionContext failed: ${res.status} ${await res.text()}`);
}
}
/**
* Returns mod_muc_flip's per-room participant tracking state.
*
* @param {string} roomJid e.g. 'room@conference.localhost'
* @returns {Promise<{participants_details: object, kicked_participant_nick?: string, flip_participant_nick?: string}>}
*/
export async function getRoomParticipants(roomJid) {
const res = await fetch(`${BASE}/rooms/participants?jid=${encodeURIComponent(roomJid)}`);
if (res.status === 404) {
return null;
}
if (!res.ok) {
throw new Error(`getRoomParticipants failed: ${res.status}`);
}
return res.json();
}
/**
* Enables or disables the lobby for the given room.
*
* @param {string} roomJid e.g. 'room@conference.localhost'
* @param {boolean} enabled
* @returns {Promise<{ok: boolean, lobbyroom: string|null}>}
*/
async function setLobby(roomJid, enabled) {
const res = await fetch(`${BASE}/rooms/lobby`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jid: roomJid,
enabled
})
});
if (!res.ok) {
throw new Error(`setLobby(${enabled}) failed: ${res.status} ${await res.text()}`);
}
return res.json();
}
/**
* Enables the lobby for the given room.
* Returns the lobby room JID.
*
* @param {string} roomJid e.g. 'room@conference.localhost'
* @returns {Promise<string>} lobby room JID
*/
export async function enableLobby(roomJid) {
const data = await setLobby(roomJid, true);
return data.lobbyroom;
}
/**
* Disables the lobby for the given room and unsets members-only.
*
* @param {string} roomJid e.g. 'room@conference.localhost'
*/
export async function disableLobby(roomJid) {
await setLobby(roomJid, false);
}
/**
* Sets the affiliation of a JID in a room.
*
* @param {string} roomJid e.g. 'room@conference.localhost'
* @param {string} occupantJid bare JID of the user, e.g. 'user@localhost'
* @param {string} affiliation e.g. 'member', 'owner', 'none'
*/
export async function setAffiliation(roomJid, occupantJid, affiliation) {
const res = await fetch(`${BASE}/rooms/affiliation`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jid: roomJid,
// eslint-disable-next-line camelcase
occupant_jid: occupantJid,
affiliation
})
});
if (!res.ok) {
throw new Error(`setAffiliation failed: ${res.status} ${await res.text()}`);
}
}
/**
* Returns the jitsiMetadata table for a room as seen by Prosody.
* Useful for asserting server-side state after metadata updates.
*
* Returns null if the room does not exist.
* Throws if the server cannot encode the metadata (indicates corrupt state).
*
* @param {string} roomJid e.g. 'room@conference.localhost'
* @returns {Promise<{jid: string, metadata: object}|null>}
*/
export async function getRoomMetadata(roomJid) {
const res = await fetch(`${BASE}/rooms/metadata?jid=${encodeURIComponent(roomJid)}`);
if (res.status === 404) {
return null;
}
if (!res.ok) {
throw new Error(`GET /rooms/metadata failed: ${res.status} ${await res.text()}`);
}
return res.json();
}
/**
* Returns the aggregated live-translation request map stored on room._data by
* mod_audio_translation_component. Reads server state directly, so it is robust
* against broadcast de-duplication (no broadcast fires when the aggregate is
* unchanged).
*
* Returns null if the room does not exist; the map is undefined when there are
* no subscriptions.
*
* @param {string} roomJid e.g. 'room@conference.localhost'
* @returns {Promise<{jid: string, audioTranslationRequests?: object}|null>}
*/
export async function getAudioTranslationRequests(roomJid) {
const res = await fetch(
`${BASE}/rooms/audio-translation-requests?jid=${encodeURIComponent(roomJid)}`);
if (res.status === 404) {
return null;
}
if (!res.ok) {
throw new Error(`GET /rooms/audio-translation-requests failed: ${res.status} ${await res.text()}`);
}
return res.json();
}
/**
* Returns every OTLP ExportTraceServiceRequest body received by the mock
* receiver (mod_test_observer_http) so far. Used by mod_trace tests.
* @returns {Promise<Array<object>>}
*/
export async function getOtlpTraces() {
const res = await fetch(`${BASE}/otlp-traces`);
if (!res.ok) {
throw new Error(`GET /otlp-traces failed: ${res.status}`);
}
return res.json();
}
/**
* Clears the recorded OTLP export list.
*/
export async function clearOtlpTraces() {
const res = await fetch(`${BASE}/otlp-traces`, { method: 'DELETE' });
if (res.status !== 204) {
throw new Error(`DELETE /otlp-traces failed: ${res.status}`);
}
}
/**
* Returns room state from Prosody's internal MUC state.
* @param {string} roomJid e.g. 'room@conference.localhost'
* @returns {Promise<{jid: string, hidden: boolean, occupant_count: number}|null>}
*/
export async function getRoomState(roomJid) {
const res = await fetch(`${BASE}/rooms?jid=${encodeURIComponent(roomJid)}`);
if (res.status === 404) {
return null;
}
if (!res.ok) {
throw new Error(`GET /rooms failed: ${res.status}`);
}
return res.json();
}
/**
* Calls the mod_muc_kick_participant HTTP endpoint to kick a participant.
*
* Returns raw { status, body } so tests can assert on error responses.
*
* @param {string} roomJid e.g. 'room@conference.localhost'
* @param {string} participantId Occupant nick / resource to kick.
* @param {string} token Bearer token.
* @param {object} [opts]
* @param {boolean} [opts.omitAuth] If true, omits the Authorization header.
* @returns {Promise<{status: number, body: string}>}
*/
export async function kickParticipant(roomJid, participantId, token, { omitAuth = false } = {}) {
const roomName = roomJid.split('@')[0];
const url = new URL(KICK_PARTICIPANT_URL);
url.searchParams.set('room', roomName);
const headers = { 'Content-Type': 'application/json' };
if (!omitAuth) {
headers.Authorization = `Bearer ${token}`;
}
const res = await fetch(url.toString(), {
method: 'PUT',
headers,
body: JSON.stringify({ participantId })
});
return { status: res.status,
body: await res.text() };
}
/**
* Calls the mod_muc_jigasi_invite HTTP endpoint to invite Jigasi to dial out.
*
* Returns raw { status, body } so tests can assert on error responses.
*
* @param {string} roomJid Conference room JID, e.g. 'room@conference.localhost'
* @param {string} phoneNo Dial target, e.g. '+15551234567'
* @param {string} token Bearer token.
* @param {object} [opts]
* @param {boolean} [opts.omitAuth] If true, omits the Authorization header.
* @returns {Promise<{status: number, body: string}>}
*/
export async function inviteJigasi(roomJid, phoneNo, token, { omitAuth = false } = {}) {
const headers = { 'Content-Type': 'application/json' };
if (!omitAuth) {
headers.Authorization = `Bearer ${token}`;
}
const res = await fetch(JIGASI_INVITE_URL, {
method: 'POST',
headers,
body: JSON.stringify({ conference: roomJid,
phoneNo })
});
return { status: res.status,
body: await res.text() };
}
/**
* Calls the mod_system_chat_message HTTP endpoint to send a system chat message
* to one or more participants.
*
* Returns raw { status, body } so tests can assert on error responses.
*
* @param {string} roomJid e.g. 'room@conference.localhost'
* @param {string[]} connectionJIDs Full JIDs of recipients, e.g. ['user@localhost/res']
* @param {string} message Message text.
* @param {string} token Bearer token.
* @param {object} [opts]
* @param {boolean} [opts.omitAuth] If true, omits the Authorization header.
* @param {string} [opts.displayName] Optional display name to include.
* @returns {Promise<{status: number, body: string}>}
*/
export async function sendSystemChatMessage(roomJid, connectionJIDs, message, token, { // eslint-disable-line max-params
omitAuth = false,
displayName
} = {}) {
const headers = { 'Content-Type': 'application/json' };
if (!omitAuth) {
headers.Authorization = `Bearer ${token}`;
}
const body = { room: roomJid,
connectionJIDs,
message };
if (displayName !== undefined) {
body.displayName = displayName;
}
const res = await fetch(SYSTEM_CHAT_URL, {
method: 'POST',
headers,
body: JSON.stringify(body)
});
return { status: res.status,
body: await res.text() };
}
/**
* Calls the mod_muc_end_meeting HTTP endpoint to terminate a conference.
*
* Returns the raw { status, body } rather than throwing on non-2xx so that
* tests can assert on error responses (401, 404, etc.) directly.
*
* @param {string} roomJid e.g. 'room@conference.localhost'
* @param {string} token Bearer token (system token for success; login token to test rejection).
* @param {object} [opts]
* @param {boolean} [opts.silentReconnect] If true, adds silent-reconnect=true to the query.
* @param {boolean} [opts.omitAuth] If true, omits the Authorization header entirely.
* @returns {Promise<{status: number, body: string}>}
*/
export async function endMeeting(roomJid, token, { silentReconnect = false, omitAuth = false } = {}) {
const url = new URL(END_MEETING_URL);
url.searchParams.set('conference', roomJid);
if (silentReconnect) {
url.searchParams.set('silent-reconnect', 'true');
}
const headers = { 'Content-Type': 'application/json' };
if (!omitAuth) {
headers.Authorization = `Bearer ${token}`;
}
const res = await fetch(url.toString(), { method: 'POST',
headers });
return { status: res.status,
body: await res.text() };
}