mirror of
https://github.com/RocketChat/Rocket.Chat.git
synced 2025-12-27 22:40:49 +00:00
test: message media tests (#37528)
This commit is contained in:
parent
ff536cb437
commit
951a9bcf9c
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@ -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:
|
||||
|
||||
@ -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,
|
||||
|
||||
170
apps/meteor/tests/data/file.helper.ts
Normal file
170
apps/meteor/tests/data/file.helper.ts
Normal 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);
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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');
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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.
|
||||
*
|
||||
|
||||
BIN
ee/packages/federation-matrix/tests/resources/sample_audio.mp3
Normal file
BIN
ee/packages/federation-matrix/tests/resources/sample_audio.mp3
Normal file
Binary file not shown.
BIN
ee/packages/federation-matrix/tests/resources/sample_image.webp
Normal file
BIN
ee/packages/federation-matrix/tests/resources/sample_image.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
BIN
ee/packages/federation-matrix/tests/resources/sample_pdf.pdf
Normal file
BIN
ee/packages/federation-matrix/tests/resources/sample_pdf.pdf
Normal file
Binary file not shown.
@ -0,0 +1 @@
|
||||
sample text file
|
||||
BIN
ee/packages/federation-matrix/tests/resources/sample_video.webm
Normal file
BIN
ee/packages/federation-matrix/tests/resources/sample_video.webm
Normal file
Binary file not shown.
121
ee/packages/federation-matrix/tests/setup-qase.ts
Normal file
121
ee/packages/federation-matrix/tests/setup-qase.ts
Normal 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;
|
||||
}
|
||||
@ -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"
|
||||
|
||||
22
yarn.lock
22
yarn.lock
@ -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"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user