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,430 @@
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import AdmZip from 'adm-zip';
import { expect } from 'chai';
import { parse } from 'csv-parse/sync';
import { after, before, beforeEach, describe, it } from 'mocha';
import proxyquire from 'proxyquire';
import sinon from 'sinon';
import { ProgressStep } from '../../../../../app/importer/lib/ImporterProgressStep';
const converter = {
clearImportData: sinon.stub(),
addChannel: sinon.stub(),
addUser: sinon.stub(),
addMessage: sinon.stub(),
addContact: sinon.stub(),
};
const logger = {
debug: sinon.stub(),
warn: sinon.stub(),
error: sinon.stub(),
};
const progressUpdated = sinon.stub();
const updateProgress = sinon.stub();
const updateRecord = sinon.stub();
const incrementValueById = sinon.stub();
const notifyOnSettingChanged = sinon.stub();
const findOneByUsername = sinon.stub();
const addImportIds = sinon.stub();
class MockImporter {
protected AdmZip = AdmZip;
protected logger = logger;
protected converter = converter;
public importRecord: any;
public progress: any;
constructor(_info: any, importRecord: any) {
this.importRecord = importRecord;
this.progress = { step: ProgressStep.PREPARING_STARTED, count: { total: 0 } };
}
async updateProgress(step: string) {
updateProgress(step);
this.progress.step = step;
return this.progress;
}
async updateRecord(fields: any) {
updateRecord(fields);
return this.importRecord;
}
async addCountToTotal(count: number) {
this.progress.count.total += count;
return this.progress;
}
getProgress() {
return this.progress;
}
}
const { CsvImporter } = proxyquire.noCallThru().load('../../../../../server/lib/import/csv/CsvImporter', {
'@rocket.chat/models': {
Settings: { incrementValueById },
Users: { findOneByUsername, addImportIds },
},
'csv-parse/lib/sync': { parse },
'..': {
Importer: MockImporter,
ProgressStep,
ImporterWebsocket: { progressUpdated },
},
'../../notifyListener': { notifyOnSettingChanged },
});
const allStubs = [
...Object.values(converter),
...Object.values(logger),
progressUpdated,
updateProgress,
updateRecord,
incrementValueById,
notifyOnSettingChanged,
findOneByUsername,
addImportIds,
];
type ZipEntry = { name: string; content?: string };
describe('CsvImporter', () => {
let tmpDir: string;
let zipCount = 0;
before(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rc-csv-importer-'));
});
after(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});
beforeEach(() => {
allStubs.forEach((stub) => stub.reset());
converter.clearImportData.resolves();
converter.addChannel.resolves();
converter.addUser.resolves();
converter.addMessage.resolves();
converter.addContact.resolves();
incrementValueById.resolves(null);
findOneByUsername.resolves(null);
addImportIds.resolves();
});
const writeZip = (entries: ZipEntry[]): string => {
const zip = new AdmZip();
for (const entry of entries) {
zip.addFile(entry.name, Buffer.from(entry.content ?? ''));
}
zipCount += 1;
const filePath = path.join(tmpDir, `archive-${zipCount}.zip`);
zip.writeZip(filePath);
return filePath;
};
const runImport = async (entries: ZipEntry[]) => {
const importer = new CsvImporter({ key: 'csv', name: 'CSV' }, { _id: 'import-id' });
const progress = await importer.prepareUsingLocalFile(writeZip(entries));
return { importer, progress };
};
const rates = () => progressUpdated.getCalls().map((call) => call.args[0].rate);
const CHANNELS_CSV = ' general , admin , Public , alice ; bob \nsecret,owner,Private,carol;;dave;';
const USERS_CSV = ' alice , alice@example.com , Alice Smith \nbob,bob@example.com,Bob Jones';
const CONTACTS_CSV = 'name,emails,phones\nJane Doe,jane@example.com,+5511999';
const CHANNEL_MESSAGES_CSV = 'alice,1700000000000,hello there\nbob,1700000001000,hi';
const DM_MESSAGES_CSV = 'alice,bob,1700000002000,dm from alice\nbob,alice,1700000003000,dm from bob';
const fullArchive = (): ZipEntry[] => [
{ name: 'channels.csv', content: CHANNELS_CSV },
{ name: 'users.csv', content: USERS_CSV },
{ name: 'contacts.csv', content: CONTACTS_CSV },
{ name: 'general/messages.csv', content: CHANNEL_MESSAGES_CSV },
{ name: 'directmessages/messages.csv', content: DM_MESSAGES_CSV },
];
describe('archive setup and progress', () => {
it('should clear previously imported data before reading the archive', async () => {
await runImport(fullArchive());
expect(converter.clearImportData.calledOnce).to.be.true;
sinon.assert.callOrder(converter.clearImportData, converter.addUser);
});
it('should not emit a progress update when the calculated rate did not increase', async () => {
const entries = Array.from({ length: 1001 }, (_, index) => ({ name: `__MACOSX/ignored-${index}`, content: '' }));
await runImport(entries);
expect(rates().filter((rate) => rate === 0)).to.have.lengthOf(1);
expect(progressUpdated.callCount).to.equal(1002);
});
it('should log a progress update failure and keep importing', async () => {
progressUpdated.onSecondCall().throws(new Error('streamer is down'));
const { progress } = await runImport(fullArchive());
expect(logger.error.calledWithMatch({ msg: 'Error while increasing CSV import progress' })).to.be.true;
expect(converter.addUser.callCount).to.equal(2);
expect(progress.step).to.not.equal(ProgressStep.ERROR);
expect(rates()[rates().length - 1]).to.equal(100);
});
});
describe('archive filtering and room identification', () => {
it('should ignore __MACOSX and directory entries, advancing the progress rate for every entry', async () => {
await runImport([
{ name: '__MACOSX/general/messages.csv', content: CHANNEL_MESSAGES_CSV },
{ name: 'general/', content: '' },
{ name: 'general/messages.csv', content: CHANNEL_MESSAGES_CSV },
{ name: 'readme.txt', content: 'not a csv the importer knows about' },
]);
expect(converter.addMessage.callCount).to.equal(2);
expect(updateRecord.calledWithMatch({ messagesstatus: 'general/messages' })).to.be.true;
expect(updateRecord.calledWithMatch({ messagesstatus: 'general/' })).to.be.false;
expect(rates()).to.deep.equal([0, 25, 50, 75, 100, 100]);
});
it('should reuse the same generated room id for every reference to a room name', async () => {
await runImport([
{ name: 'channels.csv', content: 'general,admin,public,alice' },
{ name: 'general/first.csv', content: 'alice,1700000000000,one' },
{ name: 'general/second.csv', content: 'bob,1700000001000,two' },
]);
const channelId = converter.addChannel.firstCall.args[0].importIds[0];
expect(channelId).to.be.a('string').and.not.empty;
expect(converter.addMessage.getCalls().map((call) => call.args[0].rid)).to.deep.equal([channelId, channelId]);
});
});
describe('channel parsing', () => {
it('should parse channels.csv case-insensitively, trimming every field and dropping empty members', async () => {
await runImport([{ name: 'Channels.CSV', content: CHANNELS_CSV }]);
expect(updateProgress.calledWith(ProgressStep.PREPARING_CHANNELS)).to.be.true;
expect(converter.addChannel.firstCall.args[0]).to.deep.include({
name: 'general',
u: { _id: 'admin' },
users: ['alice', 'bob'],
});
expect(converter.addChannel.secondCall.args[0].users).to.deep.equal(['carol', 'dave']);
expect(updateRecord.calledWithExactly({ 'count.channels': 2 })).to.be.true;
});
it('should map private channels to p and every other channel to c', async () => {
await runImport([{ name: 'channels.csv', content: CHANNELS_CSV }]);
expect(converter.addChannel.firstCall.args[0].t).to.equal('c');
expect(converter.addChannel.secondCall.args[0].t).to.equal('p');
});
});
describe('user and contact parsing', () => {
it('should parse users.csv case-insensitively into importer user records', async () => {
await runImport([{ name: 'Users.CSV', content: USERS_CSV }]);
expect(updateProgress.calledWith(ProgressStep.PREPARING_USERS)).to.be.true;
expect(converter.addUser.callCount).to.equal(2);
expect(converter.addUser.firstCall.args[0]).to.deep.equal({
type: 'user',
importIds: ['alice'],
emails: ['alice@example.com'],
username: 'alice',
name: 'Alice Smith',
});
expect(updateRecord.calledWithExactly({ 'count.users': 2 })).to.be.true;
});
it('should delegate contacts.csv rows to the contact importer and count the accepted ones', async () => {
await runImport([{ name: 'Contacts.CSV', content: `${CONTACTS_CSV}\n,,` }]);
expect(updateProgress.calledWith(ProgressStep.PREPARING_CONTACTS)).to.be.true;
expect(converter.addContact.callCount).to.equal(1);
expect(converter.addContact.firstCall.args[0]).to.deep.include({
name: 'Jane Doe',
emails: ['jane@example.com'],
phones: ['+5511999'],
});
expect(updateRecord.calledWithExactly({ 'count.contacts': 1 })).to.be.true;
});
});
describe('message parsing', () => {
it('should move to the message preparation step only once for the whole archive', async () => {
await runImport([
{ name: 'general/first.csv', content: CHANNEL_MESSAGES_CSV },
{ name: 'general/second.csv', content: CHANNEL_MESSAGES_CSV },
]);
expect(updateProgress.withArgs(ProgressStep.PREPARING_MESSAGES).callCount).to.equal(1);
});
it('should skip a message file with invalid csv syntax and warn about it', async () => {
await runImport([
{ name: 'general/broken.csv', content: 'alice,1700000000000,"unclosed' },
{ name: 'general/valid.csv', content: CHANNEL_MESSAGES_CSV },
]);
expect(logger.warn.calledWithMatch({ msg: 'The file contains invalid syntax', entryName: 'general/broken.csv' })).to.be.true;
expect(converter.addMessage.callCount).to.equal(2);
expect(updateRecord.calledWithMatch({ messagesstatus: 'general/broken' })).to.be.false;
});
it('should convert a channel message with its room, author, timestamp and text', async () => {
await runImport([{ name: 'general/messages.csv', content: CHANNEL_MESSAGES_CSV }]);
const message = converter.addMessage.firstCall.args[0];
expect(Object.keys(message).sort()).to.deep.equal(['msg', 'rid', 'ts', 'u']);
expect(message.rid).to.be.a('string').and.not.empty;
expect(message).to.deep.include({
u: { _id: 'alice' },
ts: new Date(1700000000000),
msg: 'hello there',
});
expect(converter.addMessage.secondCall.args[0]).to.deep.include({
rid: message.rid,
u: { _id: 'bob' },
ts: new Date(1700000001000),
msg: 'hi',
});
});
it('should convert direct messages into a single room per participant pair, whatever the username order', async () => {
await runImport([{ name: 'directmessages/messages.csv', content: DM_MESSAGES_CSV }]);
expect(converter.addChannel.calledOnce).to.be.true;
expect(converter.addChannel.firstCall.args[0]).to.deep.equal({
importIds: ['alice/bob'],
users: ['alice', 'bob'],
t: 'd',
});
expect(converter.addMessage.firstCall.args[0]).to.deep.equal({
rid: 'alice/bob',
u: { _id: 'alice' },
ts: new Date(1700000002000),
msg: 'dm from alice',
});
expect(converter.addMessage.getCalls().map((call) => call.args[0].rid)).to.deep.equal(['alice/bob', 'alice/bob']);
});
it('should skip a direct message row that has no other participant', async () => {
await runImport([{ name: 'directmessages/messages.csv', content: 'alice,,1700000002000,orphan' }]);
expect(converter.addChannel.called).to.be.false;
expect(converter.addMessage.called).to.be.false;
});
it('should accumulate the message count across files, clearing the file status after each one', async () => {
await runImport([
{ name: 'general/first.csv', content: CHANNEL_MESSAGES_CSV },
{ name: 'general/second.csv', content: 'alice,1700000004000,third' },
]);
expect(updateRecord.calledWithExactly({ 'count.messages': 2, 'messagesstatus': null })).to.be.true;
expect(updateRecord.calledWithExactly({ 'count.messages': 3, 'messagesstatus': null })).to.be.true;
const statusCalls = updateRecord
.getCalls()
.filter((call) => 'messagesstatus' in call.args[0])
.map((call) => call.args[0].messagesstatus);
expect(statusCalls).to.deep.equal(['general/first', null, 'general/second', null]);
});
});
describe('post-processing', () => {
it('should increment the csv importer setting with the imported user count and notify the listeners', async () => {
const updatedSetting = { _id: 'CSV_Importer_Count', value: 7 };
incrementValueById.resolves(updatedSetting);
await runImport(fullArchive());
expect(incrementValueById.calledOnceWithExactly('CSV_Importer_Count', 2, { returnDocument: 'after' })).to.be.true;
expect(notifyOnSettingChanged.calledOnceWithExactly(updatedSetting)).to.be.true;
});
it('should not touch the csv importer setting when no user was parsed', async () => {
await runImport([{ name: 'general/messages.csv', content: CHANNEL_MESSAGES_CSV }]);
expect(incrementValueById.called).to.be.false;
});
it('should not notify the listeners when the setting update returned nothing', async () => {
await runImport([{ name: 'users.csv', content: USERS_CSV }]);
expect(notifyOnSettingChanged.called).to.be.false;
});
it('should add the import id to existing users that do not carry it yet', async () => {
findOneByUsername.withArgs('alice').resolves({ _id: 'user-alice', importIds: ['other'] });
findOneByUsername.withArgs('dana').resolves({ _id: 'user-dana' });
await runImport([{ name: 'general/messages.csv', content: 'alice,1700000000000,hello\ndana,1700000001000,hi' }]);
expect(addImportIds.calledTwice).to.be.true;
expect(addImportIds.calledWithExactly('user-alice', 'alice')).to.be.true;
expect(addImportIds.calledWithExactly('user-dana', 'dana')).to.be.true;
});
it('should reconcile a direct message recipient that never sent a message', async () => {
findOneByUsername.withArgs('bob').resolves({ _id: 'user-bob', importIds: [] });
await runImport([
{ name: 'users.csv', content: ' alice , alice@example.com , Alice Smith ' },
{ name: 'directmessages/messages.csv', content: 'alice,bob,1700000002000,dm from alice' },
]);
expect(findOneByUsername.calledOnceWithExactly('bob')).to.be.true;
expect(addImportIds.calledOnceWithExactly('user-bob', 'bob')).to.be.true;
});
it('should leave unknown users and users that already carry the import id untouched', async () => {
findOneByUsername.withArgs('carol').resolves({ _id: 'user-carol', importIds: ['carol'] });
await runImport([{ name: 'general/messages.csv', content: 'ghost,1700000000000,hello\ncarol,1700000001000,hi' }]);
expect(findOneByUsername.calledWithExactly('ghost')).to.be.true;
expect(findOneByUsername.calledWithExactly('carol')).to.be.true;
expect(addImportIds.called).to.be.false;
});
it('should add every parsed record to the import total and return the importer progress', async () => {
const { importer, progress } = await runImport(fullArchive());
expect(progress.count.total).to.equal(9);
expect(progress).to.equal(importer.progress);
});
it('should fail the import when the archive holds no valid record', async () => {
const { progress } = await runImport([{ name: '__MACOSX/ignored', content: '' }]);
expect(logger.error.calledWithMatch({ msg: 'No valid record found in the import file.' })).to.be.true;
expect(updateProgress.calledWith(ProgressStep.ERROR)).to.be.true;
expect(progress.step).to.equal(ProgressStep.ERROR);
});
it('should not fail the import when only contacts were found', async () => {
const { progress } = await runImport([{ name: 'contacts.csv', content: CONTACTS_CSV }]);
expect(updateProgress.calledWith(ProgressStep.ERROR)).to.be.false;
expect(progress.step).to.equal(ProgressStep.PREPARING_CONTACTS);
});
});
});

View File

@@ -0,0 +1,357 @@
import { EventEmitter } from 'events';
import { expect } from 'chai';
import { describe, it, beforeEach } from 'mocha';
import proxyquire from 'proxyquire';
import sinon from 'sinon';
class MockIncomingMessage extends EventEmitter {
statusCode: number;
headers: Record<string, string>;
constructor(statusCode: number, headers: Record<string, string> = {}) {
super();
this.statusCode = statusCode;
this.headers = headers;
}
resume() {
// no-op
}
}
class MockClientRequest extends EventEmitter {
// no-op
}
const createMockHttpModule = (responseFactory: () => MockIncomingMessage, errorOnConnect = false) => ({
get: (_url: string, callback: (res: MockIncomingMessage) => void) => {
const req = new MockClientRequest();
if (errorOnConnect) {
setImmediate(() => req.emit('error', new Error('Connection refused')));
} else {
setImmediate(() => {
const res = responseFactory();
callback(res);
});
}
return req;
},
});
const createPendingFileImporter = (httpMock: ReturnType<typeof createMockHttpModule>) => {
const { PendingFileImporter } = proxyquire.noCallThru().load('../../../../../server/lib/import/pending-files/PendingFileImporter', {
'node:http': httpMock,
'node:https': httpMock,
'@rocket.chat/core-services': { api: { broadcast: sinon.stub() } },
'@rocket.chat/models': {
Messages: {
countAllImportedMessagesWithFilesToDownload: sinon.stub().resolves(0),
findAllImportedMessagesWithFilesToDownload: sinon.stub().returns([]),
setImportFileRocketChatAttachment: sinon.stub().resolves(),
},
Users: {
findOneById: sinon.stub().resolves(null),
},
},
'@rocket.chat/random': { Random: { id: () => 'random-id' } },
'../../media/file-upload': { FileUpload: { getStore: sinon.stub(), getPath: sinon.stub() } },
'../../../meteor-methods/messages/sendFileMessage': { parseFileIntoMessageAttachments: sinon.stub() },
'..': {
Importer: class MockImporter {
logger = { debug: sinon.stub(), error: sinon.stub(), info: sinon.stub() };
progress = { count: { total: 0 } };
reportProgress() {
// no-op
}
async updateProgress() {
// no-op
}
async updateRecord() {
// no-op
}
async addCountCompleted() {
// no-op
}
async addCountError() {
// no-op
}
getProgress() {
return this.progress;
}
isCursorNotFoundError() {
return false;
}
},
ProgressStep: {
PREPARING_STARTED: 'preparing_started',
IMPORTING_FILES: 'importing_files',
DONE: 'done',
ERROR: 'error',
},
},
});
const info = { key: 'pending-files', name: 'Pending Files' };
const importRecord = { _id: 'test-import' };
return new PendingFileImporter(info, importRecord, {});
};
describe('PendingFileImporter', () => {
describe('downloadFile', () => {
it('should resolve with buffer on successful 200 response', async () => {
const responseData = Buffer.from('file content');
const httpMock = createMockHttpModule(() => {
const res = new MockIncomingMessage(200, { 'content-type': 'image/png' });
setImmediate(() => {
res.emit('data', responseData);
res.emit('end');
});
return res;
});
const importer = createPendingFileImporter(httpMock);
const details: { type?: string } = {};
// Access private method through the instance
const result = await (importer as any).downloadFile('https://example.com/file.png', details);
expect(result).to.deep.equal(responseData);
expect(details.type).to.equal('image/png');
});
it('should reject on non-2xx status codes (e.g., 302 redirect)', async () => {
const httpMock = createMockHttpModule(() => {
const res = new MockIncomingMessage(302, { location: 'https://example.com/login' });
return res;
});
const importer = createPendingFileImporter(httpMock);
const details: { type?: string } = {};
try {
await (importer as any).downloadFile('https://example.com/file.png', details);
expect.fail('Should have thrown an error');
} catch (err: any) {
expect(err.message).to.include('Unexpected response status 302');
}
});
it('should reject on 404 status code', async () => {
const httpMock = createMockHttpModule(() => {
const res = new MockIncomingMessage(404);
return res;
});
const importer = createPendingFileImporter(httpMock);
const details: { type?: string } = {};
try {
await (importer as any).downloadFile('https://example.com/file.png', details);
expect.fail('Should have thrown an error');
} catch (err: any) {
expect(err.message).to.include('Unexpected response status 404');
}
});
it('should reject on 500 server error', async () => {
const httpMock = createMockHttpModule(() => {
const res = new MockIncomingMessage(500);
return res;
});
const importer = createPendingFileImporter(httpMock);
const details: { type?: string } = {};
try {
await (importer as any).downloadFile('https://example.com/file.png', details);
expect.fail('Should have thrown an error');
} catch (err: any) {
expect(err.message).to.include('Unexpected response status 500');
}
});
it('should reject on connection errors', async () => {
const httpMock = createMockHttpModule(
() => new MockIncomingMessage(200),
true, // errorOnConnect
);
const importer = createPendingFileImporter(httpMock);
const details: { type?: string } = {};
try {
await (importer as any).downloadFile('https://example.com/file.png', details);
expect.fail('Should have thrown an error');
} catch (err: any) {
expect(err.message).to.equal('Connection refused');
}
});
it('should reject on stream errors during download', async () => {
const httpMock = createMockHttpModule(() => {
const res = new MockIncomingMessage(200, { 'content-type': 'image/png' });
setImmediate(() => {
res.emit('data', Buffer.from('partial'));
res.emit('error', new Error('Stream error'));
});
return res;
});
const importer = createPendingFileImporter(httpMock);
const details: { type?: string } = {};
try {
await (importer as any).downloadFile('https://example.com/file.png', details);
expect.fail('Should have thrown an error');
} catch (err: any) {
expect(err.message).to.equal('Stream error');
}
});
it('should not set content-type if details already has a type', async () => {
const responseData = Buffer.from('file content');
const httpMock = createMockHttpModule(() => {
const res = new MockIncomingMessage(200, { 'content-type': 'text/html' });
setImmediate(() => {
res.emit('data', responseData);
res.emit('end');
});
return res;
});
const importer = createPendingFileImporter(httpMock);
const details: { type?: string } = { type: 'image/png' };
await (importer as any).downloadFile('https://example.com/file.png', details);
expect(details.type).to.equal('image/png');
});
it('should handle responses with undefined statusCode', async () => {
const httpMock = createMockHttpModule(() => {
const res = new MockIncomingMessage(undefined as any);
return res;
});
const importer = createPendingFileImporter(httpMock);
const details: { type?: string } = {};
try {
await (importer as any).downloadFile('https://example.com/file.png', details);
expect.fail('Should have thrown an error');
} catch (err: any) {
expect(err.message).to.include('Unexpected response status');
}
});
it('should accept status code 201', async () => {
const responseData = Buffer.from('created');
const httpMock = createMockHttpModule(() => {
const res = new MockIncomingMessage(201);
setImmediate(() => {
res.emit('data', responseData);
res.emit('end');
});
return res;
});
const importer = createPendingFileImporter(httpMock);
const details: { type?: string } = {};
const result = await (importer as any).downloadFile('https://example.com/file.png', details);
expect(result).to.deep.equal(responseData);
});
});
describe('getMessageAttachment', () => {
let importer: any;
beforeEach(() => {
const httpMock = createMockHttpModule(() => new MockIncomingMessage(200));
importer = createPendingFileImporter(httpMock);
});
it('should create image attachment for image types', () => {
const file = { _id: 'file-1', name: 'image.png', type: 'image/png', size: 1024 };
const url = '/file-upload/file-1/image.png';
const attachment = importer.getMessageAttachment(file, url);
expect(attachment).to.deep.equal({
title: 'image.png',
title_link: url,
image_url: url,
image_type: 'image/png',
image_size: 1024,
image_dimensions: undefined,
});
});
it('should create audio attachment for audio types', () => {
const file = { _id: 'file-1', name: 'audio.mp3', type: 'audio/mpeg', size: 2048 };
const url = '/file-upload/file-1/audio.mp3';
const attachment = importer.getMessageAttachment(file, url);
expect(attachment).to.deep.equal({
title: 'audio.mp3',
title_link: url,
audio_url: url,
audio_type: 'audio/mpeg',
audio_size: 2048,
});
});
it('should create video attachment for video types', () => {
const file = { _id: 'file-1', name: 'video.mp4', type: 'video/mp4', size: 4096 };
const url = '/file-upload/file-1/video.mp4';
const attachment = importer.getMessageAttachment(file, url);
expect(attachment).to.deep.equal({
title: 'video.mp4',
title_link: url,
video_url: url,
video_type: 'video/mp4',
video_size: 4096,
});
});
it('should create generic attachment for other types', () => {
const file = { _id: 'file-1', name: 'doc.pdf', type: 'application/pdf', size: 8192 };
const url = '/file-upload/file-1/doc.pdf';
const attachment = importer.getMessageAttachment(file, url);
expect(attachment).to.deep.equal({
title: 'doc.pdf',
title_link: url,
});
});
it('should create generic attachment for files without type', () => {
const file = { _id: 'file-1', name: 'unknown', size: 512 };
const url = '/file-upload/file-1/unknown';
const attachment = importer.getMessageAttachment(file, url);
expect(attachment).to.deep.equal({
title: 'unknown',
title_link: url,
});
});
});
});

View File

@@ -0,0 +1,298 @@
import { expect } from 'chai';
import { describe, it, beforeEach } from 'mocha';
import proxyquire from 'proxyquire';
import sinon from 'sinon';
const addMessageStub = sinon.stub().resolves();
const settingsStub = { get: sinon.stub() };
const { SlackImporter } = proxyquire.noCallThru().load('../../../../../server/lib/import/slack/SlackImporter', {
'@rocket.chat/models': {
'Messages': { find: sinon.stub().returns({ toArray: () => [] }) },
'Settings': { findOneById: sinon.stub().resolves(null) },
'ImportData': {},
'@global': true,
},
'..': {
Importer: class MockImporter {
logger = { debug: sinon.stub(), error: sinon.stub(), info: sinon.stub(), warn: sinon.stub() };
progress = { count: { total: 0 } };
converter = {
addMessage: addMessageStub,
addUser: sinon.stub().resolves(),
addChannel: sinon.stub().resolves(),
};
_useUpsert = false;
reportProgress() {
// no-op
}
async updateProgress() {
// no-op
}
async updateRecord() {
// no-op
}
getProgress() {
return this.progress;
}
},
ProgressStep: {
PREPARING_STARTED: 'preparing_started',
IMPORTING_USERS: 'importing_users',
IMPORTING_CHANNELS: 'importing_channels',
IMPORTING_MESSAGES: 'importing_messages',
DONE: 'done',
ERROR: 'error',
},
ImporterWebsocket: { progressUpdated: sinon.stub() },
},
'../../notifyListener': { notifyOnSettingChanged: sinon.stub() },
'../../../../app/mentions/lib/MentionsParser': {
MentionsParser: class {
getUserMentions() {
return [];
}
getChannelMentions() {
return [];
}
},
},
'../../../settings': { settings: settingsStub },
'../../utils/getUserAvatarURL': { getUserAvatarURL: sinon.stub().returns('/avatar/user') },
});
describe('SlackImporter', () => {
let importer: any;
beforeEach(() => {
addMessageStub.reset();
settingsStub.get.reset();
const info = { key: 'slack', name: 'Slack' };
const importRecord = { _id: 'test-import' };
importer = new SlackImporter(info, importRecord, {});
});
describe('processMessageSubType - file_share', () => {
it('should create hidden file message with empty msg for file_share subtype', async () => {
const message = {
type: 'message',
subtype: 'file_share',
ts: '1234567890.000001',
user: 'U12345',
file: {
id: 'F12345',
name: 'test.png',
url_private_download: 'https://files.slack.com/files/F12345/test.png?token=xoxe-123',
size: 1024,
is_external: false,
},
};
const slackChannelId = 'C12345';
const newMessage = {
_id: 'slack-C12345-1234567890-000001',
rid: slackChannelId,
ts: new Date(1234567890000),
msg: '',
u: { _id: 'U12345' },
};
await importer.processMessageSubType(message, slackChannelId, newMessage, {});
expect(addMessageStub.calledOnce).to.be.true;
const fileMessage = addMessageStub.firstCall.args[0];
expect(fileMessage._hidden).to.equal(true);
expect(fileMessage.msg).to.equal('');
expect(fileMessage._importFile).to.exist;
expect(fileMessage._importFile.downloadUrl).to.equal('https://files.slack.com/files/F12345/test.png?token=xoxe-123');
});
it('should not create file message if file has no download URL', async () => {
const message = {
type: 'message',
subtype: 'file_share',
ts: '1234567890.000001',
user: 'U12345',
file: {
id: 'F12345',
name: 'test.png',
size: 1024,
},
};
const slackChannelId = 'C12345';
const newMessage = {
_id: 'slack-C12345-1234567890-000001',
rid: slackChannelId,
ts: new Date(1234567890000),
msg: '',
u: { _id: 'U12345' },
};
await importer.processMessageSubType(message, slackChannelId, newMessage, {});
expect(addMessageStub.called).to.be.false;
});
it('should include thread message id if file is shared in a thread', async () => {
const message = {
type: 'message',
subtype: 'file_share',
ts: '1234567890.000002',
thread_ts: '1234567890.000001',
user: 'U12345',
file: {
id: 'F12345',
name: 'test.png',
url_private_download: 'https://files.slack.com/files/F12345/test.png',
size: 1024,
},
};
const slackChannelId = 'C12345';
const newMessage = {
_id: 'slack-C12345-1234567890-000002',
rid: slackChannelId,
ts: new Date(1234567890000),
msg: '',
u: { _id: 'U12345' },
};
await importer.processMessageSubType(message, slackChannelId, newMessage, {});
expect(addMessageStub.calledOnce).to.be.true;
const fileMessage = addMessageStub.firstCall.args[0];
expect(fileMessage.tmid).to.equal('slack-C12345-1234567890-000001');
expect(fileMessage._hidden).to.equal(true);
});
});
describe('prepareMessageObject - messages with files', () => {
it('should create hidden file messages for messages with files array', async () => {
const message = {
type: 'message',
ts: '1234567890.000001',
user: 'U12345',
text: 'Check out these files',
files: [
{
id: 'F12345',
name: 'image1.png',
url_private_download: 'https://files.slack.com/files/F12345/image1.png',
size: 1024,
},
{
id: 'F12346',
name: 'image2.png',
url_private_download: 'https://files.slack.com/files/F12346/image2.png',
size: 2048,
},
],
};
const slackChannelId = 'C12345';
await importer.prepareMessageObject(message, {}, slackChannelId);
const fileMessages = addMessageStub.getCalls().filter((call: any) => call.args[0]._importFile);
expect(fileMessages.length).to.equal(2);
fileMessages.forEach((call: any) => {
const fileMessage = call.args[0];
expect(fileMessage._hidden).to.equal(true);
expect(fileMessage.msg).to.equal('');
expect(fileMessage._importFile).to.exist;
});
});
it('should create unique file message IDs for multiple files', async () => {
const message = {
type: 'message',
ts: '1234567890.000001',
user: 'U12345',
text: 'Multiple files',
files: [
{ id: 'F1', name: 'file1.png', url_private_download: 'https://files.slack.com/F1', size: 100 },
{ id: 'F2', name: 'file2.png', url_private_download: 'https://files.slack.com/F2', size: 200 },
],
};
await importer.prepareMessageObject(message, {}, 'C12345');
const fileMessages = addMessageStub.getCalls().filter((call: any) => call.args[0]._importFile);
const ids = fileMessages.map((call: any) => call.args[0]._id);
expect(ids[0]).to.include('-file1');
expect(ids[1]).to.include('-file2');
expect(ids[0]).to.not.equal(ids[1]);
});
});
describe('convertSlackFileToPendingFile', () => {
it('should convert Slack file to pending file format', () => {
const slackFile = {
id: 'F12345',
name: 'document.pdf',
url_private_download: 'https://files.slack.com/files/F12345/document.pdf?token=xoxe-abc',
size: 4096,
is_external: false,
mimetype: 'application/pdf',
};
const pendingFile = importer.convertSlackFileToPendingFile(slackFile);
expect(pendingFile.id).to.equal('F12345');
expect(pendingFile.name).to.equal('document.pdf');
expect(pendingFile.downloadUrl).to.equal('https://files.slack.com/files/F12345/document.pdf?token=xoxe-abc');
expect(pendingFile.size).to.equal(4096);
expect(pendingFile.external).to.equal(false);
expect(pendingFile.source).to.equal('slack');
expect(pendingFile.original).to.deep.include(slackFile);
});
it('should handle external files', () => {
const slackFile = {
id: 'F99999',
name: 'external.pdf',
url_private_download: 'https://external.com/file.pdf',
size: 1000,
is_external: true,
};
const pendingFile = importer.convertSlackFileToPendingFile(slackFile);
expect(pendingFile.external).to.equal(true);
});
});
describe('makeSlackMessageId', () => {
it('should generate base message ID without file index', () => {
const id = importer.makeSlackMessageId('C12345', '1234567890.000001');
expect(id).to.equal('slack-C12345-1234567890-000001');
});
it('should generate message ID with file index', () => {
const id = importer.makeSlackMessageId('C12345', '1234567890.000001', '1');
expect(id).to.equal('slack-C12345-1234567890-000001-file1');
});
it('should replace dots with dashes in timestamp', () => {
const id = importer.makeSlackMessageId('CHANNEL', '123.456.789');
expect(id).to.equal('slack-CHANNEL-123-456-789');
});
});
});

View File

@@ -0,0 +1,259 @@
import { expect } from 'chai';
import proxyquire from 'proxyquire';
import sinon from 'sinon';
const settingsStub = sinon.stub();
const modelsMock = {
Rooms: {
findOneByImportId: sinon.stub(),
},
};
const insertMessage = sinon.stub();
const { MessageConverter } = proxyquire.noCallThru().load('../../../../../server/lib/import/classes/converters/MessageConverter', {
'../../../messages/insertMessage': {
insertMessage,
},
'meteor/check': sinon.stub(),
'meteor/meteor': sinon.stub(),
'@rocket.chat/models': { ...modelsMock, '@global': true },
});
describe('Message Converter', () => {
beforeEach(() => {
modelsMock.Rooms.findOneByImportId.reset();
insertMessage.reset();
settingsStub.reset();
});
const messageToImport = {
ts: Date.now(),
u: {
_id: 'rocket.cat',
},
rid: 'general',
msg: 'testing',
};
describe('[insertMessage]', () => {
it('function should be called by the converter', async () => {
const converter = new MessageConverter({ workInMemory: true });
converter._cache.addRoom('general', 'general');
sinon.stub(converter, 'insertMessage');
sinon.stub(converter, 'resetLastMessages');
await converter.addObject(messageToImport);
await converter.convertData();
expect(converter.insertMessage.getCalls()).to.be.an('array').with.lengthOf(1);
expect(converter.insertMessage.getCall(0).args).to.be.an('array').that.is.not.empty;
expect(converter.insertMessage.getCall(0).args[0]).to.be.deep.equal(messageToImport);
});
it('should call insertMessage lib function to save the message', async () => {
const converter = new MessageConverter({ workInMemory: true });
converter._cache.addRoom('general', 'main');
await (converter as any).insertMessage(messageToImport);
expect(insertMessage.getCalls()).to.be.an('array').with.lengthOf(1);
expect(insertMessage.getCall(0).args).to.be.an('array').with.lengthOf(4);
expect(insertMessage.getCall(0).args[0]).to.be.deep.equal({
_id: 'rocket.cat',
username: 'rocket.cat',
});
expect(insertMessage.getCall(0).args[1]).to.deep.include({
ts: messageToImport.ts,
msg: messageToImport.msg,
rid: 'main',
});
});
});
describe('[buildMessageObject]', () => {
it('should have the basic info', async () => {
const converter = new MessageConverter({ workInMemory: true });
const converted = await converter.buildMessageObject(messageToImport, 'general', { _id: 'rocket.cat', username: 'rocket.cat' });
expect(converted)
.to.be.an('object')
.that.deep.includes({
ts: messageToImport.ts,
msg: messageToImport.msg,
u: {
_id: 'rocket.cat',
username: 'rocket.cat',
},
});
});
it('should not have properties with undefined values', async () => {
const converter = new MessageConverter({ workInMemory: true });
const converted = await converter.buildMessageObject(messageToImport, 'general', { _id: 'rocket.cat', username: 'rocket.cat' });
Object.entries(converted).forEach(([key, value]) => {
expect(value, `Property "${key}" should not be undefined`).to.not.be.undefined;
});
});
// #TODO: Validate all message attributes
it('should include _hidden field when present', async () => {
const converter = new MessageConverter({ workInMemory: true });
const hiddenMessage = {
...messageToImport,
_hidden: true,
};
const converted = await converter.buildMessageObject(hiddenMessage, 'general', { _id: 'rocket.cat', username: 'rocket.cat' });
expect(converted).to.have.property('_hidden', true);
});
it('should include _importFile field when present', async () => {
const converter = new MessageConverter({ workInMemory: true });
const importFile = {
id: 'F12345',
name: 'test.png',
downloadUrl: 'https://files.slack.com/F12345/test.png',
size: 1024,
source: 'slack',
};
const fileMessage = {
...messageToImport,
_hidden: true,
msg: '',
_importFile: importFile,
};
const converted = await converter.buildMessageObject(fileMessage, 'general', { _id: 'rocket.cat', username: 'rocket.cat' });
expect(converted).to.have.property('_hidden', true);
expect(converted).to.have.property('msg', '');
expect(converted).to.have.property('_importFile').that.deep.includes({
id: 'F12345',
name: 'test.png',
});
});
it('should not include _hidden field when not present or false', async () => {
const converter = new MessageConverter({ workInMemory: true });
const converted = await converter.buildMessageObject(messageToImport, 'general', { _id: 'rocket.cat', username: 'rocket.cat' });
expect(converted).to.not.have.property('_hidden');
});
});
describe('callbacks', () => {
it('beforeImportFn should be triggered', async () => {
const beforeImportFn = sinon.stub();
const converter = new MessageConverter({ workInMemory: true });
converter._cache.addRoom('general', 'general');
sinon.stub(converter, 'insertMessage');
sinon.stub(converter, 'resetLastMessages');
await converter.addObject(messageToImport);
await converter.convertData({
beforeImportFn,
});
expect(beforeImportFn.getCalls()).to.be.an('array').with.lengthOf(1);
});
it('afterImportFn should be triggered', async () => {
const afterImportFn = sinon.stub();
const converter = new MessageConverter({ workInMemory: true });
converter._cache.addRoom('general', 'general');
sinon.stub(converter, 'insertMessage');
sinon.stub(converter, 'resetLastMessages');
await converter.addObject(messageToImport);
await converter.convertData({
afterImportFn,
});
expect(converter.insertMessage.getCalls()).to.be.an('array').with.lengthOf(1);
expect(afterImportFn.getCalls()).to.be.an('array').with.lengthOf(1);
});
it('should skip record if beforeImportFn returns false', async () => {
let recordId = null;
const beforeImportFn = sinon.stub();
const afterImportFn = sinon.stub();
beforeImportFn.callsFake((record) => {
recordId = record._id;
return false;
});
const converter = new MessageConverter({ workInMemory: true });
converter._cache.addRoom('general', 'general');
sinon.stub(converter, 'insertMessage');
sinon.stub(converter, 'resetLastMessages');
sinon.stub(converter, 'skipRecord');
await converter.addObject(messageToImport);
await converter.convertData({
beforeImportFn,
afterImportFn,
});
expect(beforeImportFn.getCalls()).to.be.an('array').with.lengthOf(1);
expect(afterImportFn.getCalls()).to.be.an('array').with.lengthOf(0);
expect(converter.skipRecord.getCalls()).to.be.an('array').with.lengthOf(1);
expect(converter.skipRecord.getCall(0).args).to.be.an('array').that.is.deep.equal([recordId]);
expect(converter.insertMessage.getCalls()).to.be.an('array').with.lengthOf(0);
});
it('should not skip record if beforeImportFn returns true', async () => {
const beforeImportFn = sinon.stub();
const afterImportFn = sinon.stub();
beforeImportFn.callsFake(() => true);
const converter = new MessageConverter({ workInMemory: true });
converter._cache.addRoom('general', 'general');
sinon.stub(converter, 'insertMessage');
sinon.stub(converter, 'resetLastMessages');
sinon.stub(converter, 'skipRecord');
await converter.addObject(messageToImport);
await converter.convertData({
beforeImportFn,
afterImportFn,
});
expect(beforeImportFn.getCalls()).to.be.an('array').with.lengthOf(1);
expect(converter.skipRecord.getCalls()).to.be.an('array').with.lengthOf(0);
expect(converter.insertMessage.getCalls()).to.be.an('array').with.lengthOf(1);
expect(afterImportFn.getCalls()).to.be.an('array').with.lengthOf(1);
});
it('onErrorFn should be triggered if mandatory attributes are missing', async () => {
const converter = new MessageConverter({ workInMemory: true });
converter._cache.addRoom('general', 'general');
sinon.stub(converter, 'resetLastMessages');
const onErrorFn = sinon.stub();
sinon.stub(converter, 'saveError');
await converter.addObject({});
await converter.convertData({ onErrorFn });
expect(onErrorFn.getCall(0)).to.not.be.null;
expect(converter.saveError.getCall(0)).to.not.be.null;
});
});
});

View File

@@ -0,0 +1,134 @@
import type { IImportRecord, IImportRecordType } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import proxyquire from 'proxyquire';
import sinon from 'sinon';
const settingsStub = sinon.stub();
const modelsMock = {
ImportData: {
find: sinon.stub(),
updateOne: sinon.stub(),
col: {
insertOne: sinon.stub(),
},
},
};
const { RecordConverter } = proxyquire.noCallThru().load('../../../../../server/lib/import/classes/converters/RecordConverter', {
'meteor/check': sinon.stub(),
'meteor/meteor': sinon.stub(),
'@rocket.chat/models': { ...modelsMock, '@global': true },
});
class TestConverter extends RecordConverter<IImportRecord> {
constructor(workInMemory = true) {
super({ workInMemory });
}
protected getDataType(): IImportRecordType {
return 'user';
}
}
describe('Record Converter', () => {
const userToImport = {
name: 'user1',
emails: ['user1@domain.com'],
importIds: ['importId1'],
username: 'username1',
};
describe('Working with Mongo Collection', () => {
beforeEach(() => {
modelsMock.ImportData.col.insertOne.reset();
modelsMock.ImportData.find.reset();
modelsMock.ImportData.updateOne.reset();
modelsMock.ImportData.find.callsFake(() => ({ toArray: () => [] }));
});
describe('Adding and Retrieving users', () => {
it('should store objects in the collection', async () => {
const converter = new TestConverter(false);
await converter.addObject(userToImport);
expect(modelsMock.ImportData.col.insertOne.getCall(0)).to.not.be.null;
});
it('should read objects from the collection', async () => {
const converter = new TestConverter(false);
await converter.addObject(userToImport);
await converter.getDataToImport();
expect(modelsMock.ImportData.find.getCall(0)).to.not.be.null;
});
it('should flag skipped records on the document', async () => {
const converter = new TestConverter(false);
await (converter as any).skipRecord('skippedId');
expect(modelsMock.ImportData.updateOne.getCall(0)).to.not.be.null;
expect(modelsMock.ImportData.updateOne.getCall(0).args).to.be.an('array').that.deep.contains({ _id: 'skippedId' });
});
it('should store error information on the document', async () => {
const converter = new TestConverter(false);
await (converter as any).saveError('errorId', new Error());
expect(modelsMock.ImportData.updateOne.getCall(0)).to.not.be.null;
expect(modelsMock.ImportData.updateOne.getCall(0).args).to.be.an('array').that.deep.contains({ _id: 'errorId' });
});
});
});
describe('Working in Memory', () => {
beforeEach(() => {
modelsMock.ImportData.col.insertOne.reset();
modelsMock.ImportData.updateOne.reset();
modelsMock.ImportData.find.reset();
settingsStub.reset();
});
describe('Adding and Retrieving users', () => {
it('should not store objects in the collection', async () => {
const converter = new TestConverter(true);
await converter.addObject(userToImport);
expect(modelsMock.ImportData.col.insertOne.getCall(0)).to.be.null;
});
it('should not try to read objects from the collection', async () => {
const converter = new TestConverter(true);
await converter.addObject(userToImport);
await converter.getDataToImport();
expect(modelsMock.ImportData.find.getCall(0)).to.be.null;
});
it('should properly retrieve the data added to memory', async () => {
const converter = new TestConverter(true);
await converter.addObject(userToImport);
const dataToImport = await converter.getDataToImport();
expect(dataToImport.length).to.be.equal(1);
expect(dataToImport[0].data).to.be.equal(userToImport);
});
it('should not access the collection when flagging skipped records', async () => {
const converter = new TestConverter(true);
await (converter as any).skipRecord('skippedId');
expect(modelsMock.ImportData.updateOne.getCall(0)).to.be.null;
});
it('should not access the collection when storing error information', async () => {
const converter = new TestConverter(true);
await (converter as any).saveError('errorId', new Error());
expect(modelsMock.ImportData.updateOne.getCall(0)).to.be.null;
});
});
});
});

View File

@@ -0,0 +1,269 @@
import { expect } from 'chai';
import proxyquire from 'proxyquire';
import sinon from 'sinon';
const settingsStub = sinon.stub();
const modelsMock = {
Rooms: {
archiveById: sinon.stub(),
updateOne: sinon.stub(),
findOneById: sinon.stub(),
findDirectRoomContainingAllUsernames: sinon.stub(),
findOneByNonValidatedName: sinon.stub(),
},
Subscriptions: {
archiveByRoomId: sinon.stub(),
},
};
const createDirectMessage = sinon.stub();
const saveRoomSettings = sinon.stub();
const { RoomConverter } = proxyquire.noCallThru().load('../../../../../server/lib/import/classes/converters/RoomConverter', {
'../../../../meteor-methods/messages/createDirectMessage': {
createDirectMessage,
},
'../../../../meteor-methods/rooms/saveRoomSettings': {
saveRoomSettings,
},
'../../../notifyListener': {
notifyOnSubscriptionChangedByRoomId: sinon.stub(),
},
'../../../../meteor-methods/rooms/createChannel': {
createChannelMethod: sinon.stub(),
},
'../../../../meteor-methods/rooms/createPrivateGroup': {
createPrivateGroupMethod: sinon.stub(),
},
'meteor/check': sinon.stub(),
'meteor/meteor': sinon.stub(),
'@rocket.chat/models': { ...modelsMock, '@global': true },
});
describe('Room Converter', () => {
beforeEach(() => {
modelsMock.Rooms.archiveById.reset();
modelsMock.Rooms.updateOne.reset();
modelsMock.Rooms.findOneById.reset();
modelsMock.Rooms.findDirectRoomContainingAllUsernames.reset();
modelsMock.Rooms.findOneByNonValidatedName.reset();
modelsMock.Subscriptions.archiveByRoomId.reset();
createDirectMessage.reset();
saveRoomSettings.reset();
settingsStub.reset();
});
const roomToImport = {
name: 'room1',
importIds: ['importIdRoom1'],
};
describe('[findExistingRoom]', () => {
it('function should be called by the converter', async () => {
const converter = new RoomConverter({ workInMemory: true });
sinon.stub(converter, 'findExistingRoom');
sinon.stub(converter, 'insertOrUpdateRoom');
await converter.addObject(roomToImport);
await converter.convertChannels('startedByUserId');
expect(converter.findExistingRoom.getCall(0)).to.not.be.null;
});
it('should search by name', async () => {
const converter = new RoomConverter({ workInMemory: true });
await converter.findExistingRoom(roomToImport);
expect(modelsMock.Rooms.findOneByNonValidatedName.getCalls()).to.be.an('array').with.lengthOf(1);
expect(modelsMock.Rooms.findOneByNonValidatedName.getCall(0).args).to.be.an('array').that.contains('room1');
});
it('should not search by name if there is none', async () => {
const converter = new RoomConverter({ workInMemory: true });
await converter.findExistingRoom({});
expect(modelsMock.Rooms.findOneByNonValidatedName.getCalls()).to.be.an('array').with.lengthOf(0);
});
it('should search DMs by usernames', async () => {
const converter = new RoomConverter({ workInMemory: true });
converter._cache.addUser('importId1', 'userId1', 'username1');
converter._cache.addUser('importId2', 'userId2', 'username2');
await converter.findExistingRoom({
t: 'd',
users: ['importId1', 'importId2'],
importIds: ['importIdRoom1'],
});
expect(modelsMock.Rooms.findDirectRoomContainingAllUsernames.getCalls()).to.be.an('array').with.lengthOf(1);
expect(modelsMock.Rooms.findDirectRoomContainingAllUsernames.getCall(0).args)
.to.be.an('array')
.that.deep.includes(['username1', 'username2']);
});
});
describe('[insertRoom]', () => {
it('function should be called by the converter', async () => {
const converter = new RoomConverter({ workInMemory: true });
sinon.stub(converter, 'findExistingRoom');
sinon.stub(converter, 'insertRoom');
sinon.stub(converter, 'updateRoom');
await converter.addObject(roomToImport);
await converter.convertChannels('startedByUserId');
expect(converter.updateRoom.getCalls()).to.be.an('array').with.lengthOf(0);
expect(converter.insertRoom.getCalls()).to.be.an('array').with.lengthOf(1);
expect(converter.insertRoom.getCall(0).args).to.be.an('array').that.is.not.empty;
expect(converter.insertRoom.getCall(0).args[0]).to.be.deep.equal(roomToImport);
});
it('function should not be called for existing rooms', async () => {
const converter = new RoomConverter({ workInMemory: true });
sinon.stub(converter, 'findExistingRoom');
converter.findExistingRoom.returns({ _id: 'oldId' });
sinon.stub(converter, 'insertRoom');
sinon.stub(converter, 'updateRoom');
await converter.addObject(roomToImport);
await converter.convertChannels('startedByUserId');
expect(converter.insertRoom.getCall(0)).to.be.null;
});
it('should call createDirectMessage to create DM rooms', async () => {
const converter = new RoomConverter({ workInMemory: true });
sinon.stub(converter, 'updateRoomId');
createDirectMessage.callsFake((_options, data) => {
return {
...data,
_id: 'Id1',
};
});
converter._cache.addUser('importId1', 'userId1', 'username1');
converter._cache.addUser('importId2', 'userId2', 'username2');
await (converter as any).insertRoom(
{
t: 'd',
users: ['importId1', 'importId2'],
importIds: ['importIdRoom1'],
},
'startedByUserId',
);
expect(createDirectMessage.getCalls()).to.be.an('array').with.lengthOf(1);
expect(createDirectMessage.getCall(0).args).to.be.an('array').with.lengthOf(3).that.deep.includes(['username1', 'username2']);
});
// #TODO: Validate all room types
});
describe('callbacks', () => {
it('beforeImportFn should be triggered', async () => {
const beforeImportFn = sinon.stub();
const converter = new RoomConverter({ workInMemory: true });
sinon.stub(converter, 'findExistingRoom');
sinon.stub(converter, 'insertOrUpdateRoom');
await converter.addObject(roomToImport);
await converter.convertChannels('startedByUserId', {
beforeImportFn,
});
expect(beforeImportFn.getCalls()).to.be.an('array').with.lengthOf(1);
});
it('afterImportFn should be triggered', async () => {
const afterImportFn = sinon.stub();
const converter = new RoomConverter({ workInMemory: true });
sinon.stub(converter, 'findExistingRoom');
sinon.stub(converter, 'insertOrUpdateRoom');
await converter.addObject(roomToImport);
await converter.convertChannels('startedByUserId', {
afterImportFn,
});
expect(converter.insertOrUpdateRoom.getCalls()).to.be.an('array').with.lengthOf(1);
expect(afterImportFn.getCalls()).to.be.an('array').with.lengthOf(1);
});
it('should skip record if beforeImportFn returns false', async () => {
let recordId = null;
const beforeImportFn = sinon.stub();
const afterImportFn = sinon.stub();
beforeImportFn.callsFake((record) => {
recordId = record._id;
return false;
});
const converter = new RoomConverter({ workInMemory: true });
sinon.stub(converter, 'findExistingRoom');
sinon.stub(converter, 'insertOrUpdateRoom');
sinon.stub(converter, 'skipRecord');
await converter.addObject(roomToImport);
await converter.convertChannels('startedByUserId', {
beforeImportFn,
afterImportFn,
});
expect(beforeImportFn.getCalls()).to.be.an('array').with.lengthOf(1);
expect(afterImportFn.getCalls()).to.be.an('array').with.lengthOf(0);
expect(converter.skipRecord.getCalls()).to.be.an('array').with.lengthOf(1);
expect(converter.skipRecord.getCall(0).args).to.be.an('array').that.is.deep.equal([recordId]);
expect(converter.insertOrUpdateRoom.getCalls()).to.be.an('array').with.lengthOf(0);
});
it('should not skip record if beforeImportFn returns true', async () => {
const beforeImportFn = sinon.stub();
const afterImportFn = sinon.stub();
beforeImportFn.callsFake(() => true);
const converter = new RoomConverter({ workInMemory: true });
sinon.stub(converter, 'findExistingRoom');
sinon.stub(converter, 'insertOrUpdateRoom');
sinon.stub(converter, 'skipRecord');
await converter.addObject(roomToImport);
await converter.convertChannels('startedByUserId', {
beforeImportFn,
afterImportFn,
});
expect(beforeImportFn.getCalls()).to.be.an('array').with.lengthOf(1);
expect(converter.skipRecord.getCalls()).to.be.an('array').with.lengthOf(0);
expect(converter.insertOrUpdateRoom.getCalls()).to.be.an('array').with.lengthOf(1);
expect(afterImportFn.getCalls()).to.be.an('array').with.lengthOf(1);
});
it('onErrorFn should be triggered if there is no name and is not a DM', async () => {
const converter = new RoomConverter({ workInMemory: true });
const onErrorFn = sinon.stub();
sinon.stub(converter, 'findExistingRoom');
sinon.stub(converter, 'insertOrUpdateRoom');
sinon.stub(converter, 'saveError');
await converter.addObject({});
await converter.convertChannels('startedByUserId', { onErrorFn });
expect(converter.insertOrUpdateRoom.getCall(0)).to.be.null;
expect(onErrorFn.getCall(0)).to.not.be.null;
expect(converter.saveError.getCall(0)).to.not.be.null;
});
});
});

View File

@@ -0,0 +1,625 @@
import { expect } from 'chai';
import proxyquire from 'proxyquire';
import sinon from 'sinon';
const settingsStub = sinon.stub();
const modelsMock = {
Users: {
findOneByEmailAddress: sinon.stub(),
findOneByUsernameIgnoringCase: sinon.stub(),
findOneById: sinon.stub(),
},
};
const addUserToDefaultChannels = sinon.stub();
const generateUsernameSuggestion = sinon.stub();
const insertUserDoc = sinon.stub();
const callbacks = {
run: sinon.stub(),
};
const bcryptHash = sinon.stub();
const sha = sinon.stub();
const generateTempPassword = sinon.stub();
const { UserConverter } = proxyquire.noCallThru().load('../../../../../server/lib/import/classes/converters/UserConverter', {
'../../../callbacks': {
callbacks,
},
'../../../rooms/addUserToDefaultChannels': {
addUserToDefaultChannels,
},
'../../../users/getUsernameSuggestion': {
generateUsernameSuggestion,
},
'../../../users/saveUserIdentity': {
saveUserIdentity: sinon.stub(),
},
'../../../users/setUserActiveStatus': {
setUserActiveStatus: sinon.stub(),
},
'../../../notifyListener': {
notifyOnUserChange: sinon.stub(),
},
'./generateTempPassword': {
generateTempPassword,
'@global': true,
},
'bcrypt': {
'hash': bcryptHash,
'@global': true,
},
'meteor/check': sinon.stub(),
'meteor/meteor': sinon.stub(),
'@rocket.chat/sha256': {
SHA256: sha,
},
'meteor/accounts-base': {
Accounts: {
insertUserDoc,
_bcryptRounds: () => 10,
},
},
'@rocket.chat/models': { ...modelsMock, '@global': true },
});
describe('User Converter', () => {
beforeEach(() => {
modelsMock.Users.findOneByEmailAddress.reset();
modelsMock.Users.findOneByUsernameIgnoringCase.reset();
modelsMock.Users.findOneById.reset();
callbacks.run.reset();
insertUserDoc.reset();
addUserToDefaultChannels.reset();
generateUsernameSuggestion.reset();
settingsStub.reset();
});
const userToImport = {
name: 'user1',
emails: ['user1@domain.com'],
importIds: ['importId1'],
username: 'username1',
};
describe('[findExistingUser]', () => {
it('function should be called by the converter', async () => {
const converter = new UserConverter({ workInMemory: true });
const findExistingUser = sinon.stub(converter, 'findExistingUser');
findExistingUser.throws();
await converter.addObject(userToImport);
await converter.convertData();
expect(findExistingUser.getCall(0)).to.not.be.null;
expect(callbacks.run.getCall(0)).to.not.be.null;
expect(callbacks.run.getCall(0).args).to.be.deep.equal(['afterUserImport', { inserted: [], updated: [], skipped: 0, failed: 1 }]);
});
it('should search by email address', async () => {
const converter = new UserConverter({ workInMemory: true });
await converter.findExistingUser(userToImport);
expect(modelsMock.Users.findOneByEmailAddress.getCalls()).to.be.an('array').with.lengthOf(1);
expect(modelsMock.Users.findOneByEmailAddress.getCall(0).args).to.be.an('array').that.contains('user1@domain.com');
});
it('should search by username', async () => {
const converter = new UserConverter({ workInMemory: true });
await converter.findExistingUser(userToImport);
expect(modelsMock.Users.findOneByUsernameIgnoringCase.getCalls()).to.be.an('array').with.lengthOf(1);
expect(modelsMock.Users.findOneByUsernameIgnoringCase.getCall(0).args).to.be.an('array').that.contains('username1');
});
it('should not search by username if an user is found by email', async () => {
const converter = new UserConverter({ workInMemory: true });
modelsMock.Users.findOneByEmailAddress.resolves(userToImport);
await converter.findExistingUser(userToImport);
expect(modelsMock.Users.findOneByUsernameIgnoringCase.getCall(0)).to.be.null;
});
});
describe('[buildNewUserObject]', () => {
const mappedUser = (expectedData: Record<string, any>) => ({
type: 'user',
services: {
password: {
bcrypt: 'hashed=tempPassword',
},
},
...expectedData,
});
const converter = new UserConverter({ workInMemory: true });
const hashPassword = sinon.stub(converter, 'hashPassword');
generateTempPassword.returns('tempPassword');
hashPassword.callsFake(async (pass) => `hashed=${pass}`);
bcryptHash.callsFake((pass: string) => `hashed=${pass}`);
sha.callsFake((pass: string) => pass);
it('should map an empty object', async () => {
expect(
await converter.buildNewUserObject({
emails: [],
importIds: [],
}),
).to.be.deep.equal(mappedUser({}));
});
it('should map the name and username', async () => {
expect(
await converter.buildNewUserObject({
emails: [],
importIds: [],
name: 'name1',
username: 'username1',
}),
).to.be.deep.equal(
mappedUser({
username: 'username1',
name: 'name1',
}),
);
});
it('should map optional fields', async () => {
expect(
await converter.buildNewUserObject({
emails: [],
importIds: [],
statusText: 'statusText1',
bio: 'bio1',
avatarUrl: 'avatarUrl',
utcOffset: 3,
}),
).to.be.deep.equal(
mappedUser({
statusText: 'statusText1',
bio: 'bio1',
_pendingAvatarUrl: 'avatarUrl',
utcOffset: 3,
}),
);
});
it('should map custom fields', async () => {
expect(
await (converter as any).buildNewUserObject({
emails: [],
importIds: [],
customFields: {
age: 32,
nickname: 'stitch',
},
}),
).to.be.deep.equal(
mappedUser({
customFields: {
age: 32,
nickname: 'stitch',
},
}),
);
});
it('should not map roles', async () => {
expect(
await converter.buildNewUserObject({
emails: [],
importIds: [],
roles: ['role1'],
}),
).to.be.deep.equal(mappedUser({}));
});
it('should map identifiers', async () => {
expect(
await converter.buildNewUserObject({
name: 'user1',
emails: ['user1@domain.com'],
importIds: ['importId1'],
username: 'username1',
}),
).to.be.deep.equal(
mappedUser({
username: 'username1',
name: 'user1',
importIds: ['importId1'],
emails: [{ address: 'user1@domain.com', verified: false }],
}),
);
});
it('should map password', async () => {
expect(
await converter.buildNewUserObject({
emails: [],
importIds: [],
password: 'batata',
}),
).to.be.deep.equal(
mappedUser({
services: {
password: {
bcrypt: 'hashed=batata',
},
},
}),
);
});
it('should map ldap service data', async () => {
expect(
await converter.buildNewUserObject({
emails: [],
importIds: [],
services: {
ldap: {
id: 'id',
},
},
}),
).to.be.deep.equal(
mappedUser({
services: {
ldap: {
id: 'id',
},
},
ldap: true,
}),
);
});
it('should map deleted users', async () => {
expect(
await converter.buildNewUserObject({
emails: [],
importIds: [],
deleted: true,
}),
).to.be.deep.equal(
mappedUser({
active: false,
}),
);
});
it('should map restored users', async () => {
expect(
await converter.buildNewUserObject({
emails: [],
importIds: [],
deleted: false,
}),
).to.be.deep.equal(
mappedUser({
active: true,
}),
);
});
it('should map user type', async () => {
expect(
await converter.buildNewUserObject({
emails: [],
importIds: [],
type: 'user',
}),
).to.be.deep.equal(mappedUser({}));
});
it('should map bot type', async () => {
expect(
await converter.buildNewUserObject({
emails: [],
importIds: [],
type: 'bot',
}),
).to.be.deep.equal(
mappedUser({
type: 'bot',
services: {
password: {
bcrypt: 'hashed=tempPassword',
},
},
}),
);
});
});
describe('[insertUser]', () => {
it('function should be called by the converter', async () => {
const converter = new UserConverter({ workInMemory: true, skipDefaultChannels: true });
modelsMock.Users.findOneByEmailAddress.resolves(null);
modelsMock.Users.findOneByUsernameIgnoringCase.resolves(null);
sinon.stub(converter, 'insertUser');
sinon.stub(converter, 'updateUser');
await converter.addObject(userToImport);
await converter.convertData();
expect(converter.updateUser.getCalls()).to.be.an('array').with.lengthOf(0);
expect(converter.insertUser.getCalls()).to.be.an('array').with.lengthOf(1);
expect(converter.insertUser.getCall(0).args).to.be.an('array').that.is.not.empty;
expect(converter.insertUser.getCall(0).args[0]).to.be.deep.equal(userToImport);
expect(addUserToDefaultChannels.getCalls()).to.be.an('array').with.lengthOf(0);
});
it('function should not be called when skipNewUsers = true', async () => {
const converter = new UserConverter({ workInMemory: true, skipNewUsers: true });
sinon.stub(converter, 'findExistingUser');
sinon.stub(converter, 'insertUser');
sinon.stub(converter, 'updateUser');
sinon.stub(converter, 'skipMemoryRecord');
await converter.addObject(userToImport);
await converter.convertData();
expect(converter.insertUser.getCall(0)).to.be.null;
expect(converter.skipMemoryRecord.getCall(0)).to.not.be.null;
expect(callbacks.run.getCall(0)).to.not.be.null;
expect(callbacks.run.getCall(0).args).to.be.deep.equal(['afterUserImport', { inserted: [], updated: [], skipped: 1, failed: 0 }]);
});
it('function should not be called for existing users', async () => {
const converter = new UserConverter({ workInMemory: true });
sinon.stub(converter, 'findExistingUser');
converter.findExistingUser.returns({ _id: 'oldId' });
sinon.stub(converter, 'insertUser');
sinon.stub(converter, 'updateUser');
await converter.addObject(userToImport);
await converter.convertData();
expect(converter.insertUser.getCall(0)).to.be.null;
expect(callbacks.run.getCall(0)).to.not.be.null;
expect(callbacks.run.getCall(0).args).to.be.deep.equal([
'afterUserImport',
{ inserted: [], updated: ['oldId'], skipped: 0, failed: 0 },
]);
});
it('addUserToDefaultChannels should be called by the converter on successful insert', async () => {
const converter = new UserConverter({ workInMemory: true, skipDefaultChannels: false });
modelsMock.Users.findOneByEmailAddress.resolves(null);
modelsMock.Users.findOneByUsernameIgnoringCase.resolves(null);
modelsMock.Users.findOneById.withArgs('newId').returns({ newUser: true });
sinon.stub(converter, 'insertUser');
converter.insertUser.callsFake(() => 'newId');
await converter.addObject(userToImport);
await converter.convertData();
expect(converter.insertUser.getCalls()).to.be.an('array').with.lengthOf(1);
expect(converter.insertUser.getCall(0).args).to.be.an('array').that.is.not.empty;
expect(converter.insertUser.getCall(0).args[0]).to.be.deep.equal(userToImport);
expect(addUserToDefaultChannels.getCalls()).to.be.an('array').with.lengthOf(1);
expect(addUserToDefaultChannels.getCall(0).args).to.be.an('array').that.deep.contains({ newUser: true });
});
it('should call insertUserDoc with the mapped data and roles', async () => {
const converter = new UserConverter({ workInMemory: true });
let insertedUser = null;
insertUserDoc.callsFake((_options, data) => {
insertedUser = {
...data,
_id: 'Id1',
};
return 'Id1';
});
modelsMock.Users.findOneById.withArgs('Id1').resolves(insertedUser);
await (converter as any).insertUser({ ...userToImport, roles: ['role1', 'role2'] });
expect(insertUserDoc.getCalls()).to.be.an('array').with.lengthOf(1);
expect(insertUserDoc.getCall(0).args).to.be.an('array').with.lengthOf(2);
const usedParams = insertUserDoc.getCall(0).args[1];
expect(usedParams).to.deep.include({
type: 'user',
username: 'username1',
name: 'user1',
importIds: ['importId1'],
emails: [{ address: 'user1@domain.com', verified: false }],
globalRoles: ['role1', 'role2'],
});
});
});
describe('[updateUser]', () => {
it('function should be called by the converter', async () => {
const converter = new UserConverter({ workInMemory: true });
sinon.stub(converter, 'findExistingUser');
converter.findExistingUser.returns({ _id: 'oldId' });
sinon.stub(converter, 'insertUser');
sinon.stub(converter, 'updateUser');
await converter.addObject(userToImport);
await converter.convertData();
expect(converter.insertUser.getCalls()).to.be.an('array').with.lengthOf(0);
expect(converter.updateUser.getCalls()).to.be.an('array').with.lengthOf(1);
expect(converter.updateUser.getCall(0).args).to.be.an('array').that.is.not.empty;
expect(converter.updateUser.getCall(0).args[1]).to.be.deep.equal(userToImport);
});
it('function should not be called when skipExistingUsers = true', async () => {
const converter = new UserConverter({ workInMemory: true, skipExistingUsers: true });
sinon.stub(converter, 'findExistingUser');
converter.findExistingUser.returns({ _id: 'oldId' });
sinon.stub(converter, 'insertUser');
sinon.stub(converter, 'updateUser');
sinon.stub(converter, 'skipMemoryRecord');
await converter.addObject(userToImport);
await converter.convertData();
expect(converter.updateUser.getCall(0)).to.be.null;
expect(converter.skipMemoryRecord.getCall(0)).to.not.be.null;
expect(callbacks.run.getCall(0)).to.not.be.null;
expect(callbacks.run.getCall(0).args).to.be.deep.equal(['afterUserImport', { inserted: [], updated: [], skipped: 1, failed: 0 }]);
});
it('function should not be called for new users', async () => {
const converter = new UserConverter({ workInMemory: true });
sinon.stub(converter, 'findExistingUser');
sinon.stub(converter, 'insertUser');
sinon.stub(converter, 'updateUser');
await converter.addObject(userToImport);
await converter.convertData();
expect(converter.updateUser.getCall(0)).to.be.null;
});
});
// #TODO: Validate batch conversions
describe('callbacks', () => {
it('beforeImportFn should be triggered', async () => {
const beforeImportFn = sinon.stub();
beforeImportFn.callsFake(() => true);
const converter = new UserConverter({ workInMemory: true, skipDefaultChannels: true });
sinon.stub(converter, 'findExistingUser');
sinon.stub(converter, 'insertUser');
sinon.stub(converter, 'updateUser');
await converter.addObject(userToImport);
await converter.convertData({
beforeImportFn,
});
expect(beforeImportFn.getCalls()).to.be.an('array').with.lengthOf(1);
expect(converter.insertUser.getCalls()).to.be.an('array').with.lengthOf(1);
});
it('afterImportFn should be triggered', async () => {
const afterImportFn = sinon.stub();
const converter = new UserConverter({ workInMemory: true, skipDefaultChannels: true });
sinon.stub(converter, 'findExistingUser');
sinon.stub(converter, 'insertUser');
sinon.stub(converter, 'updateUser');
await converter.addObject(userToImport);
await converter.convertData({
afterImportFn,
});
expect(converter.insertUser.getCalls()).to.be.an('array').with.lengthOf(1);
expect(afterImportFn.getCalls()).to.be.an('array').with.lengthOf(1);
});
it('should skip record if beforeImportFn returns false', async () => {
let recordId = null;
const beforeImportFn = sinon.stub();
const afterImportFn = sinon.stub();
beforeImportFn.callsFake((record) => {
recordId = record._id;
return false;
});
const converter = new UserConverter({ workInMemory: true, skipDefaultChannels: true });
sinon.stub(converter, 'findExistingUser');
sinon.stub(converter, 'insertUser');
sinon.stub(converter, 'updateUser');
sinon.stub(converter, 'skipMemoryRecord');
await converter.addObject(userToImport);
await converter.convertData({
beforeImportFn,
afterImportFn,
});
expect(beforeImportFn.getCalls()).to.be.an('array').with.lengthOf(1);
expect(converter.skipMemoryRecord.getCalls()).to.be.an('array').with.lengthOf(1);
expect(afterImportFn.getCalls()).to.be.an('array').with.lengthOf(0);
expect(converter.skipMemoryRecord.getCall(0).args).to.be.an('array').that.is.deep.equal([recordId]);
expect(callbacks.run.getCall(0)).to.not.be.null;
expect(callbacks.run.getCall(0).args).to.be.deep.equal(['afterUserImport', { inserted: [], updated: [], skipped: 1, failed: 0 }]);
});
it('should not skip record if beforeImportFn returns true', async () => {
let userId = null;
const beforeImportFn = sinon.stub();
const afterImportFn = sinon.stub();
beforeImportFn.callsFake(() => true);
afterImportFn.callsFake((record) => {
userId = record.data._id;
});
const converter = new UserConverter({ workInMemory: true, skipDefaultChannels: true });
sinon.stub(converter, 'findExistingUser');
sinon.stub(converter, 'insertUser');
sinon.stub(converter, 'updateUser');
await converter.addObject(userToImport);
await converter.convertData({
beforeImportFn,
afterImportFn,
});
expect(beforeImportFn.getCalls()).to.be.an('array').with.lengthOf(1);
expect(converter.insertUser.getCalls()).to.be.an('array').with.lengthOf(1);
expect(afterImportFn.getCalls()).to.be.an('array').with.lengthOf(1);
expect(callbacks.run.getCall(0)).to.not.be.null;
expect(callbacks.run.getCall(0).args).to.be.deep.equal([
'afterUserImport',
{ inserted: [userId], updated: [], skipped: 0, failed: 0 },
]);
});
it('onErrorFn should be triggered if there is no email and no username', async () => {
const converter = new UserConverter({ workInMemory: true, skipDefaultChannels: true });
const onErrorFn = sinon.stub();
sinon.stub(converter, 'findExistingUser');
sinon.stub(converter, 'insertUser');
sinon.stub(converter, 'updateUser');
sinon.stub(converter, 'saveError');
await converter.addObject({
name: 'user1',
emails: [],
importIds: [],
});
await converter.convertData({ onErrorFn });
expect(converter.insertUser.getCall(0)).to.be.null;
expect(callbacks.run.getCall(0)).to.not.be.null;
expect(callbacks.run.getCall(0).args).to.be.deep.equal(['afterUserImport', { inserted: [], updated: [], skipped: 0, failed: 1 }]);
expect(onErrorFn.getCall(0)).to.not.be.null;
expect(converter.saveError.getCall(0)).to.not.be.null;
});
// #TODO: Validate afterBatchFn
});
});