snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-16 01:21:02 +05:00
commit 30810eecd7
10161 changed files with 987110 additions and 0 deletions

View File

@@ -0,0 +1,279 @@
import { faker } from '@faker-js/faker';
import { expect } from 'chai';
import { before, describe, it } from 'mocha';
import proxyquire from 'proxyquire';
import sinon from 'sinon';
import type { MessageData } from '../../../../../server/lib/dataExport/exportRoomMessagesToFile';
import { exportMessagesMock } from '../../../app/apps/server/mocks/data/messages.data';
// Create stubs for dependencies
const stubs = {
findPaginatedMessages: sinon.stub(),
mkdir: sinon.stub(),
writeFile: sinon.stub(),
findPaginatedMessagesCursor: sinon.stub(),
findPaginatedMessagesTotal: sinon.stub(),
translateKey: sinon.stub(),
settings: sinon.stub(),
};
const { getMessageData, exportRoomMessages, exportMessageObject } = proxyquire
.noCallThru()
.load('../../../../../server/lib/dataExport/exportRoomMessagesToFile.ts', {
'@rocket.chat/models': {
Messages: {
findPaginated: stubs.findPaginatedMessages,
},
},
'fs/promises': {
mkdir: stubs.mkdir,
writeFile: stubs.writeFile,
},
'../i18n': {
i18n: {
t: stubs.translateKey,
},
},
'../../settings': {
settings: stubs.settings,
},
});
describe('Export - exportMessageObject', () => {
let messagesData: MessageData[];
const translationPlaceholder = 'translation-placeholder';
before(() => {
stubs.translateKey.returns(translationPlaceholder);
messagesData = exportMessagesMock.map((message) => getMessageData(message, false));
});
it('should only stringify message object when exporting message as json', async () => {
const result = await exportMessageObject('json', messagesData[3]);
expect(result).to.be.a.string;
expect(result).to.equal(JSON.stringify(messagesData[3]));
});
it('should correctly add tags when exporting plain text message object as html', async () => {
const result = await exportMessageObject('html', messagesData[3]);
expect(result).to.be.a.string;
expect(result).to.equal(
`<p><strong>${messagesData[3].username}</strong> (${new Date(messagesData[3].ts).toUTCString()}):<br/>\n${messagesData[3].msg}\n</p>`,
);
});
it('should correctly format system messages when exporting message object as html', async () => {
const result = await exportMessageObject('html', messagesData[0]);
expect(messagesData[0].msg).to.equal(translationPlaceholder);
expect(result).to.be.a.string;
expect(result).to.equal(
`<p><strong>${messagesData[0].username}</strong> (${new Date(messagesData[0].ts).toUTCString()}):<br/>\n<i>${
messagesData[0].msg
}</i>\n</p>`,
);
});
it('should correctly format non italic system messages when exporting message object as html', async () => {
const result = await exportMessageObject('html', messagesData[4]);
expect(messagesData[4].msg).to.equal(translationPlaceholder);
expect(result).to.be.a.string;
expect(result).to.equal(
`<p><strong>${messagesData[4].username}</strong> (${new Date(messagesData[4].ts).toUTCString()}):<br/>\n${messagesData[4].msg}\n</p>`,
);
});
it('should correctly reference file when exporting a message object with an attachment as html', async () => {
const result = await exportMessageObject('html', messagesData[1], [exportMessagesMock[1].file]);
expect(result).to.be.a.string;
expect(result).to.equal(
`<p><strong>${messagesData[1].username}</strong> (${new Date(messagesData[1].ts).toUTCString()}):<br/>\n${
messagesData[1].msg
}\n<br/><a href="./assets/${exportMessagesMock[1].file?._id}-${exportMessagesMock[1].file?.name}">${
messagesData[1].attachments?.[0].title
}</a>\n</p>`,
);
});
it('should use fallback attachment description when no title is provided on message object export as html', async () => {
const result = await exportMessageObject('html', messagesData[2], [exportMessagesMock[2].file]);
expect(stubs.translateKey.calledWith('Message_Attachments')).to.be.true;
expect(result).to.be.a.string;
expect(result).to.equal(
`<p><strong>${messagesData[2].username}</strong> (${new Date(messagesData[2].ts).toUTCString()}):<br/>\n${
exportMessagesMock[1].msg
}\n<br/><a href="./assets/${exportMessagesMock[2].file?._id}-${
exportMessagesMock[2].file?.name
}">${translationPlaceholder}</a>\n</p>`,
);
});
});
describe('Export - exportMessageObject HTML escaping (XSS prevention)', () => {
const ts = new Date('2020-01-01T00:00:00.000Z');
it('should escape HTML in the message body when exporting as html', async () => {
const result = await exportMessageObject('html', {
msg: '<img src=x onerror="alert(1)">',
username: 'attacker',
ts,
});
expect(result).to.contain('&lt;img src=x onerror=&quot;alert(1)&quot;&gt;');
expect(result).to.not.contain('<img src=x');
});
it('should escape HTML in the username when exporting as html', async () => {
const result = await exportMessageObject('html', {
msg: 'hello',
username: '<script>alert(1)</script>',
ts,
});
expect(result).to.contain('&lt;script&gt;alert(1)&lt;/script&gt;');
expect(result).to.not.contain('<script>');
});
it('should escape HTML inside italicized (system) messages when exporting as html', async () => {
const result = await exportMessageObject('html', {
msg: '<img src=x onerror=alert(1)>',
username: 'attacker',
ts,
type: 'wm',
});
expect(result).to.contain('<i>&lt;img src=x onerror=alert(1)&gt;</i>');
expect(result).to.not.contain('<img src=x');
});
it('should escape HTML in the attachment description and prevent href attribute breakout', async () => {
const result = await exportMessageObject(
'html',
{
msg: 'file',
username: 'attacker',
ts,
attachments: [
{
type: 'file',
title: '<img src=x onerror=alert(1)>',
title_link: '/file-upload/evil-id/x',
},
] as any,
},
[{ _id: 'evil-id', name: '"><img src=x onerror=alert(1)>.png' } as any],
);
expect(result).to.contain('&lt;img src=x');
expect(result).to.not.contain('<img src=x');
// the crafted filename must not break out of the href="" attribute
expect(result).to.not.contain('"><img');
});
it('should not escape plain JSON exports (escaping is html-only)', async () => {
const messageObject = { msg: '<b>x</b>', username: 'attacker', ts };
const result = await exportMessageObject('json', messageObject);
expect(result).to.equal(JSON.stringify(messageObject));
});
});
describe('Export - exportRoomMessages', () => {
const totalMessages = 10;
const userData = {
_id: faker.database.mongodbObjectId(),
name: faker.person.fullName(),
username: faker.internet.userName(),
};
before(() => {
stubs.findPaginatedMessagesCursor.resolves(exportMessagesMock);
stubs.findPaginatedMessagesTotal.resolves(totalMessages);
stubs.findPaginatedMessages.returns({
cursor: { toArray: stubs.findPaginatedMessagesCursor },
totalCount: stubs.findPaginatedMessagesTotal(),
});
stubs.translateKey.returns('translated-placeholder-uj');
});
it('should correctly export multiple messages to result when exporting room as json', async () => {
const result = await exportRoomMessages('test-rid', 'json', 0, 100, userData);
expect(stubs.translateKey.calledWith('User_joined_the_channel')).to.be.true;
expect(result).to.be.an('object');
expect(result).to.have.property('total', totalMessages);
expect(result).to.have.property('exported', exportMessagesMock.length);
expect(result).to.have.property('messages').that.is.an('array').of.length(exportMessagesMock.length);
const messagesWithFiles = exportMessagesMock.filter((message) => message.file);
expect(result).to.have.property('uploads').that.is.an('array').of.length(messagesWithFiles.length);
});
it('should correctly export multiple messages to result when exporting room as html', async () => {
const result = await exportRoomMessages('test-rid', 'html', 0, 100, userData);
expect(stubs.translateKey.calledWith('User_joined_the_channel')).to.be.true;
expect(result).to.be.an('object');
expect(result).to.have.property('total', totalMessages);
expect(result).to.have.property('exported', exportMessagesMock.length);
expect(result).to.have.property('messages').that.is.an('array').of.length(exportMessagesMock.length);
const messagesWithFiles = exportMessagesMock.filter((message) => message.file);
expect(result).to.have.property('uploads').that.is.an('array').of.length(messagesWithFiles.length);
});
it('should export multiple files and filter out thumbnails', async () => {
const message = {
_id: faker.database.mongodbObjectId(),
rid: 'test-rid',
ts: new Date(),
u: { _id: faker.database.mongodbObjectId(), username: 'testuser' },
msg: 'Message with files',
files: [
{ _id: 'file-1', name: 'photo.jpg', type: 'image/jpeg', size: 500000 },
{ _id: 'file-2', name: 'doc.pdf', type: 'application/pdf', size: 10000 },
{ _id: 'thumb-1', name: 'photo_thumb.jpg', type: 'image/jpeg', size: 5000, typeGroup: 'thumb' },
],
attachments: [{ type: 'file', title: 'photo.jpg', title_link: '/file-upload/file-1/photo.jpg' }],
};
stubs.findPaginatedMessagesCursor.resolves([message]);
stubs.findPaginatedMessagesTotal.resolves(1);
stubs.findPaginatedMessages.returns({
cursor: { toArray: stubs.findPaginatedMessagesCursor },
totalCount: stubs.findPaginatedMessagesTotal(),
});
const result = await exportRoomMessages('test-rid', 'html', 0, 100, userData);
expect(result.uploads).to.have.length(2);
expect(result.uploads.some((f: { typeGroup?: string }) => f.typeGroup === 'thumb')).to.be.false;
});
it('should fallback to msg.file when msg.files is not available', async () => {
const message = {
_id: faker.database.mongodbObjectId(),
rid: 'test-rid',
ts: new Date(),
u: { _id: faker.database.mongodbObjectId(), username: 'testuser' },
msg: 'Old format message',
file: { _id: 'single-file', name: 'doc.pdf', type: 'application/pdf', size: 10000 },
attachments: [{ type: 'file', title: 'doc.pdf', title_link: '/file-upload/single-file/doc.pdf' }],
};
stubs.findPaginatedMessagesCursor.resolves([message]);
stubs.findPaginatedMessagesTotal.resolves(1);
stubs.findPaginatedMessages.returns({
cursor: { toArray: stubs.findPaginatedMessagesCursor },
totalCount: stubs.findPaginatedMessagesTotal(),
});
const result = await exportRoomMessages('test-rid', 'html', 0, 100, userData);
expect(result.uploads).to.have.length(1);
expect(result.uploads[0]._id).to.equal('single-file');
});
});

View File

@@ -0,0 +1,153 @@
import { expect } from 'chai';
import { before, describe, it } from 'mocha';
import proxyquire from 'proxyquire';
import sinon from 'sinon';
// Create stubs for dependencies
const stubs = {
findOneUserById: sinon.stub(),
randomId: sinon.stub(),
stat: sinon.stub(),
getStore: sinon.stub(),
insertFileStub: sinon.stub(),
createReadStream: sinon.stub(),
};
const { uploadZipFile } = proxyquire.noCallThru().load('../../../../../server/lib/dataExport/uploadZipFile.ts', {
'@rocket.chat/models': {
Users: {
findOneById: stubs.findOneUserById,
},
},
'@rocket.chat/random': {
Random: {
id: stubs.randomId,
},
},
'node:fs/promises': {
stat: stubs.stat,
},
'node:fs': {
createReadStream: stubs.createReadStream,
},
'../media/file-upload': {
FileUpload: {
getStore: stubs.getStore,
},
},
});
describe('Export - uploadZipFile', () => {
const randomId = 'random-id';
const fileStat = 100;
const userName = 'John Doe';
const userUsername = 'john.doe';
const userId = 'user-id';
const filePath = 'random-path';
before(() => {
stubs.findOneUserById.returns({ name: userName });
stubs.stat.returns({ size: fileStat });
stubs.randomId.returns(randomId);
stubs.getStore.returns({ insert: stubs.insertFileStub });
stubs.insertFileStub.callsFake((details) => ({ _id: details._id, name: details.name }));
});
it('should correctly build file name for json exports', async () => {
const result = await uploadZipFile(filePath, userId, 'json');
expect(stubs.findOneUserById.calledWith(userId)).to.be.true;
expect(stubs.stat.calledWith(filePath)).to.be.true;
expect(stubs.createReadStream.calledWith(filePath)).to.be.true;
expect(stubs.getStore.calledWith('UserDataFiles')).to.be.true;
expect(
stubs.insertFileStub.calledWith(
sinon.match({
_id: randomId,
userId,
type: 'application/zip',
size: fileStat,
}),
),
).to.be.true;
expect(result).to.have.property('_id', randomId);
expect(result).to.have.property('name').that.is.a.string;
const fileName: string = result.name;
expect(fileName.endsWith(encodeURIComponent(`${userName}-data-${randomId}.zip`))).to.be.true;
});
it('should correctly build file name for html exports', async () => {
const result = await uploadZipFile(filePath, userId, 'html');
expect(stubs.findOneUserById.calledWith(userId)).to.be.true;
expect(stubs.stat.calledWith(filePath)).to.be.true;
expect(stubs.createReadStream.calledWith(filePath)).to.be.true;
expect(stubs.getStore.calledWith('UserDataFiles')).to.be.true;
expect(
stubs.insertFileStub.calledWith(
sinon.match({
_id: randomId,
userId,
type: 'application/zip',
size: fileStat,
}),
),
).to.be.true;
expect(result).to.have.property('_id', randomId);
expect(result).to.have.property('name').that.is.a.string;
const fileName: string = result.name;
expect(fileName.endsWith(encodeURIComponent(`${userName}-${randomId}.zip`))).to.be.true;
});
it("should use username as a fallback in the zip file name when user's name is not defined", async () => {
stubs.findOneUserById.returns({ username: userUsername });
const result = await uploadZipFile(filePath, userId, 'html');
expect(stubs.findOneUserById.calledWith(userId)).to.be.true;
expect(stubs.stat.calledWith(filePath)).to.be.true;
expect(stubs.createReadStream.calledWith(filePath)).to.be.true;
expect(stubs.getStore.calledWith('UserDataFiles')).to.be.true;
expect(
stubs.insertFileStub.calledWith(
sinon.match({
_id: randomId,
userId,
type: 'application/zip',
size: fileStat,
}),
),
).to.be.true;
expect(result).to.have.property('_id', randomId);
expect(result).to.have.property('name').that.is.a.string;
const fileName: string = result.name;
expect(fileName.endsWith(`${userUsername}-${randomId}.zip`)).to.be.true;
});
it("should use userId as a fallback in the zip file name when user's name and username are not defined", async () => {
stubs.findOneUserById.returns(undefined);
const result = await uploadZipFile(filePath, userId, 'html');
expect(stubs.findOneUserById.calledWith(userId)).to.be.true;
expect(stubs.stat.calledWith(filePath)).to.be.true;
expect(stubs.createReadStream.calledWith(filePath)).to.be.true;
expect(stubs.getStore.calledWith('UserDataFiles')).to.be.true;
expect(
stubs.insertFileStub.calledWith(
sinon.match({
_id: randomId,
userId,
type: 'application/zip',
size: fileStat,
}),
),
).to.be.true;
expect(result).to.have.property('_id', randomId);
expect(result).to.have.property('name').that.is.a.string;
const fileName: string = result.name;
expect(fileName.endsWith(`${userId}-${randomId}.zip`)).to.be.true;
});
});