497 lines
16 KiB
JavaScript
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() };
|
|
}
|