The communications platform that puts data protection first.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
Rocket.Chat/apps/meteor/tests/data/rooms.helper.ts

490 lines
15 KiB

import type { Credentials } from '@rocket.chat/api-client';
import type { IRoom, ISubscription, IUser, IMessage } from '@rocket.chat/core-typings';
import type { Endpoints } from '@rocket.chat/rest-typings';
import { api, credentials, methodCall, request } from './api-data';
import type { IRequestConfig } from './users.helper';
type CreateRoomParams = {
name?: IRoom['name'];
type: IRoom['t'];
username?: string;
members?: string[];
credentials?: Credentials;
extraData?: Record<string, any>;
config?: IRequestConfig;
};
export const createRoom = ({ name, type, username, members, credentials: customCredentials, extraData, config }: CreateRoomParams) => {
if (!type) {
throw new Error('"type" is required in "createRoom.ts" test helper');
}
const requestInstance = config?.request || request;
const credentialsInstance = config?.credentials || customCredentials || credentials;
if (type === 'd' && !username) {
throw new Error('To be able to create DM Room, you must provide the username');
}
const endpoints = {
c: 'channels.create',
p: 'groups.create',
d: 'im.create',
} as const;
const params = type === 'd' ? { username } : { name };
const roomType = endpoints[type as keyof typeof endpoints];
return requestInstance
.post(api(roomType))
.set(credentialsInstance)
.send({
...params,
...(members && { members }),
...(extraData && { extraData }),
});
};
type ActionType = 'delete' | 'close' | 'addOwner' | 'removeOwner';
export type ActionRoomParams = {
action: ActionType;
type: Exclude<IRoom['t'], 'l'>;
roomId: IRoom['_id'];
overrideCredentials?: Credentials;
extraData?: Record<string, any>;
};
export function actionRoom({ action, type, roomId, overrideCredentials = credentials, extraData = {} }: ActionRoomParams) {
if (!type) {
throw new Error(`"type" is required in "${action}Room" test helper`);
}
if (!roomId) {
throw new Error(`"roomId" is required in "${action}Room" test helper`);
}
const endpoints = {
c: 'channels',
p: 'groups',
d: 'im',
} as const;
const path = `${endpoints[type]}.${action}` as const;
if (path === 'im.addOwner' || path === 'im.removeOwner') throw new Error(`invalid path ("${path}")`);
return new Promise((resolve) => {
void request
.post(api(path))
.set(overrideCredentials)
.send({
roomId,
...extraData,
})
.end(resolve);
});
}
export const deleteRoom = ({ type, roomId }: { type: ActionRoomParams['type']; roomId: IRoom['_id'] }) =>
actionRoom({ action: 'delete', type, roomId, overrideCredentials: credentials });
export const getSubscriptionByRoomId = (roomId: IRoom['_id'], userCredentials = credentials, req = request): Promise<ISubscription> =>
new Promise((resolve, reject) => {
void req
.get(api('subscriptions.getOne'))
.set(userCredentials)
.query({ roomId })
.end((err, res) => {
if (err) {
return reject(err);
}
if (!res.body?.subscription) {
return reject(new Error('Subscription not found'));
}
resolve(res.body.subscription);
});
});
/**
* Adds users to a room using the addUsersToRoom method.
*
* Invites one or more users to join a room using the DDP method call.
* Supports both local and federated users, with proper error handling
* for federation restrictions.
*
* @param usernames - Array of usernames to add to the room
* @param rid - The unique identifier of the room
* @param userCredentials - Optional credentials for the request (deprecated, use config instead)
* @param config - Optional request configuration for custom domains
* @returns Promise resolving to the method call response
*/
export const addUserToRoom = ({
usernames,
rid,
userCredentials,
config,
}: {
usernames: string[];
rid: IRoom['_id'];
userCredentials?: Credentials;
config?: IRequestConfig;
}) => {
const requestInstance = config?.request || request;
const credentialsInstance = config?.credentials || userCredentials || credentials;
return requestInstance
.post(methodCall('addUsersToRoom'))
.set(credentialsInstance)
.send({
message: JSON.stringify({
method: 'addUsersToRoom',
params: [{ rid, users: usernames }],
id: 'id',
msg: 'method',
}),
});
};
/**
* Adds users to a room using the /invite slash command via method.call.
*
* Executes the /invite slash command using the DDP method call to add users to a room.
* This simulates the user experience of using slash commands in the UI.
* Supports both local and federated users, with proper error handling for federation restrictions.
*
* @param usernames - Array of usernames to add to the room
* @param rid - The unique identifier of the room
* @param config - Optional request configuration for custom domains
* @returns Promise resolving to the method call response
* @note The slash command expects parameters: { cmd: string, params: string, msg: IMessage, triggerId: string }
*/
export const addUserToRoomSlashCommand = ({
usernames,
rid,
config,
}: {
usernames: string[];
rid: IRoom['_id'];
config?: IRequestConfig;
}) => {
if (!usernames || usernames.length === 0) {
throw new Error('"usernames" is required in "addUserToRoomSlashCommand" test helper');
}
if (!rid) {
throw new Error('"rid" is required in "addUserToRoomSlashCommand" test helper');
}
const requestInstance = config?.request || request;
const credentialsInstance = config?.credentials || credentials;
return requestInstance
.post(methodCall('slashCommand'))
.set(credentialsInstance)
.send({
message: JSON.stringify({
method: 'slashCommand',
params: [
{
cmd: 'invite',
params: usernames.join(' '),
msg: {
rid,
_id: `test-${Date.now()}`,
},
triggerId: `test-trigger-${Date.now()}`,
},
],
id: 'id',
msg: 'method',
}),
});
};
/**
* Retrieves detailed information about a room.
*
* Fetches comprehensive room metadata including federation status,
* member counts, and other room properties needed for federation testing.
*
* @param roomId - The unique identifier of the room
* @param config - Optional request configuration for custom domains
* @returns Promise resolving to room information response
*/
export const getRoomInfo = (roomId: IRoom['_id'], config?: IRequestConfig) => {
const requestInstance = config?.request || request;
const credentialsInstance = config?.credentials || credentials;
return new Promise<ReturnType<Endpoints['/v1/rooms.info']['GET']>>((resolve) => {
void requestInstance
.get(api('rooms.info'))
.set(credentialsInstance)
.query({
roomId,
})
.end((_err: any, req: any) => {
resolve(req.body);
});
});
};
/**
* Retrieves room members ordered by their role hierarchy.
*
* Gets the complete list of room members with their roles and permissions,
* ordered by importance. Essential for verifying federation member synchronization
* and role assignments across different Rocket.Chat instances.
*
* @param roomId - The unique identifier of the room
* @param config - Optional request configuration for custom domains
* @returns Promise resolving to ordered member list response
*/
export const getRoomMembers = (roomId: IRoom['_id'], config?: IRequestConfig) => {
const requestInstance = config?.request || request;
const credentialsInstance = config?.credentials || credentials;
return new Promise<ReturnType<Endpoints['/v1/rooms.membersOrderedByRole']['GET']>>((resolve) => {
void requestInstance
.get(api('rooms.membersOrderedByRole'))
.set(credentialsInstance)
.query({
roomId,
})
.end((_err: any, req: any) => {
resolve(req.body);
});
});
};
/**
* Finds a specific room member with configurable retry logic.
*
* Searches for a member in a room by username, with retry logic to handle
* eventual consistency in federated systems. This is crucial for federation
* testing where member synchronization may take time to propagate.
*
* @param roomId - The unique identifier of the room to search
* @param username - The username to find
* @param options - Retry configuration options
* @param options.maxRetries - Maximum number of retry attempts (default: 3)
* @param options.delay - Delay between retries in milliseconds (default: 1000)
* @param options.initialDelay - Initial delay before first attempt in milliseconds (default: 0)
* @param config - Optional request configuration for custom domains
* @returns Promise resolving to the user object if found, null otherwise
*/
export const findRoomMember = async (
roomId: IRoom['_id'],
username: string,
options: { maxRetries?: number; delay?: number; initialDelay?: number } = {},
config?: IRequestConfig,
): Promise<IUser | null> => {
const { maxRetries = 3, delay = 1000, initialDelay = 0 } = options;
if (initialDelay > 0) {
await new Promise((resolve) => setTimeout(resolve, initialDelay));
}
// eslint-disable-next-line no-await-in-loop
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
// eslint-disable-next-line no-await-in-loop
const membersResponse = await getRoomMembers(roomId, config);
const member = membersResponse.members.find((member: IUser) => member.username === username);
if (member) {
return member;
}
if (attempt < maxRetries) {
// eslint-disable-next-line no-await-in-loop
await new Promise((resolve) => setTimeout(resolve, delay));
}
} catch (error) {
console.warn(`Attempt ${attempt} to find room member failed:`, error);
if (attempt < maxRetries) {
// eslint-disable-next-line no-await-in-loop
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
return null;
};
/**
* Retrieves the message history for a group/private room.
*
* Fetches the complete message history including system messages,
* user messages, and federation events. Essential for verifying
* message synchronization and system message generation in federated rooms.
*
* @param roomId - The unique identifier of the room
* @param config - Optional request configuration for custom domains
* @returns Promise resolving to message history response
*/
export const getGroupHistory = (roomId: IRoom['_id'], config?: IRequestConfig) => {
const requestInstance = config?.request || request;
const credentialsInstance = config?.credentials || credentials;
return new Promise<ReturnType<Endpoints['/v1/groups.history']['GET']>>((resolve) => {
void requestInstance
.get(api('groups.history'))
.set(credentialsInstance)
.query({
roomId,
})
.end((_err: any, req: any) => {
resolve(req.body);
});
});
};
/**
* Loads message history for a room using the loadHistory method call.
*
* Fetches message history via the DDP method call endpoint, which returns
* messages with markdown parsing metadata (md attribute). This is useful
* for testing message rendering and markdown parsing, including emoji handling.
*
* @param rid - The unique identifier of the room
* @param config - Optional request configuration for custom domains
* @param end - Optional end date to load messages before this timestamp
* @param limit - Optional limit for number of messages to return (default: 20)
* @param ls - Optional last seen timestamp for unread calculation
* @param showThreadMessages - Optional flag to include thread messages (default: true)
* @returns Promise resolving to message history with structure: { messages, firstUnread?, unreadNotLoaded? }
*/
export const loadHistory = async (
rid: IRoom['_id'],
config?: IRequestConfig,
end?: Date,
limit?: number,
ls?: string | Date,
showThreadMessages?: boolean,
) => {
const requestInstance = config?.request || request;
const credentialsInstance = config?.credentials || credentials;
const params: any[] = [rid];
if (end !== undefined) {
params.push(end);
}
if (limit !== undefined) {
params.push(limit);
}
if (ls !== undefined) {
params.push(ls);
}
if (showThreadMessages !== undefined) {
params.push(showThreadMessages);
}
const response = await requestInstance
.post(methodCall('loadHistory'))
.set(credentialsInstance)
.send({
message: JSON.stringify({
method: 'loadHistory',
params,
id: 'id',
msg: 'method',
}),
});
if (!response.body.success) {
throw new Error(`loadHistory failed: ${JSON.stringify(response.body)}`);
}
const data = JSON.parse(response.body.message);
if (data.error) {
throw new Error(`loadHistory method error: ${JSON.stringify(data.error)}`);
}
return data.result as {
messages: IMessage[];
firstUnread?: IMessage;
unreadNotLoaded?: number;
};
};
/**
* Accepts a room invite for the authenticated user.
*
* Processes a room invitation by accepting it, which grants the user
* access to the room. This is essential for federated room workflows
* where users receive invitations rather than auto-joining.
*
* @param roomId - The unique identifier of the room
* @param config - Optional request configuration for custom domains
* @returns Promise resolving to the acceptance response
*/
export const acceptRoomInvite = (roomId: IRoom['_id'], config?: IRequestConfig) => {
const requestInstance = config?.request || request;
const credentialsInstance = config?.credentials || credentials;
return new Promise<{ success: boolean; error?: string }>((resolve) => {
void requestInstance
.post(api('rooms.invite'))
.set(credentialsInstance)
.send({
roomId,
action: 'accept',
})
.end((_err: any, req: any) => {
resolve(req.body);
});
});
};
/**
* Retrieves the subscriptions for the authenticated user.
*
* Fetches the complete list of subscriptions for the authenticated user, which is essential
* for verifying federation subscription synchronization and member synchronization.
*
* @param config - Optional request configuration for custom domains
* @returns Promise resolving to the subscriptions response
*/
export const getSubscriptions = (config?: IRequestConfig) => {
const requestInstance = config?.request || request;
const credentialsInstance = config?.credentials || credentials;
return new Promise<ReturnType<Endpoints['/v1/subscriptions.get']['GET']>>((resolve) => {
void requestInstance
.get(api('subscriptions.get'))
.set(credentialsInstance)
.end((_err: any, req: any) => {
resolve(req.body);
});
});
};
/**
* Rejects a room invite for the authenticated user.
*
* Processes a room invitation by rejecting it, which prevents the user
* from joining the room and removes them from the invited members list.
* This is essential for federated room workflows where users can decline invitations.
*
* @param roomId - The unique identifier of the room
* @param config - Optional request configuration for custom domains
* @returns Promise resolving to the rejection response
*/
export const rejectRoomInvite = (roomId: IRoom['_id'], config?: IRequestConfig) => {
const requestInstance = config?.request || request;
const credentialsInstance = config?.credentials || credentials;
return new Promise<{ success: boolean; error?: string }>((resolve) => {
void requestInstance
.post(api('rooms.invite'))
.set(credentialsInstance)
.send({
roomId,
action: 'reject',
})
.end((_err: any, req: any) => {
resolve(req.body);
});
});
};