test: message media tests (#37528)

This commit is contained in:
Danilo Morães 2025-12-01 09:01:00 -03:00 committed by Guilherme Gazzo
parent ff536cb437
commit 951a9bcf9c
16 changed files with 1305 additions and 10 deletions

View File

@ -654,6 +654,7 @@ jobs:
env:
ROCKETCHAT_IMAGE: ghcr.io/${{ needs.release-versions.outputs.lowercase-repo }}/rocket.chat:${{ needs.release-versions.outputs.gh-docker-tag }}
ENTERPRISE_LICENSE_RC1: ${{ secrets.ENTERPRISE_LICENSE_RC1 }}
QASE_TESTOPS_JEST_API_TOKEN: ${{ secrets.QASE_TESTOPS_JEST_API_TOKEN }}
run: yarn test:integration --image "${ROCKETCHAT_IMAGE}"
report-coverage:

View File

@ -120,6 +120,7 @@ async function createUsersSubscriptions({
await Rooms.incUsersCountById(room._id, subs.length);
}
// eslint-disable-next-line complexity
export const createRoom = async <T extends RoomType>(
type: T,
name: T extends 'd' ? undefined : string,

View File

@ -0,0 +1,170 @@
import * as fs from 'fs';
import type { IMessage } from '@rocket.chat/core-typings';
import { api } from './api-data';
import type { IRequestConfig } from './users.helper';
/**
* Uploads a file to Rocket.Chat using the two-step process (rooms.media then rooms.mediaConfirm).
*
* @param roomId - The room ID where the file will be uploaded
* @param filePath - Path to the file to upload
* @param description - Description for the file
* @param config - Request configuration with credentials and request instance
* @param message - Optional message text to include with the file
* @returns Promise resolving to the message response
*/
export async function uploadFileToRC(
roomId: string,
filePath: string,
description: string,
config: IRequestConfig,
message = '',
): Promise<{ message: IMessage }> {
const requestInstance = config.request;
const credentialsInstance = config.credentials;
// Step 1: Upload file to rooms.media/:rid
const mediaResponse = await requestInstance
.post(api(`rooms.media/${roomId}`))
.set(credentialsInstance)
.attach('file', filePath)
.expect('Content-Type', 'application/json')
.expect(200);
if (!mediaResponse.body.success || !mediaResponse.body.file?._id) {
throw new Error(`File upload failed: ${JSON.stringify(mediaResponse.body)}`);
}
const fileId = mediaResponse.body.file._id;
// Step 2: Confirm and send message with rooms.mediaConfirm/:rid/:fileId
const confirmResponse = await requestInstance
.post(api(`rooms.mediaConfirm/${roomId}/${fileId}`))
.set(credentialsInstance)
.send({
msg: message,
description,
})
.expect('Content-Type', 'application/json')
.expect(200);
if (!confirmResponse.body.success || !confirmResponse.body.message) {
throw new Error(`File confirmation failed: ${JSON.stringify(confirmResponse.body)}`);
}
return confirmResponse.body;
}
/**
* Gets the list of files for a room.
*
* @param roomId - The room ID
* @param config - Request configuration
* @param options - Optional query parameters (name for filtering, count, offset)
* @returns Promise resolving to the files list response
*/
export async function getFilesList(
roomId: string,
config: IRequestConfig,
options: { name?: string; count?: number; offset?: number } = {},
): Promise<{
files: Array<{
_id: string;
name: string;
size: number;
type: string;
rid: string;
userId: string;
path?: string;
url?: string;
uploadedAt?: string;
federation?: {
mrid?: string;
mxcUri?: string;
serverName?: string;
mediaId?: string;
};
}>;
count: number;
offset: number;
total: number;
success: boolean;
}> {
const requestInstance = config.request;
const credentialsInstance = config.credentials;
const queryParams: Record<string, string> = {
roomId,
count: String(options.count || 10),
offset: String(options.offset || 0),
sort: JSON.stringify({ uploadedAt: -1 }),
};
if (options.name) {
queryParams.name = options.name;
}
const response = await requestInstance
.get(api('groups.files'))
.set(credentialsInstance)
.query(queryParams)
.expect('Content-Type', 'application/json')
.expect(200);
if (!response.body.success) {
throw new Error(`Failed to get files list: ${JSON.stringify(response.body)}`);
}
return response.body;
}
/**
* Downloads a file and verifies it matches the original file using binary comparison.
*
* @param fileUrl - The URL to download the file from (relative path like /file-upload/...)
* @param originalFilePath - Path to the original file to compare against
* @param config - Request configuration
* @returns Promise resolving to true if files match byte-by-byte
*/
export async function downloadFileAndVerifyBinary(fileUrl: string, originalFilePath: string, config: IRequestConfig): Promise<boolean> {
const requestInstance = config.request;
const credentialsInstance = config.credentials;
const response = await requestInstance.get(fileUrl).set(credentialsInstance).expect(200);
// Handle different response types:
// - For text/plain, supertest parses as JSON (resulting in {}), so use response.text
// - For binary files, response.body might be a Buffer
// - For other text types, response.text contains the content
let downloadedBuffer: Buffer;
if (Buffer.isBuffer(response.body)) {
// Binary file - response.body is already a Buffer
downloadedBuffer = response.body;
} else if (response.text !== undefined) {
// Text response (including text/plain) - use response.text to avoid JSON parsing
// Convert to Buffer using binary encoding to preserve exact bytes
downloadedBuffer = Buffer.from(response.text, 'binary');
} else if (typeof response.body === 'string') {
// Fallback: if body is a string, convert to buffer
downloadedBuffer = Buffer.from(response.body, 'binary');
} else {
// If body is an object (like {} from JSON parsing), this is an error
throw new Error(
`Failed to get file content. Response body type: ${typeof response.body}. ` +
`This usually means supertest parsed a text/plain response as JSON. ` +
`Response text available: ${response.text !== undefined ? 'yes' : 'no'}`,
);
}
// Read the original file
const originalBuffer = fs.readFileSync(originalFilePath);
// Compare buffers byte-by-byte
if (downloadedBuffer.length !== originalBuffer.length) {
return false;
}
return downloadedBuffer.equals(originalBuffer);
}

View File

@ -21,6 +21,27 @@ export default {
forceExit: true, // Force Jest to exit after tests complete
detectOpenHandles: true, // Detect open handles that prevent Jest from exiting
globalTeardown: '<rootDir>/tests/teardown.ts',
// To disable Qase integration, remove this line or comment it out
setupFilesAfterEnv: ['<rootDir>/tests/setup-qase.ts'],
verbose: false,
silent: false,
reporters: [
'default',
...(process.env.QASE_TESTOPS_JEST_API_TOKEN
? [
[
'jest-qase-reporter',
{
mode: 'testops',
testops: {
api: { token: process.env.QASE_TESTOPS_JEST_API_TOKEN },
project: 'RC',
run: { complete: true },
},
debug: true,
},
] as [string, { [x: string]: unknown }],
]
: []),
] as Config['reporters'],
} satisfies Config;

View File

@ -1,5 +1,12 @@
import * as path from 'path';
import type { IMessage } from '@rocket.chat/core-typings';
import {
uploadFileToRC,
getFilesList,
downloadFileAndVerifyBinary as downloadFileAndCompareBinary,
} from '../../../../../apps/meteor/tests/data/file.helper';
import { sendMessage } from '../../../../../apps/meteor/tests/data/messages.helper';
import { createRoom, loadHistory } from '../../../../../apps/meteor/tests/data/rooms.helper';
import { getRequestConfig, createUser } from '../../../../../apps/meteor/tests/data/users.helper';
@ -83,9 +90,6 @@ import { SynapseClient } from '../helper/synapse-client';
// Accept invitation for the federated user
const acceptedRoomId = await hs1AdminApp.acceptInvitationForRoomName(channelName);
expect(acceptedRoomId).not.toBe('');
// Wait for federation synchronization
await new Promise((resolve) => setTimeout(resolve, 2000));
}, 10000);
it('Send a text message', async () => {
@ -416,9 +420,6 @@ import { SynapseClient } from '../helper/synapse-client';
// Accept invitation for the federated user
const acceptedRoomId = await hs1AdminApp.acceptInvitationForRoomName(channelName);
expect(acceptedRoomId).not.toBe('');
// Wait for federation synchronization
await new Promise((resolve) => setTimeout(resolve, 2000));
}, 10000);
it('Send a text message', async () => {
@ -671,5 +672,716 @@ import { SynapseClient } from '../helper/synapse-client';
});
});
});
describe('Media', () => {
// Test file resources
const resourcesDir = path.join(__dirname, '../resources');
const testFiles = {
image: {
path: path.join(resourcesDir, 'sample_image.webp'),
fileName: 'sample_image.webp',
description: 'Image upload test',
},
pdf: {
path: path.join(resourcesDir, 'sample_pdf.pdf'),
fileName: 'sample_pdf.pdf',
description: 'PDF document test',
},
video: {
path: path.join(resourcesDir, 'sample_video.webm'),
fileName: 'sample_video.webm',
description: 'Video upload test',
},
audio: {
path: path.join(resourcesDir, 'sample_audio.mp3'),
fileName: 'sample_audio.mp3',
description: 'Audio upload test',
},
text: {
path: path.join(resourcesDir, 'sample_text.txt'),
fileName: 'sample_text.txt',
description: 'Text file upload test',
},
};
describe('On RC', () => {
let channelName: string;
let federatedChannel: any;
beforeAll(async () => {
channelName = `federated-room-media-rc-${Date.now()}`;
// Create a federated private room with federated user
const createResponse = await createRoom({
type: 'p',
name: channelName,
members: [federationConfig.hs1.adminMatrixUserId],
extraData: {
federated: true,
},
config: rc1AdminRequestConfig,
});
federatedChannel = createResponse.body.group;
expect(federatedChannel).toHaveProperty('_id');
expect(federatedChannel).toHaveProperty('name', channelName);
expect(federatedChannel).toHaveProperty('t', 'p');
expect(federatedChannel).toHaveProperty('federated', true);
// Accept invitation for the federated user
const acceptedRoomId = await hs1AdminApp.acceptInvitationForRoomName(channelName);
expect(acceptedRoomId).not.toBe('');
}, 10000);
describe('Upload one image, and add a description', () => {
it('should appear correctly locally and on the remote Element as messages', async () => {
const fileInfo = testFiles.image;
const uploadResponse = await uploadFileToRC(federatedChannel._id, fileInfo.path, fileInfo.description, rc1AdminRequestConfig);
expect(uploadResponse.message).toBeDefined();
// RC view: Verify in RC loadHistory
const historyResponse = await loadHistory(federatedChannel._id, rc1AdminRequestConfig);
const rcMessage = historyResponse.messages.find((message: IMessage) => message.files?.[0]?.name === fileInfo.fileName);
expect(rcMessage).toBeDefined();
// RC view: Verify files array
expect(rcMessage?.files).toBeDefined();
expect(rcMessage?.files?.[0]?.name).toBe(fileInfo.fileName);
expect(rcMessage?.files?.[0]?.type).toBe('image/webp');
expect(rcMessage?.files?.[0]?.size).toBe(uploadResponse.message.files?.[0]?.size);
// RC view: Verify attachments array
expect(rcMessage?.attachments).toBeDefined();
expect(rcMessage?.attachments?.[0]?.title).toBe(fileInfo.fileName);
expect(rcMessage?.attachments?.[0]?.title_link).toMatch(/^\/file-upload\/[^/]+\/.+$/);
expect(rcMessage?.attachments?.[0]?.title_link_download).toBe(true);
expect((rcMessage?.attachments?.[0] as any)?.type).toBe('file');
expect(rcMessage?.attachments?.[0]?.description).toBe(fileInfo.description);
expect((rcMessage?.attachments?.[0] as any)?.image_url).toMatch(/^\/file-upload\/[^/]+\/.+$/);
expect((rcMessage?.attachments?.[0] as any)?.image_type).toBe('image/webp');
expect((rcMessage?.attachments?.[0] as any)?.image_size).toBe(uploadResponse.message.files?.[0]?.size);
// RC view: Verify federation
expect(rcMessage?.federation?.eventId).not.toBe('');
// Element view: Verify in Element
const synapseMessage = await hs1AdminApp.findFileMessageInRoom(channelName, fileInfo.fileName);
expect(synapseMessage).not.toBeNull();
expect(synapseMessage?.content.body).toBe(fileInfo.fileName);
expect(synapseMessage?.content.msgtype).toBe('m.image');
expect(rcMessage?.federation?.eventId).toBe(synapseMessage?.event_id);
});
it('should appear in the list of files locally', async () => {
const fileInfo = testFiles.image;
// RC view: Verify in RC file list
const rcFilesList = await getFilesList(federatedChannel._id, rc1AdminRequestConfig);
const rcFile = rcFilesList.files.find((file) => file.name === fileInfo.fileName);
expect(rcFile).toBeDefined();
expect(rcFile?.type).toBe('image/webp');
expect(rcFile?.federation).toBeDefined();
// RC view: The file should have federation metadata
expect(rcFile?.federation?.mxcUri).toBeDefined();
});
it('should be able to download the files locally and on the remote Element', async () => {
const fileInfo = testFiles.image;
// RC view: Get the file from history to get download URL
const historyResponse = await loadHistory(federatedChannel._id, rc1AdminRequestConfig);
const rcMessage = historyResponse.messages.find((message: IMessage) => message.files?.[0]?.name === fileInfo.fileName);
expect(rcMessage).toBeDefined();
expect(rcMessage?.attachments?.[0]?.title_link).toBeDefined();
// RC view: Download and verify binary match from RC
const downloadUrl = rcMessage?.attachments?.[0]?.title_link as string;
const rcFilesMatch = await downloadFileAndCompareBinary(downloadUrl, fileInfo.path, rc1AdminRequestConfig);
expect(rcFilesMatch).toBe(true);
// Element view: Download and verify binary match from Element
const synapseMessage = await hs1AdminApp.findFileMessageInRoom(channelName, fileInfo.fileName);
expect(synapseMessage).not.toBeNull();
expect(synapseMessage?.content.url).toBeDefined();
const synapseFilesMatch = await hs1AdminApp.downloadFileAndCompareBinary(synapseMessage?.content.url as string, fileInfo.path);
expect(synapseFilesMatch).toBe(true);
});
});
describe('Upload one PDF, and add a description', () => {
it('should appear correctly locally and on the remote Element as messages', async () => {
const fileInfo = testFiles.pdf;
const uploadResponse = await uploadFileToRC(federatedChannel._id, fileInfo.path, fileInfo.description, rc1AdminRequestConfig);
expect(uploadResponse.message).toBeDefined();
// RC view: Verify in RC loadHistory
const historyResponse = await loadHistory(federatedChannel._id, rc1AdminRequestConfig);
const rcMessage = historyResponse.messages.find((message: IMessage) => message.files?.[0]?.name === fileInfo.fileName);
expect(rcMessage).toBeDefined();
// RC view: Verify files array
expect(rcMessage?.files).toBeDefined();
expect(rcMessage?.files?.[0]?.name).toBe(fileInfo.fileName);
expect(rcMessage?.files?.[0]?.type).toBe('application/pdf');
expect(rcMessage?.files?.[0]?.size).toBe(uploadResponse.message.files?.[0]?.size);
// RC view: Verify attachments array
expect(rcMessage?.attachments).toBeDefined();
expect(rcMessage?.attachments?.[0]?.title).toBe(fileInfo.fileName);
expect(rcMessage?.attachments?.[0]?.title_link).toMatch(/^\/file-upload\/[^/]+\/.+$/);
expect(rcMessage?.attachments?.[0]?.title_link_download).toBe(true);
expect((rcMessage?.attachments?.[0] as any)?.type).toBe('file');
expect(rcMessage?.attachments?.[0]?.description).toBe(fileInfo.description);
expect(rcMessage?.attachments?.[0]?.size).toBe(uploadResponse.message.files?.[0]?.size);
// RC view: Verify federation
expect(rcMessage?.federation?.eventId).not.toBe('');
// Element view: Verify in Element
const synapseMessage = await hs1AdminApp.findFileMessageInRoom(channelName, fileInfo.fileName);
expect(synapseMessage).not.toBeNull();
expect(synapseMessage?.content.body).toBe(fileInfo.fileName);
expect(synapseMessage?.content.msgtype).toBe('m.file');
expect(rcMessage?.federation?.eventId).toBe(synapseMessage?.event_id);
});
it('should appear in the list of files locally', async () => {
const fileInfo = testFiles.pdf;
// RC view: Verify in RC file list
const rcFilesList = await getFilesList(federatedChannel._id, rc1AdminRequestConfig);
const rcFile = rcFilesList.files.find((file) => file.name === fileInfo.fileName);
expect(rcFile).toBeDefined();
expect(rcFile?.type).toBe('application/pdf');
expect(rcFile?.federation).toBeDefined();
// RC view: The file should have federation metadata
expect(rcFile?.federation?.mxcUri).toBeDefined();
});
it('should be able to download the files locally and on the remote Element', async () => {
const fileInfo = testFiles.pdf;
// RC view: Get the file from history to get download URL
const historyResponse = await loadHistory(federatedChannel._id, rc1AdminRequestConfig);
const rcMessage = historyResponse.messages.find((message: IMessage) => message.files?.[0]?.name === fileInfo.fileName);
expect(rcMessage).toBeDefined();
expect(rcMessage?.attachments?.[0]?.title_link).toBeDefined();
// RC view: Download and verify binary match from RC
const downloadUrl = rcMessage?.attachments?.[0]?.title_link as string;
const rcFilesMatch = await downloadFileAndCompareBinary(downloadUrl, fileInfo.path, rc1AdminRequestConfig);
expect(rcFilesMatch).toBe(true);
// Element view: Download and verify binary match from Element
const synapseMessage = await hs1AdminApp.findFileMessageInRoom(channelName, fileInfo.fileName);
expect(synapseMessage).not.toBeNull();
expect(synapseMessage?.content.url).toBeDefined();
const synapseFilesMatch = await hs1AdminApp.downloadFileAndCompareBinary(synapseMessage?.content.url as string, fileInfo.path);
expect(synapseFilesMatch).toBe(true);
});
});
describe('Upload one Video, and add a description', () => {
it('should appear correctly locally and on the remote Element as messages', async () => {
const fileInfo = testFiles.video;
const uploadResponse = await uploadFileToRC(federatedChannel._id, fileInfo.path, fileInfo.description, rc1AdminRequestConfig);
expect(uploadResponse.message).toBeDefined();
// RC view: Verify in RC loadHistory
const historyResponse = await loadHistory(federatedChannel._id, rc1AdminRequestConfig);
const rcMessage = historyResponse.messages.find((message: IMessage) => message.files?.[0]?.name === fileInfo.fileName);
expect(rcMessage).toBeDefined();
// RC view: Verify files array
expect(rcMessage?.files).toBeDefined();
expect(rcMessage?.files?.[0]?.name).toBe(fileInfo.fileName);
expect(rcMessage?.files?.[0]?.type).toBe('video/webm');
expect(rcMessage?.files?.[0]?.size).toBe(uploadResponse.message.files?.[0]?.size);
// RC view: Verify attachments array
expect(rcMessage?.attachments).toBeDefined();
expect(rcMessage?.attachments?.[0]?.title).toBe(fileInfo.fileName);
expect(rcMessage?.attachments?.[0]?.title_link).toMatch(/^\/file-upload\/[^/]+\/.+$/);
expect(rcMessage?.attachments?.[0]?.title_link_download).toBe(true);
expect((rcMessage?.attachments?.[0] as any)?.type).toBe('file');
expect(rcMessage?.attachments?.[0]?.description).toBe(fileInfo.description);
expect((rcMessage?.attachments?.[0] as any)?.video_url).toMatch(/^\/file-upload\/[^/]+\/.+$/);
expect((rcMessage?.attachments?.[0] as any)?.video_type).toBe('video/webm');
expect((rcMessage?.attachments?.[0] as any)?.video_size).toBe(uploadResponse.message.files?.[0]?.size);
// RC view: Verify federation
expect(rcMessage?.federation?.eventId).not.toBe('');
// Element view: Verify in Element
const synapseMessage = await hs1AdminApp.findFileMessageInRoom(channelName, fileInfo.fileName);
expect(synapseMessage).not.toBeNull();
expect(synapseMessage?.content.body).toBe(fileInfo.fileName);
expect(synapseMessage?.content.msgtype).toBe('m.video');
expect(rcMessage?.federation?.eventId).toBe(synapseMessage?.event_id);
});
it('should appear in the list of files locally', async () => {
const fileInfo = testFiles.video;
// RC view: Verify in RC file list
const rcFilesList = await getFilesList(federatedChannel._id, rc1AdminRequestConfig);
const rcFile = rcFilesList.files.find((file) => file.name === fileInfo.fileName);
expect(rcFile).toBeDefined();
expect(rcFile?.type).toBe('video/webm');
expect(rcFile?.federation).toBeDefined();
// RC view: The file should have federation metadata
expect(rcFile?.federation?.mxcUri).toBeDefined();
});
it('should be able to download the files locally and on the remote Element', async () => {
const fileInfo = testFiles.video;
// RC view: Get the file from history to get download URL
const historyResponse = await loadHistory(federatedChannel._id, rc1AdminRequestConfig);
const rcMessage = historyResponse.messages.find((message: IMessage) => message.files?.[0]?.name === fileInfo.fileName);
expect(rcMessage).toBeDefined();
expect(rcMessage?.attachments?.[0]?.title_link).toBeDefined();
// RC view: Download and verify binary match from RC
const downloadUrl = rcMessage?.attachments?.[0]?.title_link as string;
const rcFilesMatch = await downloadFileAndCompareBinary(downloadUrl, fileInfo.path, rc1AdminRequestConfig);
expect(rcFilesMatch).toBe(true);
// Element view: Download and verify binary match from Element
const synapseMessage = await hs1AdminApp.findFileMessageInRoom(channelName, fileInfo.fileName);
expect(synapseMessage).not.toBeNull();
expect(synapseMessage?.content.url).toBeDefined();
const synapseFilesMatch = await hs1AdminApp.downloadFileAndCompareBinary(synapseMessage?.content.url as string, fileInfo.path);
expect(synapseFilesMatch).toBe(true);
});
});
describe('Upload one Audio, and add a description', () => {
it('should appear correctly locally and on the remote Element as messages', async () => {
const fileInfo = testFiles.audio;
const uploadResponse = await uploadFileToRC(federatedChannel._id, fileInfo.path, fileInfo.description, rc1AdminRequestConfig);
expect(uploadResponse.message).toBeDefined();
// RC view: Verify in RC loadHistory
const historyResponse = await loadHistory(federatedChannel._id, rc1AdminRequestConfig);
const rcMessage = historyResponse.messages.find((message: IMessage) => message.files?.[0]?.name === fileInfo.fileName);
expect(rcMessage).toBeDefined();
// RC view: Verify files array
expect(rcMessage?.files).toBeDefined();
expect(rcMessage?.files?.[0]?.name).toBe(fileInfo.fileName);
expect(rcMessage?.files?.[0]?.type).toBe('audio/mpeg');
expect(rcMessage?.files?.[0]?.size).toBe(uploadResponse.message.files?.[0]?.size);
// RC view: Verify attachments array
expect(rcMessage?.attachments).toBeDefined();
expect(rcMessage?.attachments?.[0]?.title).toBe(fileInfo.fileName);
expect(rcMessage?.attachments?.[0]?.title_link).toMatch(/^\/file-upload\/[^/]+\/.+$/);
expect(rcMessage?.attachments?.[0]?.title_link_download).toBe(true);
expect((rcMessage?.attachments?.[0] as any)?.type).toBe('file');
expect(rcMessage?.attachments?.[0]?.description).toBe(fileInfo.description);
expect((rcMessage?.attachments?.[0] as any)?.audio_url).toMatch(/^\/file-upload\/[^/]+\/.+$/);
expect((rcMessage?.attachments?.[0] as any)?.audio_type).toBe('audio/mpeg');
expect((rcMessage?.attachments?.[0] as any)?.audio_size).toBe(uploadResponse.message.files?.[0]?.size);
// RC view: Verify federation
expect(rcMessage?.federation?.eventId).not.toBe('');
// Element view: Verify in Element
const synapseMessage = await hs1AdminApp.findFileMessageInRoom(channelName, fileInfo.fileName);
expect(synapseMessage).not.toBeNull();
expect(synapseMessage?.content.body).toBe(fileInfo.fileName);
expect(synapseMessage?.content.msgtype).toBe('m.audio');
expect(rcMessage?.federation?.eventId).toBe(synapseMessage?.event_id);
});
it('should appear in the list of files locally', async () => {
const fileInfo = testFiles.audio;
// RC view: Verify in RC file list
const rcFilesList = await getFilesList(federatedChannel._id, rc1AdminRequestConfig);
const rcFile = rcFilesList.files.find((file) => file.name === fileInfo.fileName);
expect(rcFile).toBeDefined();
expect(rcFile?.type).toBe('audio/mpeg');
expect(rcFile?.federation).toBeDefined();
// RC view: The file should have federation metadata
expect(rcFile?.federation?.mxcUri).toBeDefined();
});
it('should be able to download the files locally and on the remote Element', async () => {
const fileInfo = testFiles.audio;
// RC view: Get the file from history to get download URL
const historyResponse = await loadHistory(federatedChannel._id, rc1AdminRequestConfig);
const rcMessage = historyResponse.messages.find((message: IMessage) => message.files?.[0]?.name === fileInfo.fileName);
expect(rcMessage).toBeDefined();
expect(rcMessage?.attachments?.[0]?.title_link).toBeDefined();
// RC view: Download and verify binary match from RC
const downloadUrl = rcMessage?.attachments?.[0]?.title_link as string;
const rcFilesMatch = await downloadFileAndCompareBinary(downloadUrl, fileInfo.path, rc1AdminRequestConfig);
expect(rcFilesMatch).toBe(true);
// Element view: Download and verify binary match from Element
const synapseMessage = await hs1AdminApp.findFileMessageInRoom(channelName, fileInfo.fileName);
expect(synapseMessage).not.toBeNull();
expect(synapseMessage?.content.url).toBeDefined();
const synapseFilesMatch = await hs1AdminApp.downloadFileAndCompareBinary(synapseMessage?.content.url as string, fileInfo.path);
expect(synapseFilesMatch).toBe(true);
});
});
describe('Upload one Text File, and add a description', () => {
it('should appear correctly locally and on the remote Element as messages', async () => {
const fileInfo = testFiles.text;
const uploadResponse = await uploadFileToRC(federatedChannel._id, fileInfo.path, fileInfo.description, rc1AdminRequestConfig);
expect(uploadResponse.message).toBeDefined();
// RC view: Verify in RC loadHistory
const historyResponse = await loadHistory(federatedChannel._id, rc1AdminRequestConfig);
const rcMessage = historyResponse.messages.find((message: IMessage) => message.files?.[0]?.name === fileInfo.fileName);
expect(rcMessage).toBeDefined();
// RC view: Verify files array
expect(rcMessage?.files).toBeDefined();
expect(rcMessage?.files?.[0]?.name).toBe(fileInfo.fileName);
expect(rcMessage?.files?.[0]?.type).toBe('text/plain');
expect(rcMessage?.files?.[0]?.size).toBe(uploadResponse.message.files?.[0]?.size);
// RC view: Verify attachments array
expect(rcMessage?.attachments).toBeDefined();
expect(rcMessage?.attachments?.[0]?.title).toBe(fileInfo.fileName);
expect(rcMessage?.attachments?.[0]?.title_link).toMatch(/^\/file-upload\/[^/]+\/.+$/);
expect(rcMessage?.attachments?.[0]?.title_link_download).toBe(true);
expect((rcMessage?.attachments?.[0] as any)?.type).toBe('file');
expect(rcMessage?.attachments?.[0]?.description).toBe(fileInfo.description);
// RC view: Verify federation
expect(rcMessage?.federation?.eventId).not.toBe('');
// Element view: Verify in Element
const synapseMessage = await hs1AdminApp.findFileMessageInRoom(channelName, fileInfo.fileName);
expect(synapseMessage).not.toBeNull();
expect(synapseMessage?.content.body).toBe(fileInfo.fileName);
expect(synapseMessage?.content.msgtype).toBe('m.file');
expect(rcMessage?.federation?.eventId).toBe(synapseMessage?.event_id);
});
it('should appear in the list of files locally', async () => {
const fileInfo = testFiles.text;
// RC view: Verify in RC file list
const rcFilesList = await getFilesList(federatedChannel._id, rc1AdminRequestConfig);
const rcFile = rcFilesList.files.find((file) => file.name === fileInfo.fileName);
expect(rcFile).toBeDefined();
expect(rcFile?.type).toBe('text/plain');
expect(rcFile?.federation).toBeDefined();
// RC view: The file should have federation metadata
expect(rcFile?.federation?.mxcUri).toBeDefined();
});
it('should be able to download the files locally and on the remote Element', async () => {
const fileInfo = testFiles.text;
// RC view: Get the file from history to get download URL
const historyResponse = await loadHistory(federatedChannel._id, rc1AdminRequestConfig);
const rcMessage = historyResponse.messages.find((message: IMessage) => message.files?.[0]?.name === fileInfo.fileName);
expect(rcMessage).toBeDefined();
expect(rcMessage?.attachments?.[0]?.title_link).toBeDefined();
// RC view: Download and verify binary match from RC
const downloadUrl = rcMessage?.attachments?.[0]?.title_link as string;
const rcFilesMatch = await downloadFileAndCompareBinary(downloadUrl, fileInfo.path, rc1AdminRequestConfig);
expect(rcFilesMatch).toBe(true);
// Element view: Download and verify binary match from Element
const synapseMessage = await hs1AdminApp.findFileMessageInRoom(channelName, fileInfo.fileName);
expect(synapseMessage).not.toBeNull();
expect(synapseMessage?.content.url).toBeDefined();
const synapseFilesMatch = await hs1AdminApp.downloadFileAndCompareBinary(synapseMessage?.content.url as string, fileInfo.path);
expect(synapseFilesMatch).toBe(true);
});
});
});
describe('On Element', () => {
let channelName: string;
let federatedChannel: any;
beforeAll(async () => {
channelName = `federated-room-media-element-${Date.now()}`;
// Create a federated private room with federated user
const createResponse = await createRoom({
type: 'p',
name: channelName,
members: [federationConfig.hs1.adminMatrixUserId],
extraData: {
federated: true,
},
config: rc1AdminRequestConfig,
});
federatedChannel = createResponse.body.group;
expect(federatedChannel).toHaveProperty('_id');
expect(federatedChannel).toHaveProperty('name', channelName);
expect(federatedChannel).toHaveProperty('t', 'p');
expect(federatedChannel).toHaveProperty('federated', true);
// Accept invitation for the federated user
const acceptedRoomId = await hs1AdminApp.acceptInvitationForRoomName(channelName);
expect(acceptedRoomId).not.toBe('');
}, 10000);
describe('Upload one image', () => {
it('should appear correctly on the remote RC as messages', async () => {
const fileInfo = testFiles.image;
await hs1AdminApp.uploadFile(channelName, fileInfo.path, fileInfo.fileName);
// Element view: Verify in Element
const synapseMessage = await hs1AdminApp.findFileMessageInRoom(channelName, fileInfo.fileName);
expect(synapseMessage).not.toBeNull();
expect(synapseMessage?.content.msgtype).toBe('m.image');
// RC view: Verify in RC loadHistory
const historyResponse = await loadHistory(federatedChannel._id, rc1AdminRequestConfig);
const rcMessage = historyResponse.messages.find((message: IMessage) => message.attachments?.[0]?.title === fileInfo.fileName);
expect(rcMessage).toBeDefined();
expect((rcMessage?.attachments?.[0] as any)?.type).toBe('file');
expect(rcMessage?.federation?.eventId).toBe(synapseMessage?.event_id);
});
it('should appear in the list of files on the remote RC', async () => {
const fileInfo = testFiles.image;
// RC view: Verify in RC file list
const rcFilesList = await getFilesList(federatedChannel._id, rc1AdminRequestConfig);
const rcFile = rcFilesList.files.find((file) => file.name === fileInfo.fileName);
expect(rcFile).toBeDefined();
});
it('should be able to download the files on the remote RC', async () => {
const fileInfo = testFiles.image;
// RC view: Download and verify binary match from RC
const historyResponse = await loadHistory(federatedChannel._id, rc1AdminRequestConfig);
const rcMessage = historyResponse.messages.find((message: IMessage) => message.attachments?.[0]?.title === fileInfo.fileName);
expect(rcMessage?.attachments?.[0]?.title_link).toBeDefined();
const downloadUrl = rcMessage?.attachments?.[0]?.title_link as string;
const rcFilesMatch = await downloadFileAndCompareBinary(downloadUrl, fileInfo.path, rc1AdminRequestConfig);
expect(rcFilesMatch).toBe(true);
});
it('should be possible to filter the list of files on the remote RC', async () => {
const fileInfo = testFiles.image;
const filteredFiles = await getFilesList(federatedChannel._id, rc1AdminRequestConfig, {
name: fileInfo.fileName,
});
expect(filteredFiles.files.find((file) => file.name === fileInfo.fileName)).toBeDefined();
});
});
describe('Upload one PDF', () => {
it('should appear correctly on the remote RC as messages', async () => {
const fileInfo = testFiles.pdf;
await hs1AdminApp.uploadFile(channelName, fileInfo.path, fileInfo.fileName);
// Element view: Verify in Element
const synapseMessage = await hs1AdminApp.findFileMessageInRoom(channelName, fileInfo.fileName);
expect(synapseMessage).not.toBeNull();
expect(synapseMessage?.content.msgtype).toBe('m.file');
// RC view: Verify in RC loadHistory
const historyResponse = await loadHistory(federatedChannel._id, rc1AdminRequestConfig);
const rcMessage = historyResponse.messages.find((message: IMessage) => message.attachments?.[0]?.title === fileInfo.fileName);
expect(rcMessage).toBeDefined();
expect(rcMessage?.federation?.eventId).toBe(synapseMessage?.event_id);
});
it('should appear in the list of files on the remote RC', async () => {
const fileInfo = testFiles.pdf;
// RC view: Verify in RC file list
const rcFilesList = await getFilesList(federatedChannel._id, rc1AdminRequestConfig);
expect(rcFilesList.files.find((file) => file.name === fileInfo.fileName)).toBeDefined();
});
it('should be able to download the files on the remote RC', async () => {
const fileInfo = testFiles.pdf;
// RC view: Download and verify binary match from RC
const historyResponse = await loadHistory(federatedChannel._id, rc1AdminRequestConfig);
const rcMessage = historyResponse.messages.find((message: IMessage) => message.attachments?.[0]?.title === fileInfo.fileName);
expect(rcMessage?.attachments?.[0]?.title_link).toBeDefined();
const downloadUrl = rcMessage?.attachments?.[0]?.title_link as string;
const rcFilesMatch = await downloadFileAndCompareBinary(downloadUrl, fileInfo.path, rc1AdminRequestConfig);
expect(rcFilesMatch).toBe(true);
});
it('should be possible to filter the list of files on the remote RC', async () => {
const fileInfo = testFiles.pdf;
const filteredFiles = await getFilesList(federatedChannel._id, rc1AdminRequestConfig, {
name: fileInfo.fileName,
});
expect(filteredFiles.files.find((file) => file.name === fileInfo.fileName)).toBeDefined();
});
});
describe('Upload one Video', () => {
it('should appear correctly on the remote RC as messages', async () => {
const fileInfo = testFiles.video;
await hs1AdminApp.uploadFile(channelName, fileInfo.path, fileInfo.fileName);
// Element view: Verify in Element
const synapseMessage = await hs1AdminApp.findFileMessageInRoom(channelName, fileInfo.fileName);
expect(synapseMessage).not.toBeNull();
expect(synapseMessage?.content.msgtype).toBe('m.video');
// RC view: Verify in RC loadHistory
const historyResponse = await loadHistory(federatedChannel._id, rc1AdminRequestConfig);
const rcMessage = historyResponse.messages.find((message: IMessage) => message.attachments?.[0]?.title === fileInfo.fileName);
expect(rcMessage).toBeDefined();
expect(rcMessage?.federation?.eventId).toBe(synapseMessage?.event_id);
});
it('should appear in the list of files on the remote RC', async () => {
const fileInfo = testFiles.video;
// RC view: Verify in RC file list
const rcFilesList = await getFilesList(federatedChannel._id, rc1AdminRequestConfig);
expect(rcFilesList.files.find((file) => file.name === fileInfo.fileName)).toBeDefined();
});
it('should be able to download the files on the remote RC', async () => {
const fileInfo = testFiles.video;
// RC view: Download and verify binary match from RC
const historyResponse = await loadHistory(federatedChannel._id, rc1AdminRequestConfig);
const rcMessage = historyResponse.messages.find((message: IMessage) => message.attachments?.[0]?.title === fileInfo.fileName);
expect(rcMessage?.attachments?.[0]?.title_link).toBeDefined();
const downloadUrl = rcMessage?.attachments?.[0]?.title_link as string;
const rcFilesMatch = await downloadFileAndCompareBinary(downloadUrl, fileInfo.path, rc1AdminRequestConfig);
expect(rcFilesMatch).toBe(true);
});
it('should be possible to filter the list of files on the remote RC', async () => {
const fileInfo = testFiles.video;
const filteredFiles = await getFilesList(federatedChannel._id, rc1AdminRequestConfig, {
name: fileInfo.fileName,
});
expect(filteredFiles.files.find((file) => file.name === fileInfo.fileName)).toBeDefined();
});
});
describe('Upload one Audio', () => {
it('should appear correctly on the remote RC as messages', async () => {
const fileInfo = testFiles.audio;
await hs1AdminApp.uploadFile(channelName, fileInfo.path, fileInfo.fileName);
// Element view: Verify in Element
const synapseMessage = await hs1AdminApp.findFileMessageInRoom(channelName, fileInfo.fileName);
expect(synapseMessage).not.toBeNull();
expect(synapseMessage?.content.msgtype).toBe('m.audio');
// RC view: Verify in RC loadHistory
const historyResponse = await loadHistory(federatedChannel._id, rc1AdminRequestConfig);
const rcMessage = historyResponse.messages.find((message: IMessage) => message.attachments?.[0]?.title === fileInfo.fileName);
expect(rcMessage).toBeDefined();
expect(rcMessage?.federation?.eventId).toBe(synapseMessage?.event_id);
});
it('should appear in the list of files on the remote RC', async () => {
const fileInfo = testFiles.audio;
// RC view: Verify in RC file list
const rcFilesList = await getFilesList(federatedChannel._id, rc1AdminRequestConfig);
expect(rcFilesList.files.find((file) => file.name === fileInfo.fileName)).toBeDefined();
});
it('should be able to download the files on the remote RC', async () => {
const fileInfo = testFiles.audio;
// RC view: Download and verify binary match from RC
const historyResponse = await loadHistory(federatedChannel._id, rc1AdminRequestConfig);
const rcMessage = historyResponse.messages.find((message: IMessage) => message.attachments?.[0]?.title === fileInfo.fileName);
expect(rcMessage?.attachments?.[0]?.title_link).toBeDefined();
const downloadUrl = rcMessage?.attachments?.[0]?.title_link as string;
const rcFilesMatch = await downloadFileAndCompareBinary(downloadUrl, fileInfo.path, rc1AdminRequestConfig);
expect(rcFilesMatch).toBe(true);
});
it('should be possible to filter the list of files on the remote RC', async () => {
const fileInfo = testFiles.audio;
const filteredFiles = await getFilesList(federatedChannel._id, rc1AdminRequestConfig, {
name: fileInfo.fileName,
});
expect(filteredFiles.files.find((file) => file.name === fileInfo.fileName)).toBeDefined();
});
});
describe('Upload one Text File', () => {
it('should appear correctly on the remote RC as messages', async () => {
const fileInfo = testFiles.text;
await hs1AdminApp.uploadFile(channelName, fileInfo.path, fileInfo.fileName);
// Element view: Verify in Element
const synapseMessage = await hs1AdminApp.findFileMessageInRoom(channelName, fileInfo.fileName);
expect(synapseMessage).not.toBeNull();
expect(synapseMessage?.content.msgtype).toBe('m.file');
// RC view: Verify in RC loadHistory
const historyResponse = await loadHistory(federatedChannel._id, rc1AdminRequestConfig);
const rcMessage = historyResponse.messages.find((message: IMessage) => message.attachments?.[0]?.title === fileInfo.fileName);
expect(rcMessage).toBeDefined();
expect(rcMessage?.federation?.eventId).toBe(synapseMessage?.event_id);
});
it('should appear in the list of files on the remote RC', async () => {
const fileInfo = testFiles.text;
// RC view: Verify in RC file list
const rcFilesList = await getFilesList(federatedChannel._id, rc1AdminRequestConfig);
expect(rcFilesList.files.find((file) => file.name === fileInfo.fileName)).toBeDefined();
});
it('should be able to download the files on the remote RC', async () => {
const fileInfo = testFiles.text;
// RC view: Download and verify binary match from RC
const historyResponse = await loadHistory(federatedChannel._id, rc1AdminRequestConfig);
const rcMessage = historyResponse.messages.find((message: IMessage) => message.attachments?.[0]?.title === fileInfo.fileName);
expect(rcMessage?.attachments?.[0]?.title_link).toBeDefined();
const downloadUrl = rcMessage?.attachments?.[0]?.title_link as string;
const rcFilesMatch = await downloadFileAndCompareBinary(downloadUrl, fileInfo.path, rc1AdminRequestConfig);
expect(rcFilesMatch).toBe(true);
});
it('should be possible to filter the list of files on the remote RC', async () => {
const fileInfo = testFiles.text;
const filteredFiles = await getFilesList(federatedChannel._id, rc1AdminRequestConfig, {
name: fileInfo.fileName,
});
expect(filteredFiles.files.find((file) => file.name === fileInfo.fileName)).toBeDefined();
});
});
});
});
});
});

View File

@ -176,8 +176,6 @@ import { SynapseClient } from '../helper/synapse-client';
config: rc1AdminRequestConfig,
});
console.log('response', response.body);
expect(response.body).toHaveProperty('success', true);
expect(response.body).toHaveProperty('message');

View File

@ -84,7 +84,6 @@ export class DDPListener {
return new Promise((resolve, reject) => {
// Check if message already exists
const existingMessage = this.ephemeralMessages.find((msg) => {
console.log('msg', msg);
const contentMatches = msg.msg?.includes(expectedContent);
const roomMatches = !roomId || msg.rid === roomId;
return contentMatches && roomMatches;
@ -107,7 +106,6 @@ export class DDPListener {
const checkMessages = () => {
const message = this.ephemeralMessages.find((msg) => {
console.log('msg', msg);
const contentMatches = msg.msg?.includes(expectedContent);
const roomMatches = !roomId || msg.rid === roomId;
return contentMatches && roomMatches;

View File

@ -4,6 +4,9 @@
* This file provides validated federation configuration for federation tests.
*/
import * as fs from 'fs';
import * as path from 'path';
import { createClient, type MatrixClient, KnownMembership, type Room, type RoomMember } from 'matrix-js-sdk';
/**
@ -399,6 +402,252 @@ export class SynapseClient {
return null;
}
/**
* Uploads a file to a room using Matrix JS SDK.
*
* Uploads a file to the specified room and sends it as a file message.
* Determines the appropriate msgtype based on file extension and mime type.
*
* @param roomName - The display name of the room to upload the file to
* @param filePath - Path to the file to upload
* @param fileName - The file name to use in the message body (used by findFileMessageInRoom)
* @returns Promise resolving to the Matrix event ID of the sent file message
* @throws Error if client is not initialized or room is not found
*/
async uploadFile(roomName: string, filePath: string, fileName: string): Promise<string> {
if (!this.matrixClient) {
throw new Error('Matrix client is not initialized');
}
const room = this.getRoom(roomName);
// Read file
const fileBuffer = fs.readFileSync(filePath);
const fileExtension = path.extname(fileName).toLowerCase().slice(1);
// Determine mime type based on extension
const mimeTypes: Record<string, string> = {
webp: 'image/webp',
png: 'image/png',
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
gif: 'image/gif',
pdf: 'application/pdf',
webm: 'video/webm',
mp4: 'video/mp4',
mp3: 'audio/mpeg',
wav: 'audio/wav',
txt: 'text/plain',
};
const mimeType = mimeTypes[fileExtension] || 'application/octet-stream';
// Determine msgtype based on file type
let msgtype: string;
if (mimeType.startsWith('image/')) {
msgtype = 'm.image';
} else if (mimeType.startsWith('video/')) {
msgtype = 'm.video';
} else if (mimeType.startsWith('audio/')) {
msgtype = 'm.audio';
} else {
msgtype = 'm.file';
}
// Upload file content
const uploadResponse = await this.matrixClient.uploadContent(fileBuffer, {
name: fileName,
type: mimeType,
});
if (!uploadResponse.content_uri) {
throw new Error('File upload failed: no content URI returned');
}
// Send file message
const content: any = {
msgtype,
body: fileName,
url: uploadResponse.content_uri,
info: {
mimetype: mimeType,
size: fileBuffer.length,
},
};
const response = await this.matrixClient.sendMessage(room.roomId, content);
return response.event_id;
}
/**
* Retrieves all file/media messages from a room's timeline.
*
* Gets all file message events (images, videos, audio, files) from the room's timeline.
* Useful for verifying file synchronization in federation testing.
*
* @param roomName - The display name of the room
* @returns Array of file message events from the room's timeline
* @throws Error if client is not initialized or room is not found
*/
getRoomFileMessages(roomName: string): Array<{
content: { body: string; msgtype: string; url?: string; info?: any };
event_id: string;
sender: string;
}> {
if (!this.matrixClient) {
throw new Error('Matrix client is not initialized');
}
const room = this.getRoom(roomName);
const { timeline } = room;
const messages: Array<{
content: { body: string; msgtype: string; url?: string; info?: any };
event_id: string;
sender: string;
}> = [];
for (const event of timeline) {
if (event.getType() === 'm.room.message') {
const content = event.getContent();
if (
content.msgtype === 'm.image' ||
content.msgtype === 'm.video' ||
content.msgtype === 'm.audio' ||
content.msgtype === 'm.file'
) {
messages.push({
content: {
body: content.body || '',
msgtype: content.msgtype,
url: content.url,
info: content.info,
},
event_id: event.getId() || '',
sender: event.getSender() || '',
});
}
}
}
return messages;
}
/**
* Finds a file message in a room's timeline by file name.
*
* Searches for a file message in the room's timeline that matches the specified
* file name. Useful for verifying that file messages appear correctly on
* the remote side in federation tests.
*
* @param roomName - The display name of the room to search
* @param fileName - The file name to find
* @param options - Retry configuration options
* @param options.maxRetries - Maximum number of retry attempts (default: 5)
* @param options.delay - Delay between retries in milliseconds (default: 1000)
* @param options.initialDelay - Initial delay before first attempt in milliseconds (default: 2000)
* @returns The file message event if found, null otherwise
*/
async findFileMessageInRoom(
roomName: string,
fileName: string,
options: { maxRetries?: number; delay?: number; initialDelay?: number } = {},
): Promise<{
content: { body: string; msgtype: string; url?: string; info?: any };
event_id: string;
sender: string;
} | null> {
const { maxRetries = 5, delay = 1000, initialDelay = 2000 } = options;
if (initialDelay > 0) {
await wait(initialDelay);
}
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const messages = this.getRoomFileMessages(roomName);
const message = messages.find((msg) => msg.content.body === fileName || msg.content.body?.includes(fileName));
if (message) {
return message;
}
if (attempt < maxRetries) {
await wait(delay);
}
} catch (error) {
console.warn(`Attempt ${attempt} to find file message in room failed:`, error);
if (attempt < maxRetries) {
await wait(delay);
}
}
}
return null;
}
/**
* Downloads a file from Matrix and verifies it matches the original file using binary comparison.
*
* Uses the Matrix JS SDK to download media files from the homeserver and compares
* them byte-by-byte with the original file. The MXC URI format is: mxc://serverName/mediaId
*
* @param mxcUri - The MXC URI of the media to download (e.g., "mxc://serverName/mediaId")
* @param originalFilePath - Path to the original file to compare against
* @returns Promise resolving to true if files match byte-by-byte
* @throws Error if client is not initialized or download fails
*/
async downloadFileAndCompareBinary(mxcUri: string, originalFilePath: string): Promise<boolean> {
if (!this.matrixClient) {
throw new Error('Matrix client is not initialized');
}
try {
// Use Matrix JS SDK's mxcUrlToHttp with useAuthentication=true to get the client v1 endpoint
// This generates: https://hs1/_matrix/client/v1/media/download/{serverName}/{mediaId}?allow_redirect=true
// Parameters: mxcUrl, width, height, resizeMethod, allowDirectLinks, allowRedirects, useAuthentication
const downloadUrl = this.matrixClient.mxcUrlToHttp(mxcUri, undefined, undefined, undefined, false, true, true);
if (!downloadUrl) {
throw new Error(`Failed to convert MXC URI to HTTP URL: ${mxcUri}`);
}
// Add allow_remote=true parameter to ensure Synapse fetches from remote servers
const urlWithRemote = new URL(downloadUrl);
urlWithRemote.searchParams.set('allow_remote', 'true');
const finalDownloadUrl = urlWithRemote.toString();
const accessToken = this.matrixClient.getAccessToken();
if (!accessToken) {
throw new Error('Matrix client access token not available');
}
const response = await fetch(finalDownloadUrl, {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
},
redirect: 'follow',
});
if (!response.ok) {
throw new Error(`Failed to download media: ${response.status} ${response.statusText}`);
}
const arrayBuffer = await response.arrayBuffer();
const downloadedBuffer = Buffer.from(arrayBuffer);
// Read the original file
const originalBuffer = fs.readFileSync(originalFilePath);
// Compare buffers byte-by-byte
if (downloadedBuffer.length !== originalBuffer.length) {
return false;
}
return downloadedBuffer.equals(originalBuffer);
} catch (error) {
throw new Error(`Failed to download and compare media from ${mxcUri}: ${error}`);
}
}
/**
* Closes the Matrix client connection and cleans up resources.
*

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -0,0 +1 @@
sample text file

View File

@ -0,0 +1,121 @@
/**
* Jest setup file that automatically wraps describe and it/test functions
* to register suites and tests with Qase.
*
* The Qase Jest reporter reports the directory structure up from the tests directory,
* making it not consistent with the test suite structure we currently follow in Qase.
* The solution is to wrap describe and it/test functions to automatically set the suite
* at the very start of the test to what we really want the reporting structure to be.
*
* This file is loaded via setupFilesAfterEnv in jest.config.federation.ts.
* Qase integration is only enabled when QASE_TESTOPS_JEST_API_TOKEN is set.
*/
import { qase } from 'jest-qase-reporter/jest';
const ROOT_SUITE = 'Rocket.Chat Federation Automation';
/**
* Stack to track the current suite path hierarchy
*/
const suitePathStack: string[] = [];
/**
* Store the original Jest describe function before we replace it
*/
const originalDescribe = global.describe;
/**
* Gets the full suite path including root
*/
function getFullSuitePath(): string {
return [ROOT_SUITE, ...suitePathStack].join('\t');
}
/**
* Wraps describe to automatically track suite hierarchy and set suite for tests
*/
function describeImpl(name: string, fn: () => void): void {
suitePathStack.push(name);
const currentPath = getFullSuitePath();
originalDescribe(name, () => {
// Add beforeEach to set suite for all tests in this describe block
// This must be called before the test runs so the reporter picks it up
global.beforeEach(() => {
qase.suite(currentPath);
});
// Store current it and test wrappers (they might be wrapped by parent describe)
const currentIt = global.it;
const currentTest = global.test;
// Wrap it() to automatically set suite at the very start
global.it = ((testName: any, fn?: any, timeout?: number) => {
// Handle qase-wrapped test names (qase returns a string)
if (typeof testName === 'string' && fn) {
return currentIt(
testName,
async () => {
// Set suite immediately at the start of the test
qase.suite(currentPath);
// Call the original test function and return the result
return fn();
},
timeout,
);
}
// Handle cases where testName might be a number or other type
return currentIt(testName, fn, timeout);
}) as typeof global.it;
// Wrap test() to automatically set suite at the very start
global.test = ((testName: any, fn?: any, timeout?: number) => {
if (typeof testName === 'string' && fn) {
return currentTest(
testName,
async () => {
// Set suite immediately at the start of the test
qase.suite(currentPath);
// Call the original test function and return the result
return fn();
},
timeout,
);
}
return currentTest(testName, fn, timeout);
}) as typeof global.test;
// Execute the describe block
fn();
// Restore previous wrappers
global.it = currentIt;
global.test = currentTest;
});
suitePathStack.pop();
}
// Only apply qase wrapping if the environment variable is set
if (process.env.QASE_TESTOPS_JEST_API_TOKEN) {
// Replace global describe with our wrapper
(global as any).describe = Object.assign(describeImpl, {
skip: (name: string, fn: () => void) => {
suitePathStack.push(name);
try {
originalDescribe.skip(name, fn);
} finally {
suitePathStack.pop();
}
},
only: (name: string, fn: () => void) => {
suitePathStack.push(name);
try {
originalDescribe.only(name, fn);
} finally {
suitePathStack.pop();
}
},
}) as typeof global.describe;
}

View File

@ -21,6 +21,7 @@
"@types/chart.js": "^2.9.41",
"@types/js-yaml": "^4.0.9",
"@types/node": "~22.16.5",
"jest-qase-reporter": "^2.1.3",
"ts-node": "^10.9.2",
"turbo": "~2.6.1",
"typescript": "~5.9.3"

View File

@ -25676,6 +25676,20 @@ __metadata:
languageName: node
linkType: hard
"jest-qase-reporter@npm:^2.1.3":
version: 2.1.3
resolution: "jest-qase-reporter@npm:2.1.3"
dependencies:
lodash.get: "npm:^4.4.2"
lodash.has: "npm:^4.5.2"
qase-javascript-commons: "npm:~2.4.2"
uuid: "npm:^9.0.0"
peerDependencies:
jest: ">=28.0.0"
checksum: 10/1c19643adaffd514674d1dbdc92d6377beb859d4036228133a8ad2dadadbc879bc65b74df5f14ad371b5e269976720da3b787afbfba034ff8be0f1a1792324c7
languageName: node
linkType: hard
"jest-regex-util@npm:30.0.1":
version: 30.0.1
resolution: "jest-regex-util@npm:30.0.1"
@ -26951,6 +26965,13 @@ __metadata:
languageName: node
linkType: hard
"lodash.has@npm:^4.5.2":
version: 4.5.2
resolution: "lodash.has@npm:4.5.2"
checksum: 10/35c0862e715bc22528dd3cd34f1e66d25d58f0ecef9a43aa409fb7ddebaf6495cb357ae242f141e4b2325258f4a6bafdd8928255d51f1c0a741ae9b93951c743
languageName: node
linkType: hard
"lodash.includes@npm:^4.3.0":
version: 4.3.0
resolution: "lodash.includes@npm:4.3.0"
@ -32872,6 +32893,7 @@ __metadata:
"@types/js-yaml": "npm:^4.0.9"
"@types/node": "npm:~22.16.5"
"@types/stream-buffers": "npm:^3.0.8"
jest-qase-reporter: "npm:^2.1.3"
node-gyp: "npm:^10.2.0"
ts-node: "npm:^10.9.2"
turbo: "npm:~2.6.1"