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,313 @@
import type { IRoom } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import type { Response } from 'supertest';
import { getCredentials, api, request, credentials } from '../../data/api-data';
import { sendSimpleMessage } from '../../data/chat.helper';
import { updatePermission } from '../../data/permissions.helper';
import { createRoom, deleteRoom } from '../../data/rooms.helper';
describe('[Engagement Dashboard]', function () {
this.retries(0);
const isEnterprise = Boolean(process.env.IS_EE);
before((done) => getCredentials(done));
before(() => updatePermission('view-engagement-dashboard', ['admin']));
after(() => updatePermission('view-engagement-dashboard', ['admin']));
(isEnterprise ? describe : describe.skip)('[/engagement-dashboard/channels/list]', () => {
let testRoom: IRoom;
let emptyRoom: IRoom;
before(async () => {
testRoom = (await createRoom({ type: 'c', name: `channel.test.engagement.${Date.now()}-${Math.random()}` })).body.channel;
emptyRoom = (await createRoom({ type: 'c', name: `channel.test.engagement.empty.${Date.now()}-${Math.random()}` })).body.channel;
});
after(async () => {
await deleteRoom({ type: 'c', roomId: testRoom._id });
await deleteRoom({ type: 'c', roomId: emptyRoom._id });
});
it('should fail if user does not have the view-engagement-dashboard permission', async () => {
await updatePermission('view-engagement-dashboard', []);
await request
.get(api('engagement-dashboard/channels/list'))
.set(credentials)
.query({
end: new Date().toISOString(),
start: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(),
offset: 0,
count: 25,
})
.expect('Content-Type', 'application/json')
.expect(403)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body.error).to.be.equal('User does not have the permissions required for this action [error-unauthorized]');
});
});
it('should fail if start param is not a valid date', async () => {
await updatePermission('view-engagement-dashboard', ['admin']);
await request
.get(api('engagement-dashboard/channels/list'))
.set(credentials)
.query({
start: 'invalid-date',
end: new Date().toISOString(),
offset: 0,
count: 25,
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body.error).to.be.equal('Match error: Failed Match.Where validation in field start');
});
});
it('should fail if end param is not a valid date', async () => {
await request
.get(api('engagement-dashboard/channels/list'))
.set(credentials)
.query({
start: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(),
end: 'invalid-date',
offset: 0,
count: 25,
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body.error).to.be.equal('Match error: Failed Match.Where validation in field end');
});
});
it('should fail if start param is not provided', async () => {
await request
.get(api('engagement-dashboard/channels/list'))
.set(credentials)
.query({
end: new Date(),
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body.error).to.be.equal("Match error: Missing key 'start'");
});
});
it('should fail if end param is not provided', async () => {
await request
.get(api('engagement-dashboard/channels/list'))
.set(credentials)
.query({
start: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(),
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body.error).to.be.equal("Match error: Missing key 'end'");
});
});
it('should succesfuly return results', async () => {
await sendSimpleMessage({ roomId: testRoom._id });
await request
.get(api('engagement-dashboard/channels/list'))
.set(credentials)
.query({
end: new Date().toISOString(),
start: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(),
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('offset', 0);
expect(res.body).to.have.property('count');
expect(res.body).to.have.property('total');
expect(res.body).to.have.property('channels');
expect(res.body.channels).to.be.an('array').that.is.not.empty;
expect(res.body.channels[0]).to.be.an('object').that.is.not.empty;
expect(res.body.channels[0]).to.have.property('messages').that.is.a('number');
expect(res.body.channels[0]).to.have.property('lastWeekMessages').that.is.a('number');
expect(res.body.channels[0]).to.have.property('diffFromLastWeek').that.is.a('number');
expect(res.body.channels[0].room).to.be.an('object').that.is.not.empty;
expect(res.body.channels[0].room).to.have.property('_id').that.is.a('string');
expect(res.body.channels[0].room).to.have.property('name').that.is.a('string');
expect(res.body.channels[0].room).to.have.property('ts').that.is.a('string');
expect(res.body.channels[0].room).to.have.property('t').that.is.a('string');
expect(res.body.channels[0].room).to.have.property('_updatedAt').that.is.a('string');
});
});
it('should not return empty rooms', async () => {
await request
.get(api('engagement-dashboard/channels/list'))
.set(credentials)
.query({
end: new Date().toISOString(),
start: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(),
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('offset', 0);
expect(res.body).to.have.property('count');
expect(res.body).to.have.property('total');
expect(res.body).to.have.property('channels');
const channelRecord = res.body.channels.find(({ room }: { room: { _id: string } }) => room._id === emptyRoom._id);
expect(channelRecord).to.be.undefined;
});
});
it('should correctly count messages diff compared to last week when there are messages in a room', async () => {
await request
.get(api('engagement-dashboard/channels/list'))
.set(credentials)
.query({
end: new Date().toISOString(),
start: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(),
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('offset', 0);
expect(res.body).to.have.property('count');
expect(res.body).to.have.property('total');
expect(res.body).to.have.property('channels');
expect(res.body.channels).to.be.an('array').that.is.not.empty;
const channelRecord = res.body.channels.find(({ room }: { room: { _id: string } }) => room._id === testRoom._id);
expect(channelRecord).not.to.be.undefined;
expect(channelRecord).to.be.an('object').that.is.not.empty;
expect(channelRecord).to.have.property('messages', 1);
expect(channelRecord).to.have.property('lastWeekMessages', 0);
expect(channelRecord).to.have.property('diffFromLastWeek', 1);
expect(channelRecord.room).to.be.an('object').that.is.not.empty;
expect(channelRecord.room).to.have.property('_id', testRoom._id);
expect(channelRecord.room).to.have.property('name', testRoom.name);
expect(channelRecord.room).to.have.property('ts', testRoom.ts);
expect(channelRecord.room).to.have.property('t', testRoom.t);
});
});
it('should correctly count messages diff compared to last week when there are messages in a room', async () => {
await request
.get(api('engagement-dashboard/channels/list'))
.set(credentials)
.query({
end: new Date().toISOString(),
start: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(),
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('offset', 0);
expect(res.body).to.have.property('count');
expect(res.body).to.have.property('total');
expect(res.body).to.have.property('channels');
expect(res.body.channels).to.be.an('array').that.is.not.empty;
const channelRecord = res.body.channels.find(({ room }: { room: { _id: string } }) => room._id === testRoom._id);
expect(channelRecord).not.to.be.undefined;
expect(channelRecord).to.be.an('object').that.is.not.empty;
expect(channelRecord).to.have.property('messages', 1);
expect(channelRecord).to.have.property('lastWeekMessages', 0);
expect(channelRecord).to.have.property('diffFromLastWeek', 1);
expect(channelRecord.room).to.be.an('object').that.is.not.empty;
expect(channelRecord.room).to.have.property('_id', testRoom._id);
expect(channelRecord.room).to.have.property('name', testRoom.name);
expect(channelRecord.room).to.have.property('ts', testRoom.ts);
expect(channelRecord.room).to.have.property('t', testRoom.t);
});
});
it('should correctly count messages from last week and diff when moving to the next week', async () => {
await request
.get(api('engagement-dashboard/channels/list'))
.set(credentials)
.query({
end: new Date(Date.now() + 8 * 24 * 60 * 60 * 1000).toISOString(),
start: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('offset', 0);
expect(res.body).to.have.property('count');
expect(res.body).to.have.property('total');
expect(res.body).to.have.property('channels');
expect(res.body.channels).to.be.an('array').that.is.not.empty;
const channelRecord = res.body.channels.find(({ room }: { room: { _id: string } }) => room._id === testRoom._id);
expect(channelRecord).not.to.be.undefined;
expect(channelRecord).to.be.an('object').that.is.not.empty;
expect(channelRecord).to.have.property('messages', 0);
expect(channelRecord).to.have.property('lastWeekMessages', 1);
expect(channelRecord).to.have.property('diffFromLastWeek', -1);
expect(channelRecord.room).to.be.an('object').that.is.not.empty;
expect(channelRecord.room).to.have.property('_id', testRoom._id);
expect(channelRecord.room).to.have.property('name', testRoom.name);
expect(channelRecord.room).to.have.property('ts', testRoom.ts);
expect(channelRecord.room).to.have.property('t', testRoom.t);
});
});
it('should correctly count messages from last week and diff when moving to the next week', async () => {
await request
.get(api('engagement-dashboard/channels/list'))
.set(credentials)
.query({
end: new Date(Date.now() + 8 * 24 * 60 * 60 * 1000).toISOString(),
start: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('offset', 0);
expect(res.body).to.have.property('count');
expect(res.body).to.have.property('total');
expect(res.body).to.have.property('channels');
expect(res.body.channels).to.be.an('array').that.is.not.empty;
const channelRecord = res.body.channels.find(({ room }: { room: { _id: string } }) => room._id === testRoom._id);
expect(channelRecord).not.to.be.undefined;
expect(channelRecord).to.be.an('object').that.is.not.empty;
expect(channelRecord).to.have.property('messages', 0);
expect(channelRecord).to.have.property('lastWeekMessages', 1);
expect(channelRecord).to.have.property('diffFromLastWeek', -1);
expect(channelRecord.room).to.be.an('object').that.is.not.empty;
expect(channelRecord.room).to.have.property('_id', testRoom._id);
expect(channelRecord.room).to.have.property('name', testRoom.name);
expect(channelRecord.room).to.have.property('ts', testRoom.ts);
expect(channelRecord.room).to.have.property('t', testRoom.t);
});
});
});
});

View File

@@ -0,0 +1,94 @@
import { expect } from 'chai';
import { before, after, describe, it } from 'mocha';
import type { Response } from 'supertest';
import { getCredentials, api, request, credentials } from '../../data/api-data';
import { updatePermission } from '../../data/permissions.helper';
describe('LDAP', function () {
this.retries(0);
before((done) => getCredentials(done));
describe('[/ldap.syncNow]', () => {
it('should throw an error containing totp-required error when not running EE', async function () {
// TODO this is not the right way to do it. We're doing this way for now just because we have separate CI jobs for EE and CE,
// ideally we should have a single CI job that adds a license and runs both CE and EE tests.
if (process.env.IS_EE) {
this.skip();
}
await request
.post(api('ldap.syncNow'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'totp-required');
});
});
it('should throw an error of LDAP disabled when running EE', async function () {
if (!process.env.IS_EE) {
this.skip();
}
await request
.post(api('ldap.syncNow'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'LDAP_disabled');
});
});
});
describe('[/ldap.testSearch]', () => {
before(async () => {
return updatePermission('test-admin-options', ['admin']);
});
after(async () => {
return updatePermission('test-admin-options', ['admin']);
});
it('should not allow testing LDAP search if user does NOT have the test-admin-options permission', async () => {
await updatePermission('test-admin-options', []);
await request
.post(api('ldap.testSearch'))
.set(credentials)
.send({
username: 'test-search',
})
.expect('Content-Type', 'application/json')
.expect(403)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'User does not have the permissions required for this action [error-unauthorized]');
});
});
});
describe('[/ldap.testConnection]', () => {
before(async () => {
return updatePermission('test-admin-options', ['admin']);
});
after(async () => {
return updatePermission('test-admin-options', ['admin']);
});
it('should not allow testing LDAP connection if user does NOT have the test-admin-options permission', async () => {
await updatePermission('test-admin-options', []);
await request
.post(api('ldap.testConnection'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(403)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'User does not have the permissions required for this action [error-unauthorized]');
});
});
});
});

View File

@@ -0,0 +1,50 @@
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import type { Response } from 'supertest';
import { getCredentials, api, request, credentials } from '../../data/api-data';
import { updatePermission } from '../../data/permissions.helper';
describe('SAML', () => {
before((done) => getCredentials(done));
describe('[/saml.parseMetadata]', () => {
after(() => updatePermission('test-admin-options', ['admin']));
it('should fail without the test-admin-options permission', async () => {
await updatePermission('test-admin-options', []);
await request
.post(api('saml.parseMetadata'))
.set(credentials)
.send({ url: 'https://example.com/metadata.xml' })
.expect('Content-Type', 'application/json')
.expect(403);
await updatePermission('test-admin-options', ['admin']);
});
it('should reject a body without url', async () => {
await request
.post(api('saml.parseMetadata'))
.set(credentials)
.send({})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
});
});
it('should reject a URL blocked by SSRF protection', async () => {
await request
.post(api('saml.parseMetadata'))
.set(credentials)
.send({ url: 'http://169.254.169.254/latest/meta-data' })
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body.error).to.equal('SAML_Metadata_url_blocked');
});
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,83 @@
import type { Credentials } from '@rocket.chat/api-client';
import type { IUser } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import { api, credentials, getCredentials, request } from '../../data/api-data';
import { adminUsername, password } from '../../data/user';
import type { TestUser } from '../../data/users.helper';
import { createUser, deleteUser, login } from '../../data/users.helper';
describe('AI Search', () => {
before((done) => getCredentials(done));
describe('[/ai.search]', () => {
it('should fail when query is missing', async () => {
const response = await request.get(api('ai.search')).set(credentials);
expect(response.status).to.equal(400);
expect(response.body).to.have.property('success', false);
expect(response.body).to.have.property('error');
});
it('should return the AI search response shape for authenticated users', async () => {
const response = await request.get(api('ai.search')).query({ query: adminUsername, intelligentCount: 8 }).set(credentials);
expect(response.status).to.equal(200);
expect(response.body).to.have.property('success', true);
expect(response.body).to.have.property('intelligent').that.is.an('array');
expect(response.body).to.have.property('meta');
expect(response.body.meta).to.include.keys(['intelligentSearchEnabled', 'intelligentSearchConfigured', 'answerGenerationConfigured']);
});
});
describe('[/ai.search.answer]', () => {
it('should fail when not authenticated', async () => {
const response = await request
.post(api('ai.search.answer'))
.send({ query: 'example question', messages: [{ _id: 'example-message-id' }] });
expect(response.status).to.equal(401);
});
it('should reject answer generation without source messages', async () => {
const response = await request.post(api('ai.search.answer')).set(credentials).send({ query: 'example question', messages: [] });
expect(response.status).to.equal(400);
expect(response.body).to.have.property('success', false);
});
});
describe('[/ai.llm.models]', () => {
let user: TestUser<IUser>;
let userCredentials: Credentials;
before(async () => {
user = await createUser();
userCredentials = await login(user.username, password);
});
after(() => deleteUser(user));
it('should fail when not authenticated', async () => {
const response = await request.get(api('ai.llm.models'));
expect(response.status).to.equal(401);
});
it('should fail for users without settings permissions', async () => {
const response = await request.get(api('ai.llm.models')).set(userCredentials);
expect(response.status).to.equal(403);
expect(response.body).to.have.property('success', false);
});
it('should return model options for authorized users', async () => {
const response = await request.get(api('ai.llm.models')).set(credentials);
expect(response.status).to.equal(200);
expect(response.body).to.have.property('success', true);
expect(response.body).to.have.property('data').that.is.an('array');
});
});
});

View File

@@ -0,0 +1,15 @@
import { describe, it } from 'mocha';
import { request } from '../../data/api-data';
describe('assets', () => {
it('should always have CORS headers for assets', async () => {
await request.get('/assets/favicon.svg').expect('Content-Type', 'image/svg+xml').expect('Access-Control-Allow-Origin', '*').expect(200);
await request
.get('/fonts/rocketchat.woff2')
.expect('Content-Type', 'font/woff2')
.expect('Access-Control-Allow-Origin', '*')
.expect(200);
});
});

View File

@@ -0,0 +1,69 @@
import { expect } from 'chai';
import { before, describe, it, after } from 'mocha';
import { getCredentials, api, request, credentials } from '../../data/api-data';
import { imgURL } from '../../data/interactions';
import { updatePermission } from '../../data/permissions.helper';
describe('[Assets]', () => {
before((done) => getCredentials(done));
before(() => updatePermission('manage-assets', ['admin']));
after(() => updatePermission('manage-assets', ['admin']));
describe('[/assets.setAsset]', () => {
it('should set the "logo" asset', async () => {
const res = await request
.post(api('assets.setAsset'))
.set(credentials)
.attach('asset', imgURL)
.field({
assetName: 'logo',
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(res.body).to.have.property('success', true);
});
it('should throw an error when we try set an invalid asset', async () => {
const res = await request
.post(api('assets.setAsset'))
.set(credentials)
.attach('invalidAsset', imgURL)
.expect('Content-Type', 'application/json')
.expect(400);
expect(res.body).to.have.property('success', false);
});
});
describe('[/assets.unsetAsset]', () => {
it('should unset the "logo" asset', async () => {
const res = await request
.post(api('assets.unsetAsset'))
.set(credentials)
.send({
assetName: 'logo',
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(res.body).to.have.property('success', true);
});
it('should throw an error when we try set an invalid asset', async () => {
const res = await request
.post(api('assets.unsetAsset'))
.set(credentials)
.send({
assetName: 'invalidAsset',
})
.expect('Content-Type', 'application/json')
.expect(400);
expect(res.body).to.have.property('success', false);
});
});
});

View File

@@ -0,0 +1,439 @@
import type { Credentials } from '@rocket.chat/api-client';
import type { IRoom, IUser } from '@rocket.chat/core-typings';
import { Random } from '@rocket.chat/random';
import { expect } from 'chai';
import EJSON from 'ejson';
import { before, describe, it, after } from 'mocha';
import { getCredentials, api, request, credentials, methodCall } from '../../data/api-data';
import { updatePermission } from '../../data/permissions.helper';
import { createRoom, deleteRoom } from '../../data/rooms.helper';
import { password } from '../../data/user';
import { createUser, deleteUser, login } from '../../data/users.helper';
import { IS_EE } from '../../e2e/config/constants';
(IS_EE ? describe : describe.skip)('Audit Panel', () => {
let testChannel: IRoom;
let testPrivateChannel: IRoom;
let dummyUser: IUser;
let auditor: IUser;
let auditorCredentials: Credentials;
before((done) => getCredentials(done));
before(async () => {
testChannel = (await createRoom({ type: 'c', name: `chat.api-test-${Date.now()}` })).body.channel;
testPrivateChannel = (await createRoom({ type: 'p', name: `chat.api-test-${Date.now()}` })).body.group;
dummyUser = await createUser();
auditor = await createUser({ roles: ['user', 'auditor'] });
auditorCredentials = await login(auditor.username, password);
// the auditor role has can-audit but not can-audit-log by default; grant it suite-wide for the audit.auditions cases
await updatePermission('can-audit-log', ['admin', 'auditor']);
});
after(async () => {
await updatePermission('can-audit-log', ['admin']);
await deleteRoom({ type: 'c', roomId: testChannel._id });
await deleteUser({ _id: dummyUser._id });
await deleteUser({ _id: auditor._id });
await deleteRoom({ type: 'p', roomId: testPrivateChannel._id });
});
describe('audit/rooms.members [no permissions]', () => {
before(async () => {
await updatePermission('view-members-list-all-rooms', []);
});
after(async () => {
await updatePermission('view-members-list-all-rooms', ['admin', 'auditor']);
});
it('should fail if user does not have view-members-list-all-rooms permission', async () => {
await request
.get(api('audit/rooms.members'))
.set(credentials)
.query({
roomId: 'GENERAL',
})
.expect(403);
await request
.get(api('audit/rooms.members'))
.set(auditorCredentials)
.query({
roomId: 'GENERAL',
})
.expect(403);
});
});
describe('audit/rooms.members', () => {
it('should fail if user is not logged in', async () => {
await request
.get(api('audit/rooms.members'))
.query({
roomId: 'GENERAL',
})
.expect(401);
});
it('should fail if roomId is invalid', async () => {
await request
.get(api('audit/rooms.members'))
.set(credentials)
.query({
roomId: Random.id(),
})
.expect(404);
});
it('should fail if roomId is not present', async () => {
await request.get(api('audit/rooms.members')).set(credentials).query({}).expect(400);
});
it('should fail if roomId is an empty string', async () => {
await request
.get(api('audit/rooms.members'))
.set(credentials)
.query({
roomId: '',
})
.expect(400);
});
it('should fetch the members of a room', async () => {
await request
.get(api('audit/rooms.members'))
.set(credentials)
.query({
roomId: testChannel._id,
})
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body.members).to.be.an('array');
expect(res.body.members).to.have.lengthOf(1);
});
});
it('should persist a log entry', async () => {
await request
.get(api('audit/rooms.members'))
.set(credentials)
.query({
roomId: testChannel._id,
})
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body.members).to.be.an('array');
expect(res.body.members).to.have.lengthOf(1);
});
await request
.post(methodCall('auditGetAuditions'))
.set(credentials)
.send({
message: EJSON.stringify({
method: 'auditGetAuditions',
params: [{ startDate: new Date(Date.now() - 86400000), endDate: new Date() }],
id: 'id',
msg: 'method',
}),
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
const message = JSON.parse(res.body.message);
expect(message.result).to.be.an('array').with.lengthOf.greaterThan(1);
const entry = message.result.find((audition: any) => {
return audition.fields?.rids?.includes(testChannel._id);
});
expect(entry).to.have.property('u').that.is.an('object').deep.equal({
_id: 'rocketchat.internal.admin.test',
username: 'rocketchat.internal.admin.test',
name: 'RocketChat Internal Admin Test',
});
expect(entry).to.have.property('fields').that.is.an('object');
const { fields } = entry;
expect(fields).to.have.property('msg', 'Room_members_list');
expect(fields).to.have.property('rids').that.is.an('array').with.lengthOf(1);
});
});
it('should fetch the members of a room with offset and count', async () => {
await request
.post(methodCall('addUsersToRoom'))
.set(credentials)
.send({
message: JSON.stringify({
method: 'addUsersToRoom',
params: [{ rid: testChannel._id, users: [dummyUser.username] }],
id: 'id',
msg: 'method',
}),
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
});
await request
.get(api('audit/rooms.members'))
.set(credentials)
.query({
roomId: testChannel._id,
offset: 1,
count: 1,
})
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body.members).to.be.an('array');
expect(res.body.members).to.have.lengthOf(1);
expect(res.body.members[0].username).to.be.equal(dummyUser.username);
expect(res.body.total).to.be.equal(2);
expect(res.body.offset).to.be.equal(1);
expect(res.body.count).to.be.equal(1);
});
});
it('should filter by username', async () => {
await request
.get(api('audit/rooms.members'))
.set(credentials)
.query({
roomId: testChannel._id,
filter: dummyUser.username,
})
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body.members).to.be.an('array');
expect(res.body.members).to.have.lengthOf(1);
expect(res.body.members[0].username).to.be.equal(dummyUser.username);
});
});
it('should filter by user name', async () => {
await request
.get(api('audit/rooms.members'))
.set(credentials)
.query({
roomId: testChannel._id,
filter: dummyUser.name,
})
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body.members).to.be.an('array');
expect(res.body.members).to.have.lengthOf(1);
expect(res.body.members[0].name).to.be.equal(dummyUser.name);
});
});
it('should sort by username', async () => {
await request
.get(api('audit/rooms.members'))
.set(credentials)
.query({
roomId: testChannel._id,
sort: '{ "username": -1 }',
})
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body.members).to.be.an('array');
expect(res.body.members).to.have.lengthOf(2);
expect(res.body.members[1].username).to.be.equal('rocketchat.internal.admin.test');
expect(res.body.members[0].username).to.be.equal(dummyUser.username);
});
});
it('should not allow nosqlinjection on filter param', async () => {
await request
.get(api('audit/rooms.members'))
.set(credentials)
.query({
roomId: testChannel._id,
filter: '{ "$ne": "rocketchat.internal.admin.test" }',
})
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body.members).to.be.an('array');
expect(res.body.members).to.have.lengthOf(0);
});
await request
.get(api('audit/rooms.members'))
.set(credentials)
.query({
roomId: testChannel._id,
filter: { username: 'rocketchat.internal.admin.test' },
})
.expect(400);
});
it('should allow to fetch info even if user is not in the room', async () => {
await request
.get(api('audit/rooms.members'))
.set(auditorCredentials)
.query({
roomId: testChannel._id,
})
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body.members).to.be.an('array');
expect(res.body.members[0].username).to.be.equal('rocketchat.internal.admin.test');
expect(res.body.members[1].username).to.be.equal(dummyUser.username);
expect(res.body.total).to.be.equal(2);
});
});
it('should allow to fetch info from private rooms', async () => {
await request
.get(api('audit/rooms.members'))
.set(auditorCredentials)
.query({
roomId: testPrivateChannel._id,
})
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body.members).to.be.an('array');
expect(res.body.members[0].username).to.be.equal('rocketchat.internal.admin.test');
expect(res.body.total).to.be.equal(1);
});
});
});
describe('[/audit.auditions]', () => {
const startDate = new Date(0).toISOString();
const endDate = new Date().toISOString();
it('should fail if user is not logged in', () => request.get(api('audit.auditions')).query({ startDate, endDate }).expect(401));
it('should fail with 400 when startDate/endDate are missing', () =>
request.get(api('audit.auditions')).set(auditorCredentials).query({}).expect(400));
it('should fail with 400 when a date is invalid', () =>
request.get(api('audit.auditions')).set(auditorCredentials).query({ startDate: 'not-a-date', endDate }).expect(400));
it('should fail with 403 when the user lacks the can-audit-log permission', async () => {
await updatePermission('can-audit-log', []);
await request.get(api('audit.auditions')).set(auditorCredentials).query({ startDate, endDate }).expect(403);
await updatePermission('can-audit-log', ['admin', 'auditor']);
});
it('should return the list of auditions', () =>
request
.get(api('audit.auditions'))
.set(auditorCredentials)
.query({ startDate, endDate })
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('auditions').that.is.an('array');
}));
});
describe('[/audit.messages]', () => {
const startDate = new Date(0).toISOString();
const endDate = new Date().toISOString();
it('should fail if user is not logged in', () =>
request.post(api('audit.messages')).send({ startDate, endDate, users: [], msg: '', type: 'u' }).expect(401));
it('should fail with 400 when required params are missing', () =>
request.post(api('audit.messages')).set(auditorCredentials).send({ startDate, endDate }).expect(400));
it('should fail with 400 when a date is invalid', () =>
request
.post(api('audit.messages'))
.set(auditorCredentials)
.send({ startDate: 'not-a-date', endDate, users: [], msg: '', type: 'u' })
.expect(400));
it('should fail with 403 when the user lacks the can-audit permission', async () => {
await updatePermission('can-audit', []);
await request
.post(api('audit.messages'))
.set(auditorCredentials)
.send({ startDate, endDate, users: [], msg: '', type: 'u' })
.expect(403);
await updatePermission('can-audit', ['admin', 'auditor']);
});
it('should return matching messages', () =>
request
.post(api('audit.messages'))
.set(auditorCredentials)
.send({ startDate, endDate, users: [], msg: '', type: 'u' })
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('messages').that.is.an('array');
}));
it('should persist an audit log entry', async () => {
// query auditions with a future endDate so the entry created by the request below (ts = now) is in range
const auditionsRange = { startDate, endDate: new Date(Date.now() + 60000).toISOString() };
const before = await request.get(api('audit.auditions')).set(auditorCredentials).query(auditionsRange);
await request
.post(api('audit.messages'))
.set(auditorCredentials)
.send({ startDate, endDate, users: [], msg: '', type: 'u' })
.expect(200);
const after = await request.get(api('audit.auditions')).set(auditorCredentials).query(auditionsRange);
expect(after.body.auditions.length).to.be.greaterThan(before.body.auditions.length);
});
});
describe('[/audit.omnichannelMessages]', () => {
const startDate = new Date(0).toISOString();
const endDate = new Date().toISOString();
it('should fail if user is not logged in', () =>
request.post(api('audit.omnichannelMessages')).send({ startDate, endDate, users: [], msg: '', type: 'l' }).expect(401));
it('should fail with 400 when required params are missing', () =>
request.post(api('audit.omnichannelMessages')).set(auditorCredentials).send({ startDate, endDate }).expect(400));
it('should fail with 400 when a date is invalid', () =>
request
.post(api('audit.omnichannelMessages'))
.set(auditorCredentials)
.send({ startDate: 'not-a-date', endDate, users: [], msg: '', type: 'l' })
.expect(400));
it('should fail with 403 when the user lacks the can-audit permission', async () => {
await updatePermission('can-audit', []);
await request
.post(api('audit.omnichannelMessages'))
.set(auditorCredentials)
.send({ startDate, endDate, users: [], msg: '', type: 'l' })
.expect(403);
await updatePermission('can-audit', ['admin', 'auditor']);
});
it('should return matching omnichannel messages', () =>
request
.post(api('audit.omnichannelMessages'))
.set(auditorCredentials)
.send({ startDate, endDate, users: [], msg: '', type: 'l' })
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('messages').that.is.an('array');
}));
it('should persist an audit log entry', async () => {
// query auditions with a future endDate so the entry created by the request below (ts = now) is in range
const auditionsRange = { startDate, endDate: new Date(Date.now() + 60000).toISOString() };
const before = await request.get(api('audit.auditions')).set(auditorCredentials).query(auditionsRange);
await request
.post(api('audit.omnichannelMessages'))
.set(auditorCredentials)
.send({ startDate, endDate, users: [], msg: '', type: 'l' })
.expect(200);
const after = await request.get(api('audit.auditions')).set(auditorCredentials).query(auditionsRange);
expect(after.body.auditions.length).to.be.greaterThan(before.body.auditions.length);
});
});
});

View File

@@ -0,0 +1,726 @@
import type { Credentials } from '@rocket.chat/api-client';
import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { before, describe, after, it } from 'mocha';
import { getCredentials, api, request, credentials, methodCall } from '../../data/api-data';
import { sendSimpleMessage } from '../../data/chat.helper';
import { updatePermission, updateSetting } from '../../data/permissions.helper';
import { createRoom, deleteRoom } from '../../data/rooms.helper';
import { password } from '../../data/user';
import type { TestUser } from '../../data/users.helper';
import { createUser, deleteUser, login } from '../../data/users.helper';
const resetAutoTranslateDefaults = async () => {
await Promise.all([
updateSetting('AutoTranslate_Enabled', false),
updateSetting('AutoTranslate_AutoEnableOnJoinRoom', false),
updateSetting('Language', ''),
updatePermission('auto-translate', ['admin']),
]);
};
const resetE2EDefaults = async () => {
await Promise.all([updateSetting('E2E_Enabled_Default_PrivateRooms', false), updateSetting('E2E_Enable', false)]);
};
describe('AutoTranslate', () => {
before((done) => getCredentials(done));
describe('[AutoTranslate]', () => {
describe('[/autotranslate.getSupportedLanguages', () => {
before(() => resetAutoTranslateDefaults());
after(() => resetAutoTranslateDefaults());
it('should throw an error when the "AutoTranslate_Enabled" setting is disabled', async () => {
const res = await request
.get(api('autotranslate.getSupportedLanguages'))
.set(credentials)
.query({
targetLanguage: 'en',
})
.expect('Content-Type', 'application/json')
.expect(400);
expect(res.body).to.have.a.property('success', false);
expect(res.body.error).to.be.equal('AutoTranslate is disabled.');
});
it('should throw an error when the user does not have the "auto-translate" permission', async () => {
await updateSetting('AutoTranslate_Enabled', true);
await updatePermission('auto-translate', []);
const res = await request
.get(api('autotranslate.getSupportedLanguages'))
.set(credentials)
.query({
targetLanguage: 'en',
})
.expect('Content-Type', 'application/json')
.expect(400);
expect(res.body).to.have.a.property('success', false);
expect(res.body.errorType).to.be.equal('error-action-not-allowed');
expect(res.body.error).to.be.equal('Auto-Translate is not allowed [error-action-not-allowed]');
});
it('should return a list of languages', async () => {
await updatePermission('auto-translate', ['admin']);
await request
.get(api('autotranslate.getSupportedLanguages'))
.set(credentials)
.query({
targetLanguage: 'en',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.a.property('success', true);
expect(res.body.languages).to.be.an('array');
});
});
});
describe('[/autotranslate.saveSettings', () => {
let testGroupId: IRoom['_id'];
let testChannelId: IRoom['_id'];
before(async () => {
await Promise.all([
resetAutoTranslateDefaults(),
updateSetting('E2E_Enable', true),
updateSetting('E2E_Enabled_Default_PrivateRooms', true),
]);
testGroupId = (await createRoom({ type: 'p', name: `e2etest-autotranslate-${Date.now()}` })).body.group._id;
testChannelId = (await createRoom({ type: 'c', name: `test-autotranslate-${Date.now()}` })).body.channel._id;
});
after(async () => {
await Promise.all([
resetAutoTranslateDefaults(),
resetE2EDefaults(),
deleteRoom({ type: 'p', roomId: testGroupId }),
deleteRoom({ type: 'c', roomId: testChannelId }),
]);
});
it('should throw an error when the "AutoTranslate_Enabled" setting is disabled', async () => {
const res = await request
.post(api('autotranslate.saveSettings'))
.set(credentials)
.send({
roomId: testChannelId,
field: 'autoTranslate',
defaultLanguage: 'en',
value: true,
})
.expect('Content-Type', 'application/json')
.expect(400);
expect(res.body).to.have.a.property('success', false);
expect(res.body.error).to.be.equal('AutoTranslate is disabled.');
});
it('should throw an error when the user does not have the "auto-translate" permission', async () => {
await updateSetting('AutoTranslate_Enabled', true);
await updatePermission('auto-translate', []);
const res = await request
.post(api('autotranslate.saveSettings'))
.set(credentials)
.send({
roomId: testChannelId,
defaultLanguage: 'en',
field: 'autoTranslateLanguage',
value: 'en',
})
.expect('Content-Type', 'application/json')
.expect(400);
expect(res.body).to.have.a.property('success', false);
expect(res.body.errorType).to.be.equal('error-action-not-allowed');
expect(res.body.error).to.be.equal('Auto-Translate is not allowed [error-action-not-allowed]');
});
it('should throw an error when the bodyParam "roomId" is not provided', async () => {
await updatePermission('auto-translate', ['admin']);
const res = await request
.post(api('autotranslate.saveSettings'))
.set(credentials)
.send({})
.expect('Content-Type', 'application/json')
.expect(400);
expect(res.body).to.have.a.property('success', false);
});
it('should throw an error when the bodyParam "field" is not provided', async () => {
const res = await request
.post(api('autotranslate.saveSettings'))
.set(credentials)
.send({
roomId: testChannelId,
})
.expect('Content-Type', 'application/json')
.expect(400);
expect(res.body).to.have.a.property('success', false);
});
it('should throw an error when the bodyParam "value" is not provided', async () => {
const res = await request
.post(api('autotranslate.saveSettings'))
.set(credentials)
.send({
roomId: testChannelId,
field: 'autoTranslate',
})
.expect('Content-Type', 'application/json')
.expect(400);
expect(res.body).to.have.a.property('success', false);
});
it('should throw an error when the bodyParam "autoTranslate" is not a boolean', async () => {
const res = await request
.post(api('autotranslate.saveSettings'))
.set(credentials)
.send({
roomId: testChannelId,
field: 'autoTranslate',
value: 'test',
})
.expect('Content-Type', 'application/json')
.expect(400);
expect(res.body).to.have.a.property('success', false);
});
it('should throw an error when the bodyParam "autoTranslateLanguage" is not a string', async () => {
const res = await request
.post(api('autotranslate.saveSettings'))
.set(credentials)
.send({
roomId: testChannelId,
field: 'autoTranslateLanguage',
value: 12,
})
.expect('Content-Type', 'application/json')
.expect(400);
expect(res.body).to.have.a.property('success', false);
});
it('should throw an error when the bodyParam "field" is invalid', async () => {
const res = await request
.post(api('autotranslate.saveSettings'))
.set(credentials)
.send({
roomId: testChannelId,
field: 'invalid',
value: 12,
})
.expect('Content-Type', 'application/json')
.expect(400);
expect(res.body).to.have.a.property('success', false);
});
it('should throw an error when the bodyParam "roomId" is invalid or the user is not subscribed', async () => {
const res = await request
.post(api('autotranslate.saveSettings'))
.set(credentials)
.send({
roomId: 'invalid',
field: 'autoTranslateLanguage',
value: 'en',
})
.expect('Content-Type', 'application/json')
.expect(400);
expect(res.body).to.have.a.property('success', false);
expect(res.body.errorType).to.be.equal('error-invalid-subscription');
expect(res.body.error).to.be.equal('Invalid subscription [error-invalid-subscription]');
});
it('should throw an error when E2E encryption is enabled', async () => {
await request
.post(api('autotranslate.saveSettings'))
.set(credentials)
.send({
roomId: testGroupId,
field: 'autoTranslate',
defaultLanguage: 'en',
value: true,
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'error-e2e-enabled');
});
});
it('should return success when the setting is saved correctly', async () => {
const res = await request
.post(api('autotranslate.saveSettings'))
.set(credentials)
.send({
roomId: testChannelId,
field: 'autoTranslateLanguage',
value: 'en',
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(res.body).to.have.a.property('success', true);
});
});
describe('[/autotranslate.translateMessage', () => {
let messageSent: IMessage;
let testChannelId: IRoom['_id'];
before(async () => {
await resetAutoTranslateDefaults();
testChannelId = (await createRoom({ type: 'c', name: `test-autotranslate-message-${Date.now()}` })).body.channel._id;
const res = await sendSimpleMessage({
roomId: testChannelId,
text: 'Isso é um teste',
});
messageSent = res.body.message;
});
after(async () => {
await Promise.all([resetAutoTranslateDefaults(), deleteRoom({ type: 'c', roomId: testChannelId })]);
});
it('should throw an error when the "AutoTranslate_Enabled" setting is disabled', async () => {
const res = await request
.post(api('autotranslate.translateMessage'))
.set(credentials)
.send({
messageId: 'test',
targetLanguage: 'en',
})
.expect('Content-Type', 'application/json')
.expect(400);
expect(res.body).to.have.a.property('success', false);
expect(res.body.error).to.be.equal('AutoTranslate is disabled.');
});
it('should throw an error when the bodyParam "messageId" is not provided', async () => {
await updateSetting('AutoTranslate_Enabled', true);
await updatePermission('auto-translate', ['admin']);
const res = await request
.post(api('autotranslate.translateMessage'))
.set(credentials)
.send({})
.expect('Content-Type', 'application/json')
.expect(400);
expect(res.body).to.have.a.property('success', false);
});
it('should throw an error when the bodyParam "messageId" is invalid', async () => {
const res = await request
.post(api('autotranslate.translateMessage'))
.set(credentials)
.send({
messageId: 'invalid',
})
.expect('Content-Type', 'application/json')
.expect(400);
expect(res.body).to.have.a.property('success', false);
expect(res.body.error).to.be.equal('Message not found.');
});
it('should return success when the translate is successful', async () => {
const res = await request
.post(api('autotranslate.translateMessage'))
.set(credentials)
.send({
messageId: messageSent._id,
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(res.body).to.have.a.property('success', true);
});
describe('room access check', () => {
let userA: TestUser<IUser>;
let userB: TestUser<IUser>;
let credA: Credentials;
let credB: Credentials;
let privateRoom: IRoom;
let privateMessage: IMessage;
before(async () => {
await updateSetting('AutoTranslate_Enabled', true);
userA = await createUser();
userB = await createUser();
credA = await login(userA.username, password);
credB = await login(userB.username, password);
privateRoom = (
await createRoom({
type: 'p',
name: `test-autotranslate-access-${Date.now()}`,
credentials: credA,
})
).body.group;
const msgRes = await sendSimpleMessage({
roomId: privateRoom._id,
text: 'Isso é um teste',
userCredentials: credA,
});
privateMessage = msgRes.body.message;
});
after(async () => {
await Promise.all([
updateSetting('AutoTranslate_Enabled', false),
deleteUser(userA),
deleteUser(userB),
deleteRoom({ type: 'p', roomId: privateRoom._id }),
]);
});
it('should return 403 forbidden when the user is not a member of the room', async () => {
const res = await request
.post(api('autotranslate.translateMessage'))
.set(credB)
.send({
messageId: privateMessage._id,
})
.expect('Content-Type', 'application/json')
.expect(403);
expect(res.body).to.have.a.property('success', false);
});
});
});
describe('[autoTranslate.translateMessage method]', () => {
let userA: TestUser<IUser>;
let userB: TestUser<IUser>;
let credA: Credentials;
let credB: Credentials;
let privateRoom: IRoom;
let privateMessage: IMessage;
before(async () => {
await updateSetting('AutoTranslate_Enabled', true);
userA = await createUser();
userB = await createUser();
credA = await login(userA.username, password);
credB = await login(userB.username, password);
privateRoom = (
await createRoom({
type: 'p',
name: `test-autotranslate-method-${Date.now()}`,
credentials: credA,
})
).body.group;
const msgRes = await sendSimpleMessage({
roomId: privateRoom._id,
text: 'Isso é um teste',
userCredentials: credA,
});
privateMessage = msgRes.body.message;
});
after(async () => {
await Promise.all([
updateSetting('AutoTranslate_Enabled', false),
deleteUser(userA),
deleteUser(userB),
deleteRoom({ type: 'p', roomId: privateRoom._id }),
]);
});
it('should fail when messageId is not a string', async () => {
const res = await request
.post(methodCall('autoTranslate.translateMessage'))
.set(credA)
.send({
message: JSON.stringify({
msg: 'method',
id: 'id',
method: 'autoTranslate.translateMessage',
params: [{ _id: { $gt: '' } }, 'en'],
}),
})
.expect('Content-Type', 'application/json')
.expect(400);
expect(res.body).to.have.a.property('success', false);
const parsedBody = JSON.parse(res.body.message);
expect(parsedBody).to.have.a.property('error');
});
it('should return error-not-allowed when the caller is not a member of the room', async () => {
const res = await request
.post(methodCall('autoTranslate.translateMessage'))
.set(credB)
.send({
message: JSON.stringify({
msg: 'method',
id: 'id',
method: 'autoTranslate.translateMessage',
params: [{ _id: privateMessage._id }, 'en'],
}),
})
.expect('Content-Type', 'application/json')
.expect(400);
expect(res.body).to.have.a.property('success', false);
const parsedBody = JSON.parse(res.body.message);
expect(parsedBody).to.have.a.property('error');
expect(parsedBody.error).to.have.a.property('error', 'error-not-allowed');
});
});
describe('Autoenable setting', () => {
let userA: TestUser<IUser>;
let userB: TestUser<IUser>;
let credA: Credentials;
let credB: Credentials;
let channel: IRoom;
const channelsToRemove: IRoom[] = [];
const createChannel = async (members: string[] | undefined, cred: Credentials) =>
(await createRoom({ type: 'c', members, name: `channel-test-${Date.now()}`, credentials: cred })).body.channel;
const setLanguagePref = async (language: string, cred: Credentials) => {
await request
.post(api('users.setPreferences'))
.set(cred)
.send({ data: { language } })
.expect(200)
.expect('Content-Type', 'application/json')
.expect((res) => {
expect(res.body).to.have.property('success', true);
});
};
const getSub = async (roomId: IRoom['_id'], cred: Credentials) =>
(
await request
.get(api('subscriptions.getOne'))
.set(cred)
.query({
roomId,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('subscription').and.to.be.an('object');
})
).body.subscription;
before(async () => {
await Promise.all([
updateSetting('AutoTranslate_Enabled', true),
updateSetting('AutoTranslate_AutoEnableOnJoinRoom', true),
updateSetting('Language', 'pt-BR'),
]);
userA = await createUser();
userB = await createUser();
credA = await login(userA.username, password);
credB = await login(userB.username, password);
channel = await createChannel(undefined, credA);
await setLanguagePref('en', credB);
channelsToRemove.push(channel);
});
after(async () => {
await Promise.all([
updateSetting('AutoTranslate_AutoEnableOnJoinRoom', false),
updateSetting('AutoTranslate_Enabled', false),
updateSetting('Language', ''),
deleteUser(userA),
deleteUser(userB),
channelsToRemove.map(() => deleteRoom({ type: 'c', roomId: channel._id })),
]);
});
it("should do nothing if the user hasn't changed his language preference", async () => {
const sub = await getSub(channel._id, credA);
expect(sub).to.not.have.property('autoTranslate');
expect(sub).to.not.have.property('autoTranslateLanguage');
});
it("should do nothing if the user changed his language preference to be the same as the server's", async () => {
await setLanguagePref('pt-BR', credA);
const sub = await getSub(channel._id, credA);
expect(sub).to.not.have.property('autoTranslate');
expect(sub).to.not.have.property('autoTranslateLanguage');
});
it('should enable autotranslate with the correct language when joining a room', async () => {
await request
.post(api('channels.join'))
.set(credB)
.send({
roomId: channel._id,
})
.expect('Content-Type', 'application/json')
.expect(200);
const sub = await getSub(channel._id, credB);
expect(sub).to.have.property('autoTranslate');
expect(sub).to.have.property('autoTranslateLanguage').and.to.be.equal('en');
});
it('should enable autotranslate with the correct language when creating a new room', async () => {
await setLanguagePref('en', credA);
const newChannel = await createChannel(undefined, credA);
const sub = await getSub(newChannel._id, credA);
expect(sub).to.have.property('autoTranslate');
expect(sub).to.have.property('autoTranslateLanguage').and.to.be.equal('en');
channelsToRemove.push(newChannel);
});
it('should enable autotranslate for all the members added to the room upon creation', async () => {
const newChannel = await createChannel([userA.username, userB.username], credA);
const subA = await getSub(newChannel._id, credA);
expect(subA).to.have.property('autoTranslate');
expect(subA).to.have.property('autoTranslateLanguage').and.to.be.equal('en');
const subB = await getSub(newChannel._id, credB);
expect(subB).to.have.property('autoTranslate');
expect(subB).to.have.property('autoTranslateLanguage').and.to.be.equal('en');
channelsToRemove.push(newChannel);
});
it('should enable autotranslate with the correct language when added to a room', async () => {
const newChannel = await createChannel(undefined, credA);
await request
.post(api('channels.invite'))
.set(credA)
.send({
roomId: newChannel._id,
userId: userB._id,
})
.expect('Content-Type', 'application/json')
.expect(200);
const sub = await getSub(newChannel._id, credB);
expect(sub).to.have.property('autoTranslate');
expect(sub).to.have.property('autoTranslateLanguage').and.to.be.equal('en');
channelsToRemove.push(newChannel);
});
it('should change the auto translate language when the user changes his language preference', async () => {
await setLanguagePref('es', credA);
const newChannel = await createChannel(undefined, credA);
const subscription = await getSub(newChannel._id, credA);
expect(subscription).to.have.property('autoTranslate', true);
expect(subscription).to.have.property('autoTranslateLanguage', 'es');
channelsToRemove.push(newChannel);
});
it('should enable autotranslate on existing subscriptions when user changes from workspace language to a different one', async () => {
await setLanguagePref('pt-BR', credA);
const newChannel = await createChannel(undefined, credA);
channelsToRemove.push(newChannel);
const subBefore = await getSub(newChannel._id, credA);
expect(subBefore).to.not.have.property('autoTranslate');
expect(subBefore).to.not.have.property('autoTranslateLanguage');
await setLanguagePref('en', credA);
const subAfter = await getSub(newChannel._id, credA);
expect(subAfter).to.have.property('autoTranslate', true);
expect(subAfter).to.have.property('autoTranslateLanguage', 'en');
});
it("should enable autotranslate on existing subscriptions when user changes from 'default' language to a non-workspace language", async () => {
await setLanguagePref('default', credA);
const newChannel = await createChannel(undefined, credA);
channelsToRemove.push(newChannel);
const subBefore = await getSub(newChannel._id, credA);
expect(subBefore).to.not.have.property('autoTranslate');
expect(subBefore).to.not.have.property('autoTranslateLanguage');
await setLanguagePref('en', credA);
const subAfter = await getSub(newChannel._id, credA);
expect(subAfter).to.have.property('autoTranslate', true);
expect(subAfter).to.have.property('autoTranslateLanguage', 'en');
});
it('should disable autotranslate on existing subscriptions when user changes language to default', async () => {
await setLanguagePref('en', credA);
const newChannel = await createChannel(undefined, credA);
channelsToRemove.push(newChannel);
const subBefore = await getSub(newChannel._id, credA);
expect(subBefore).to.have.property('autoTranslate', true);
await setLanguagePref('default', credA);
const subAfter = await getSub(newChannel._id, credA);
expect(subAfter).to.not.have.property('autoTranslate');
expect(subAfter).to.not.have.property('autoTranslateLanguage');
});
it('should disable autotranslate on existing subscriptions when user changes language to the workspace default', async () => {
await setLanguagePref('en', credA);
const newChannel = await createChannel(undefined, credA);
channelsToRemove.push(newChannel);
const subBefore = await getSub(newChannel._id, credA);
expect(subBefore).to.have.property('autoTranslate', true);
await setLanguagePref('pt-BR', credA);
const subAfter = await getSub(newChannel._id, credA);
expect(subAfter).to.not.have.property('autoTranslate');
expect(subAfter).to.not.have.property('autoTranslateLanguage');
});
it('should not re-enable autotranslate for rooms where the user has explicitly opted out', async () => {
await updatePermission('auto-translate', ['admin', 'user']);
await setLanguagePref('en', credA);
const newChannel = await createChannel(undefined, credA);
channelsToRemove.push(newChannel);
const subEnabled = await getSub(newChannel._id, credA);
expect(subEnabled).to.have.property('autoTranslate', true);
await request
.post(api('autotranslate.saveSettings'))
.set(credA)
.send({ roomId: newChannel._id, field: 'autoTranslate', value: false, defaultLanguage: 'en' })
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
});
const subOptedOut = await getSub(newChannel._id, credA);
expect(subOptedOut).to.have.property('autoTranslate', false);
await setLanguagePref('es', credA);
const subAfterLangChange = await getSub(newChannel._id, credA);
expect(subAfterLangChange).to.have.property('autoTranslate', false);
});
});
});
});

View File

@@ -0,0 +1,288 @@
import type { IUser } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import { MongoClient } from 'mongodb';
import { getCredentials, api, request, credentials } from '../../data/api-data';
import { getMe } from '../../data/users.helper';
import { URL_MONGODB } from '../../e2e/config/constants';
describe('banners', () => {
before((done) => getCredentials(done));
describe('[/banners.dismiss]', () => {
it('should fail if not logged in', async () => {
const res = await request
.post(api('banners.dismiss'))
.send({
bannerId: '123',
})
.expect(401);
expect(res.body).to.have.property('status', 'error');
expect(res.body).to.have.property('message');
});
it('should fail if missing bannerId key', async () => {
const res = await request.post(api('banners.dismiss')).set(credentials).send({}).expect(400);
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'invalid-params');
});
it('should fail if bannerId is empty', async () => {
const res = await request
.post(api('banners.dismiss'))
.set(credentials)
.send({
bannerId: '',
})
.expect(400);
expect(res.body).to.have.property('success', false);
});
it('should fail if bannerId is invalid', async () => {
const res = await request
.post(api('banners.dismiss'))
.set(credentials)
.send({
bannerId: '123',
})
.expect(400);
expect(res.body).to.have.property('success', false);
});
describe('banners stored in the user record', () => {
let connection: MongoClient;
const bannerId = 'alert-user-banner-test';
const getUserBanners = async () => (await getMe<IUser>(credentials)).banners;
before(async () => {
connection = await MongoClient.connect(URL_MONGODB);
await connection
.db()
.collection<IUser>('users')
.updateOne(
{ _id: 'rocketchat.internal.admin.test' },
{
$set: {
[`banners.${bannerId}`]: {
id: bannerId,
priority: 10,
title: 'Banner_Title',
text: 'Banner_Text',
textArguments: [],
modifiers: [],
link: 'https://rocket.chat',
},
},
},
);
});
after(async () => {
await connection
.db()
.collection<IUser>('users')
.updateOne(
{ _id: 'rocketchat.internal.admin.test' },
{
$unset: { [`banners.${bannerId}`]: 1 },
},
);
await connection.close();
});
it('should mark the banner as read on the user record', async () => {
const res = await request
.post(api('banners.dismiss'))
.set(credentials)
.send({
bannerId,
})
.expect(200);
expect(res.body).to.have.property('success', true);
const banners = await getUserBanners();
expect(banners).to.have.nested.property(`${bannerId}.read`, true);
});
it('should succeed if the banner was already dismissed', async () => {
const res = await request
.post(api('banners.dismiss'))
.set(credentials)
.send({
bannerId,
})
.expect(200);
expect(res.body).to.have.property('success', true);
const banners = await getUserBanners();
expect(banners).to.have.nested.property(`${bannerId}.read`, true);
});
it('should not add an unknown banner to the user record', async () => {
const res = await request
.post(api('banners.dismiss'))
.set(credentials)
.send({
bannerId: 'an-unknown-banner-id',
})
.expect(400);
expect(res.body).to.have.property('success', false);
const banners = await getUserBanners();
expect(banners).to.not.have.property('an-unknown-banner-id');
});
});
});
describe('[/banners]', () => {
it('should fail if not logged in', async () => {
return request
.get(api('banners'))
.query({
platform: 'web',
})
.expect(401)
.expect((res) => {
expect(res.body).to.have.property('status', 'error');
expect(res.body).to.have.property('message');
});
});
it('should fail if missing platform', async () => {
return request
.get(api('banners'))
.set(credentials)
.query({})
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'error-invalid-params');
});
});
it('should fail if platform is invalid', async () => {
return request
.get(api('banners'))
.set(credentials)
.query({
platform: 'invalid-platform',
})
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'error-invalid-params');
});
});
it('should succesfully return web banners', async () => {
return request
.get(api('banners'))
.set(credentials)
.query({
platform: 'web',
})
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('banners').that.is.an('array');
});
});
it('should succesfully return mobile banners', async () => {
return request
.get(api('banners'))
.set(credentials)
.query({
platform: 'mobile',
})
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('banners').that.is.an('array');
});
});
});
describe('[/banners/:id]', () => {
it('should fail if not logged in', async () => {
return request
.get(api('banners/some-id'))
.query({
platform: 'web',
})
.expect(401)
.expect((res) => {
expect(res.body).to.have.property('status', 'error');
expect(res.body).to.have.property('message');
});
});
it('should fail if missing platform', async () => {
return request
.get(api('banners/some-id'))
.set(credentials)
.query({})
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'error-invalid-params');
});
});
it('should fail if platform is invalid', async () => {
return request
.get(api('banners/some-id'))
.set(credentials)
.query({
platform: 'invalid-platform',
})
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'error-invalid-params');
});
});
it('should succesfully return a web banner by id', async () => {
return request
.get(api('banners/some-id'))
.set(credentials)
.query({
platform: 'web',
})
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('banners').that.is.an('array');
});
});
it('should succesfully return a mobile banner by id', async () => {
return request
.get(api('banners/some-id'))
.set(credentials)
.query({
platform: 'mobile',
})
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('banners').that.is.an('array');
});
});
});
});

View File

@@ -0,0 +1,720 @@
import type { Credentials } from '@rocket.chat/api-client';
import type { IUser } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import type { Response } from 'supertest';
import { sleep } from '../../../lib/utils/sleep';
import { getCredentials, api, request, credentials } from '../../data/api-data';
import { password } from '../../data/user';
import { createUser, deleteUser, login } from '../../data/users.helper';
import { withTimeout } from '../../data/utils';
import { IS_EE } from '../../e2e/config/constants';
describe('[Calendar Events]', () => {
let user2: IUser;
let userCredentials: Credentials;
before((done) => getCredentials(done));
before(async () => {
user2 = await createUser();
userCredentials = await login(user2.username, password);
});
after(() => deleteUser(user2));
describe('[/calendar-events.create]', () => {
it('should successfully create an event in the calendar', async () => {
let eventId: string | undefined;
await request
.post(api('calendar-events.create'))
.set(credentials)
.send({
startTime: new Date().toISOString(),
subject: 'Subject',
description: 'Description',
reminderMinutesBeforeStart: 10,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
eventId = res.body.id;
});
after(async () => {
await request.post(api('calendar-events.delete')).set(credentials).send({ eventId });
});
});
it('should fail to create an event without a start time', async () => {
await request
.post(api('calendar-events.create'))
.set(credentials)
.send({
subject: 'Subject',
description: 'Description',
reminderMinutesBeforeStart: 10,
})
.expect('Content-Type', 'application/json')
.expect(400);
});
it('should fail to create an event without a subject', async () => {
await request
.post(api('calendar-events.create'))
.set(credentials)
.send({
description: 'Description',
startTime: new Date().toISOString(),
reminderMinutesBeforeStart: 10,
})
.expect('Content-Type', 'application/json')
.expect(400);
});
it('should fail to create an event without a description', async () => {
await request
.post(api('calendar-events.create'))
.set(credentials)
.send({
subject: 'Subject',
startTime: new Date().toISOString(),
reminderMinutesBeforeStart: 10,
})
.expect('Content-Type', 'application/json')
.expect(400);
});
it('should successfully create an event without reminder information', async () => {
let eventId: string | undefined;
await request
.post(api('calendar-events.create'))
.set(credentials)
.send({
startTime: new Date().toISOString(),
subject: 'Subject',
description: 'Description',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
eventId = res.body.id;
});
after(async () => {
await request.post(api('calendar-events.delete')).set(credentials).send({ eventId });
});
});
});
describe('[/calendar-events.list]', () => {
const testSubject = `calendar-events.list-${Date.now()}`;
const testSubject2 = `calendar-events.list-${Date.now()}`;
let eventId: string | undefined;
let eventId2: string | undefined;
let eventId3: string | undefined;
before('create sample events', async () => {
await request
.post(api('calendar-events.create'))
.set(credentials)
.send({
startTime: new Date().toISOString(),
subject: testSubject,
description: 'Description',
reminderMinutesBeforeStart: 10,
})
.then((res) => {
eventId = res.body.id;
});
await request
.post(api('calendar-events.create'))
.set(credentials)
.send({
startTime: new Date(Date.now() + 48 * 60 * 60 * 1000).toISOString(),
subject: testSubject2,
description: 'Future Event',
reminderMinutesBeforeStart: 10,
})
.then((res) => {
eventId2 = res.body.id;
});
await request
.post(api('calendar-events.create'))
.set(userCredentials)
.send({
startTime: new Date().toISOString(),
subject: testSubject,
description: 'Description',
reminderMinutesBeforeStart: 10,
})
.then((res) => {
eventId3 = res.body.id;
});
});
after(() =>
Promise.all([
request.post(api('calendar-events.delete')).set(credentials).send({ eventId }),
request.post(api('calendar-events.delete')).set(credentials).send({ eventId: eventId2 }),
request.post(api('calendar-events.delete')).set(userCredentials).send({ eventId: eventId3 }),
]),
);
it('should list only the events with the same date', async () => {
await request
.get(api('calendar-events.list'))
.set(credentials)
.query({
date: new Date().toISOString(),
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('data').that.is.an('array');
const events = res.body.data.map((event: any) => event._id);
expect(events).to.be.an('array').that.includes(eventId);
expect(events).to.not.includes(eventId2);
});
});
it('should nost list events from other users', async () => {
await request
.get(api('calendar-events.list'))
.set(userCredentials)
.query({
date: new Date().toISOString(),
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('data').that.is.an('array');
const events = res.body.data.map((event: any) => event._id);
expect(events).to.be.an('array').that.includes(eventId3);
expect(events).to.not.includes(eventId);
});
});
});
describe('[/calendar-events.info]', () => {
const testSubject = `calendar-events.info-${Date.now()}`;
let eventId: string | undefined;
let eventId2: string | undefined;
before('create sample events', async () => {
await request
.post(api('calendar-events.create'))
.set(credentials)
.send({
startTime: new Date().toISOString(),
subject: testSubject,
description: 'Description',
reminderMinutesBeforeStart: 10,
})
.then((res) => {
eventId = res.body.id;
});
await request
.post(api('calendar-events.create'))
.set(userCredentials)
.send({
startTime: new Date().toISOString(),
subject: testSubject,
description: 'Description',
reminderMinutesBeforeStart: 10,
})
.then((res) => {
eventId2 = res.body.id;
});
});
after(() =>
Promise.all([
request.post(api('calendar-events.delete')).set(credentials).send({ eventId }),
request.post(api('calendar-events.delete')).set(userCredentials).send({ eventId: eventId2 }),
]),
);
it('should return the event information', async () => {
await request
.get(api('calendar-events.info'))
.set(credentials)
.query({
id: eventId,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('event').that.is.an('object').with.property('subject', testSubject);
});
});
it('should return the event information - regular user', async () => {
await request
.get(api('calendar-events.info'))
.set(userCredentials)
.query({
id: eventId2,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('event').that.is.an('object').with.property('subject', testSubject);
});
});
it('should fail when querying an invalid event', async () => {
await request
.get(api('calendar-events.info'))
.set(credentials)
.query({
id: 'something-random',
})
.expect('Content-Type', 'application/json')
.expect(400);
});
it('should fail when querying an event from another user', async () => {
await request
.get(api('calendar-events.info'))
.set(credentials)
.query({
id: eventId2,
})
.expect('Content-Type', 'application/json')
.expect(400);
});
});
describe('[/calendar-events.import]', () => {
const eventsToRemove: Record<string, any>[] = [];
after(() =>
Promise.all([
...eventsToRemove.map(({ credentials, eventId }) => request.post(api('calendar-events.delete')).set(credentials).send({ eventId })),
]),
);
it('should successfully import an event to the calendar', async () => {
let eventId: string | undefined;
const externalId = `calendar-events.import-${Date.now()}`;
await request
.post(api('calendar-events.import'))
.set(credentials)
.send({
startTime: new Date().toISOString(),
subject: 'Subject',
description: 'Description',
reminderMinutesBeforeStart: 10,
externalId,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
eventId = res.body.id;
eventsToRemove.push({ credentials, eventId });
});
});
it('should fail to import an event without an external id', async () => {
await request
.post(api('calendar-events.import'))
.set(credentials)
.send({
startTime: new Date().toISOString(),
subject: 'Subject',
description: 'Description',
reminderMinutesBeforeStart: 10,
})
.expect('Content-Type', 'application/json')
.expect(400);
});
it('should fail to import an event without a start time', async () => {
await request
.post(api('calendar-events.import'))
.set(credentials)
.send({
subject: 'Subject',
description: 'Description',
reminderMinutesBeforeStart: 10,
})
.expect('Content-Type', 'application/json')
.expect(400);
});
it('should fail to import an event without a subject', async () => {
await request
.post(api('calendar-events.import'))
.set(credentials)
.send({
description: 'Description',
startTime: new Date().toISOString(),
reminderMinutesBeforeStart: 10,
})
.expect('Content-Type', 'application/json')
.expect(400);
});
it('should fail to import an event without a description', async () => {
await request
.post(api('calendar-events.import'))
.set(credentials)
.send({
subject: 'Subject',
startTime: new Date().toISOString(),
reminderMinutesBeforeStart: 10,
})
.expect('Content-Type', 'application/json')
.expect(400);
});
it('should successfully import an event without reminder information', async () => {
let eventId: string | undefined;
await request
.post(api('calendar-events.import'))
.set(credentials)
.send({
startTime: new Date().toISOString(),
subject: 'Subject',
description: 'Description',
externalId: `calendar-events.import-external-id-${Date.now()}`,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
eventId = res.body.id;
});
after(async () => {
await request.post(api('calendar-events.delete')).set(credentials).send({ eventId });
});
});
it('should import a new event even if it was already imported by another user', async () => {
let eventId: string | undefined;
let eventId2: string | undefined;
const externalId = `calendar-events.import-${Date.now()}`;
await request
.post(api('calendar-events.import'))
.set(userCredentials)
.send({
startTime: new Date().toISOString(),
subject: 'First User',
description: 'Description',
reminderMinutesBeforeStart: 10,
externalId,
})
.then((res) => {
eventId = res.body.id;
eventsToRemove.push({ credentials: userCredentials, eventId });
});
await request
.post(api('calendar-events.import'))
.set(credentials)
.send({
startTime: new Date().toISOString(),
subject: 'Second User',
description: 'Description',
reminderMinutesBeforeStart: 10,
externalId,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body.id).to.not.be.equal(eventId);
eventId2 = res.body.id;
eventsToRemove.push({ credentials, eventId: eventId2 });
});
await request
.get(api('calendar-events.info'))
.set(userCredentials)
.query({ id: eventId })
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('event').that.is.an('object').with.property('subject', 'First User');
});
await request
.get(api('calendar-events.info'))
.set(credentials)
.query({ id: eventId2 })
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('event').that.is.an('object').with.property('subject', 'Second User');
});
});
it('should update an event that has the same external id', async () => {
let eventId: string | undefined;
const externalId = `calendar-events.import-twice-${Date.now()}`;
await request
.post(api('calendar-events.import'))
.set(credentials)
.send({
startTime: new Date().toISOString(),
subject: 'Subject',
description: 'Description',
reminderMinutesBeforeStart: 10,
externalId,
})
.then((res) => {
eventId = res.body.id;
eventsToRemove.push({ credentials, eventId });
});
await request
.post(api('calendar-events.import'))
.set(credentials)
.send({
startTime: new Date().toISOString(),
subject: 'New Subject',
description: 'New Description',
reminderMinutesBeforeStart: 15,
externalId,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect(async (res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('id', eventId);
});
await request
.get(api('calendar-events.info'))
.set(credentials)
.query({ id: eventId })
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('event').that.is.an('object').with.property('subject', 'New Subject');
});
});
});
describe('[/calendar-events.update]', () => {
const testSubject = `calendar-events.update-${Date.now()}`;
let eventId: string | undefined;
before('create sample events', async () => {
await request
.post(api('calendar-events.create'))
.set(userCredentials)
.send({
startTime: new Date().toISOString(),
subject: 'Old Subject',
description: 'Old Description',
reminderMinutesBeforeStart: 10,
})
.then((res) => {
eventId = res.body.id;
});
});
after(async () => {
await request.post(api('calendar-events.delete')).set(userCredentials).send({ eventId });
});
it('should update the event with the new data', async () => {
await request
.post(api('calendar-events.update'))
.set(userCredentials)
.send({
eventId,
startTime: new Date().toISOString(),
subject: testSubject,
description: 'New Description',
reminderMinutesBeforeStart: 15,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect(async (res: Response) => {
expect(res.body).to.have.property('success', true);
});
await request
.get(api('calendar-events.info'))
.set(userCredentials)
.query({ id: eventId })
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('event').that.is.an('object');
expect(res.body.event).to.have.property('subject', testSubject);
expect(res.body.event).to.have.property('description', 'New Description');
});
});
it('should fail to update an event that doesnt exist', async () => {
await request
.post(api('calendar-events.update'))
.set(userCredentials)
.send({
eventId: 'something-random',
startTime: new Date().toISOString(),
subject: testSubject,
description: 'New Description',
reminderMinutesBeforeStart: 15,
})
.expect('Content-Type', 'application/json')
.expect(400);
});
it('should fail to update an event from another user', async () => {
await request
.post(api('calendar-events.update'))
.set(credentials)
.send({
eventId,
startTime: new Date().toISOString(),
subject: 'Another Subject',
description: 'Third Description',
reminderMinutesBeforeStart: 20,
})
.expect('Content-Type', 'application/json')
.expect(400);
});
});
describe('[/calendar-events.delete]', () => {
let eventId: string | undefined;
before('create sample events', async () => {
await request
.post(api('calendar-events.create'))
.set(userCredentials)
.send({
startTime: new Date().toISOString(),
subject: 'Subject',
description: 'Description',
reminderMinutesBeforeStart: 10,
})
.then((res) => {
eventId = res.body.id;
});
});
it('should fail to delete an event from another user', async () => {
await request
.post(api('calendar-events.delete'))
.set(credentials)
.send({
eventId,
})
.expect('Content-Type', 'application/json')
.expect(400);
});
it('should delete the specified event', async () => {
await request
.post(api('calendar-events.delete'))
.set(userCredentials)
.send({
eventId,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect(async (res: Response) => {
expect(res.body).to.have.property('success', true);
});
await request
.get(api('calendar-events.info'))
.set(userCredentials)
.query({ id: eventId })
.expect('Content-Type', 'application/json')
.expect(400);
});
it('should fail to delete an event that doesnt exist', async () => {
await request
.post(api('calendar-events.delete'))
.set(userCredentials)
.send({
eventId: 'something-random',
})
.expect('Content-Type', 'application/json')
.expect(400);
});
});
(IS_EE ? describe : describe.skip)('[Calendar Events Status Sync]', () => {
before(async () => {
await request.post('/api/v1/users.setStatus').set(userCredentials).send({ status: 'online' }).expect(200);
});
// claim apply/clear land asynchronously, so poll instead of asserting on a fixed sleep
const waitForStatusSource = (
predicate: (source: string | undefined) => boolean,
{ timeout = 15000, interval = 500 } = {},
): Promise<string | undefined> =>
withTimeout(async (signal) => {
for (;;) {
const res = await request.get('/api/v1/users.getStatus').set(userCredentials).expect(200);
const source = res.body.statusSource;
if (predicate(source) || signal.aborted) {
return source;
}
await sleep(interval);
}
}, timeout);
it('should apply a calendar (external) claim for an in-progress event and clear it when removed', async () => {
const now = new Date();
const startTime = new Date(now.getTime() - 1000);
const endTime = new Date(now.getTime() + 60 * 60 * 1000);
const eventPayload = {
startTime: startTime.toISOString(),
endTime: endTime.toISOString(),
subject: 'Subject',
description: 'Description',
reminderMinutesBeforeStart: 1,
busy: true,
};
const createResponse = await request.post('/api/v1/calendar-events.create').set(userCredentials).send(eventPayload).expect(200);
const eventId = createResponse.body.id;
// REST-only user has no DDP session, so assert the claim via statusSource, not display status
expect(await waitForStatusSource((source) => source === 'external')).to.equal('external');
// the claim must carry the event's endTime as its expiry, so the presence engine can auto-clear it
const status = await request.get('/api/v1/users.getStatus').set(userCredentials).expect(200);
expect(new Date(status.body.statusExpiresAt).getTime()).to.equal(endTime.getTime());
await request.post('/api/v1/calendar-events.delete').set(userCredentials).send({ eventId }).expect(200);
expect(await waitForStatusSource((source) => source === undefined)).to.be.undefined;
});
});
});

View File

@@ -0,0 +1,560 @@
import type { Credentials } from '@rocket.chat/api-client';
import type { IUser } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import type { Response } from 'supertest';
import { getCredentials, api, request, credentials } from '../../data/api-data';
import { password } from '../../data/user';
import { createUser, deleteUser, login } from '../../data/users.helper';
describe('[Call History]', () => {
let user2: IUser;
let userCredentials: Credentials;
before((done) => getCredentials(done));
before(async () => {
user2 = await createUser();
userCredentials = await login(user2.username, password);
});
after(() => deleteUser(user2));
describe('[/call-history.list]', () => {
it('should list all history entries for that uid', async () => {
await request
.get(api('call-history.list'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('items').that.is.an('array');
expect(res.body.items).to.have.lengthOf(6);
expect(res.body).to.have.property('total', 6);
expect(res.body).to.have.property('count', 6);
const historyIds = res.body.items.map((item: any) => item._id);
expect(historyIds).to.include('rocketchat.internal.history.test.outbound');
expect(historyIds).to.include('rocketchat.internal.history.test.inbound');
expect(historyIds).to.include('rocketchat.external.history.test.outbound');
expect(historyIds).to.include('rocketchat.external.history.test.inbound');
expect(historyIds).to.include('rocketchat.internal.history.test.outbound.2');
expect(historyIds).to.include('rocketchat.internal.history.test.inbound.2');
const internalItem1 = res.body.items.find((item: any) => item._id === 'rocketchat.internal.history.test.outbound');
expect(internalItem1).to.have.property('callId', 'rocketchat.internal.call.test');
expect(internalItem1).to.have.property('state', 'ended');
expect(internalItem1).to.have.property('type', 'media-call');
expect(internalItem1).to.have.property('duration', 10);
expect(internalItem1).to.have.property('external', false);
expect(internalItem1).to.have.property('direction', 'outbound');
expect(internalItem1).to.have.property('contactId');
expect(internalItem1).to.have.property('contactName', 'Pineapple');
expect(internalItem1).to.have.property('contactUsername', 'fruit-001');
const internalItem2 = res.body.items.find((item: any) => item._id === 'rocketchat.internal.history.test.inbound');
expect(internalItem2).to.have.property('callId', 'rocketchat.internal.call.test.2');
expect(internalItem2).to.have.property('state', 'not-answered');
expect(internalItem2).to.have.property('type', 'media-call');
expect(internalItem2).to.have.property('duration', 10);
expect(internalItem2).to.have.property('external', false);
expect(internalItem2).to.have.property('direction', 'inbound');
expect(internalItem2).to.have.property('contactId');
expect(internalItem2).to.have.property('contactName', 'Apple');
expect(internalItem2).to.have.property('contactUsername', 'fruit-002');
const internalItem3 = res.body.items.find((item: any) => item._id === 'rocketchat.internal.history.test.outbound.2');
expect(internalItem3).to.have.property('callId', 'rocketchat.extra.call.test.1');
expect(internalItem3).to.have.property('state', 'transferred');
expect(internalItem3).to.have.property('direction', 'outbound');
expect(internalItem3).to.have.property('contactName', 'Grapefruit 002');
const internalItem4 = res.body.items.find((item: any) => item._id === 'rocketchat.internal.history.test.inbound.2');
expect(internalItem4).to.have.property('callId', 'rocketchat.extra.call.test.2');
expect(internalItem4).to.have.property('state', 'transferred');
expect(internalItem4).to.have.property('direction', 'inbound');
expect(internalItem4).to.have.property('contactName', 'Pasta 1');
const externalItem1 = res.body.items.find((item: any) => item._id === 'rocketchat.external.history.test.outbound');
expect(externalItem1).to.have.property('callId', 'rocketchat.external.call.test.outbound');
expect(externalItem1).to.have.property('state', 'failed');
expect(externalItem1).to.have.property('type', 'media-call');
expect(externalItem1).to.have.property('duration', 10);
expect(externalItem1).to.have.property('external', true);
expect(externalItem1).to.have.property('direction', 'outbound');
expect(externalItem1).to.have.property('contactExtension', '1001');
const externalItem2 = res.body.items.find((item: any) => item._id === 'rocketchat.external.history.test.inbound');
expect(externalItem2).to.have.property('callId', 'rocketchat.external.call.test.inbound');
expect(externalItem2).to.have.property('state', 'ended');
expect(externalItem2).to.have.property('type', 'media-call');
expect(externalItem2).to.have.property('duration', 10);
expect(externalItem2).to.have.property('external', true);
expect(externalItem2).to.have.property('direction', 'inbound');
expect(externalItem2).to.have.property('contactExtension', '1002');
});
});
it('should not list history entries from other users', async () => {
await request
.get(api('call-history.list'))
.set(userCredentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('items').that.is.an('array');
expect(res.body.items).to.have.lengthOf(0);
expect(res.body).to.have.property('total', 0);
expect(res.body).to.have.property('count', 0);
});
});
it('should apply filter by state', async () => {
await request
.get(api('call-history.list'))
.set(credentials)
.query({
state: ['ended'],
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('items').that.is.an('array');
expect(res.body.items).to.have.lengthOf(2);
expect(res.body).to.have.property('total', 2);
expect(res.body).to.have.property('count', 2);
const historyIds = res.body.items.map((item: any) => item._id);
expect(historyIds).to.include('rocketchat.internal.history.test.outbound');
expect(historyIds).to.include('rocketchat.external.history.test.inbound');
});
});
it('should apply filter by multiple states', async () => {
await request
.get(api('call-history.list'))
.set(credentials)
.query({
state: ['failed', 'ended'],
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('items').that.is.an('array');
expect(res.body.items).to.have.lengthOf(3);
expect(res.body).to.have.property('total', 3);
expect(res.body).to.have.property('count', 3);
const historyIds = res.body.items.map((item: any) => item._id);
expect(historyIds).to.include('rocketchat.internal.history.test.outbound');
expect(historyIds).to.include('rocketchat.external.history.test.outbound');
expect(historyIds).to.include('rocketchat.external.history.test.inbound');
});
});
it('should apply filter by direction', async () => {
await request
.get(api('call-history.list'))
.set(credentials)
.query({
direction: 'inbound',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('items').that.is.an('array');
expect(res.body.items).to.have.lengthOf(3);
expect(res.body).to.have.property('total', 3);
expect(res.body).to.have.property('count', 3);
const historyIds = res.body.items.map((item: any) => item._id);
expect(historyIds).to.include('rocketchat.internal.history.test.inbound');
expect(historyIds).to.include('rocketchat.external.history.test.inbound');
expect(historyIds).to.include('rocketchat.internal.history.test.inbound.2');
});
});
it('should apply filter by state and direction', async () => {
await request
.get(api('call-history.list'))
.set(credentials)
.query({
state: ['failed', 'ended'],
direction: 'inbound',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('items').that.is.an('array');
expect(res.body.items).to.have.lengthOf(1);
expect(res.body).to.have.property('total', 1);
expect(res.body).to.have.property('count', 1);
const historyIds = res.body.items.map((item: any) => item._id);
expect(historyIds).to.include('rocketchat.external.history.test.inbound');
});
});
it('should return item that match full contact name', async () => {
await request
.get(api('call-history.list'))
.set(credentials)
.query({
filter: 'Pineapple',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('items').that.is.an('array');
expect(res.body.items).to.have.lengthOf(1);
expect(res.body).to.have.property('total', 1);
expect(res.body).to.have.property('count', 1);
const historyIds = res.body.items.map((item: any) => item._id);
expect(historyIds).to.include('rocketchat.internal.history.test.outbound');
});
});
it('should return items that match partial contact name', async () => {
await request
.get(api('call-history.list'))
.set(credentials)
.query({
filter: 'Apple',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('items').that.is.an('array');
expect(res.body.items).to.have.lengthOf(2);
expect(res.body).to.have.property('total', 2);
expect(res.body).to.have.property('count', 2);
const historyIds = res.body.items.map((item: any) => item._id);
expect(historyIds).to.include('rocketchat.internal.history.test.outbound');
expect(historyIds).to.include('rocketchat.internal.history.test.inbound');
});
});
it('should return item that match full contact username', async () => {
await request
.get(api('call-history.list'))
.set(credentials)
.query({
filter: 'fruit-001',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('items').that.is.an('array');
expect(res.body.items).to.have.lengthOf(1);
expect(res.body).to.have.property('total', 1);
expect(res.body).to.have.property('count', 1);
const historyIds = res.body.items.map((item: any) => item._id);
expect(historyIds).to.include('rocketchat.internal.history.test.outbound');
});
});
it('should return items that match partial contact username', async () => {
await request
.get(api('call-history.list'))
.set(credentials)
.query({
filter: 'fruit-',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('items').that.is.an('array');
expect(res.body.items).to.have.lengthOf(2);
expect(res.body).to.have.property('total', 2);
expect(res.body).to.have.property('count', 2);
const historyIds = res.body.items.map((item: any) => item._id);
expect(historyIds).to.include('rocketchat.internal.history.test.outbound');
expect(historyIds).to.include('rocketchat.internal.history.test.inbound');
});
});
it('should return items that match partial contact name or username', async () => {
await request
.get(api('call-history.list'))
.set(credentials)
.query({
filter: 'fruit',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('items').that.is.an('array');
expect(res.body.items).to.have.lengthOf(3);
expect(res.body).to.have.property('total', 3);
expect(res.body).to.have.property('count', 3);
const historyIds = res.body.items.map((item: any) => item._id);
expect(historyIds).to.include('rocketchat.internal.history.test.outbound');
expect(historyIds).to.include('rocketchat.internal.history.test.inbound');
expect(historyIds).to.include('rocketchat.internal.history.test.outbound.2');
});
});
it('should return item that match full contact extension', async () => {
await request
.get(api('call-history.list'))
.set(credentials)
.query({
filter: '1001',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('items').that.is.an('array');
expect(res.body.items).to.have.lengthOf(1);
expect(res.body).to.have.property('total', 1);
expect(res.body).to.have.property('count', 1);
const historyIds = res.body.items.map((item: any) => item._id);
expect(historyIds).to.include('rocketchat.external.history.test.outbound');
});
});
it('should return items that match partial contact extension', async () => {
await request
.get(api('call-history.list'))
.set(credentials)
.query({
filter: '100',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('items').that.is.an('array');
expect(res.body.items).to.have.lengthOf(2);
expect(res.body).to.have.property('total', 2);
expect(res.body).to.have.property('count', 2);
const historyIds = res.body.items.map((item: any) => item._id);
expect(historyIds).to.include('rocketchat.external.history.test.outbound');
expect(historyIds).to.include('rocketchat.external.history.test.inbound');
});
});
it('should return items that match partial contact name, username or extension', async () => {
await request
.get(api('call-history.list'))
.set(credentials)
.query({
filter: '002',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('items').that.is.an('array');
expect(res.body.items).to.have.lengthOf(3);
expect(res.body).to.have.property('total', 3);
expect(res.body).to.have.property('count', 3);
const historyIds = res.body.items.map((item: any) => item._id);
expect(historyIds).to.include('rocketchat.internal.history.test.inbound');
expect(historyIds).to.include('rocketchat.internal.history.test.outbound.2');
expect(historyIds).to.include('rocketchat.external.history.test.inbound');
});
});
it('should apply filter with falsy value', async () => {
await request
.get(api('call-history.list'))
.set(credentials)
.query({
filter: '0',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('items').that.is.an('array');
expect(res.body.items).to.have.lengthOf(5);
expect(res.body).to.have.property('total', 5);
expect(res.body).to.have.property('count', 5);
const historyIds = res.body.items.map((item: any) => item._id);
expect(historyIds).to.include('rocketchat.internal.history.test.outbound');
expect(historyIds).to.include('rocketchat.internal.history.test.inbound');
expect(historyIds).to.include('rocketchat.internal.history.test.outbound.2');
expect(historyIds).to.include('rocketchat.external.history.test.outbound');
expect(historyIds).to.include('rocketchat.external.history.test.inbound');
});
});
});
describe('[/call-history.info]', () => {
it('should return the history entry information', async () => {
await request
.get(api('call-history.info'))
.set(credentials)
.query({
historyId: 'rocketchat.internal.history.test.outbound',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('item').that.is.an('object');
expect(res.body).to.have.property('call').that.is.an('object');
const { item, call } = res.body;
expect(item).to.have.property('_id', 'rocketchat.internal.history.test.outbound');
expect(item).to.have.property('callId', 'rocketchat.internal.call.test');
expect(item).to.have.property('state', 'ended');
expect(item).to.have.property('type', 'media-call');
expect(item).to.have.property('duration', 10);
expect(item).to.have.property('external', false);
expect(item).to.have.property('direction', 'outbound');
expect(item).to.have.property('contactId');
expect(item).to.have.property('contactName', 'Pineapple');
expect(item).to.have.property('contactUsername', 'fruit-001');
expect(item).to.have.property('ts');
expect(item).to.have.property('endedAt');
expect(call).to.have.property('_id', 'rocketchat.internal.call.test');
expect(call).to.have.property('service', 'webrtc');
expect(call).to.have.property('kind', 'direct');
expect(call).to.have.property('state', 'hangup');
expect(call).to.have.property('ended', true);
expect(call).to.have.property('hangupReason', 'normal');
expect(call).to.have.property('createdBy').that.is.an('object');
expect(call.createdBy).to.have.property('type', 'user');
expect(call).to.have.property('caller').that.is.an('object');
expect(call.caller).to.have.property('type', 'user');
expect(call).to.have.property('callee').that.is.an('object');
expect(call.callee).to.have.property('type', 'user');
expect(call).to.have.property('endedBy').that.is.an('object');
expect(call.endedBy).to.have.property('type', 'user');
expect(call).to.have.property('uids').that.is.an('array');
expect(call.uids).to.have.lengthOf(2);
});
});
it('should return the history entry information when searching by call id', async () => {
await request
.get(api('call-history.info'))
.set(credentials)
.query({
callId: 'rocketchat.internal.call.test.2',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('item').that.is.an('object');
expect(res.body).to.have.property('call').that.is.an('object');
const { item, call } = res.body;
expect(item).to.have.property('_id', 'rocketchat.internal.history.test.inbound');
expect(item).to.have.property('callId', 'rocketchat.internal.call.test.2');
expect(item).to.have.property('state', 'not-answered');
expect(item).to.have.property('type', 'media-call');
expect(item).to.have.property('duration', 10);
expect(item).to.have.property('external', false);
expect(item).to.have.property('direction', 'inbound');
expect(item).to.have.property('contactId');
expect(item).to.have.property('contactName', 'Apple');
expect(item).to.have.property('contactUsername', 'fruit-002');
expect(item).to.have.property('ts');
expect(item).to.have.property('endedAt');
expect(call).to.have.property('_id', 'rocketchat.internal.call.test.2');
expect(call).to.have.property('service', 'webrtc');
expect(call).to.have.property('kind', 'direct');
expect(call).to.have.property('state', 'hangup');
expect(call).to.have.property('ended', true);
expect(call).to.have.property('hangupReason', 'normal');
expect(call).to.have.property('createdBy').that.is.an('object');
expect(call.createdBy).to.have.property('type', 'user');
expect(call).to.have.property('caller').that.is.an('object');
expect(call.caller).to.have.property('type', 'user');
expect(call).to.have.property('callee').that.is.an('object');
expect(call.callee).to.have.property('type', 'user');
expect(call).to.have.property('endedBy').that.is.an('object');
expect(call.endedBy).to.have.property('type', 'user');
expect(call).to.have.property('uids').that.is.an('array');
expect(call.uids).to.have.lengthOf(2);
});
});
it('should fail when querying an invalid entry', async () => {
await request
.get(api('call-history.info'))
.set(credentials)
.query({
historyId: 'something-random',
})
.expect('Content-Type', 'application/json')
.expect(404);
});
it('should fail when querying an invalid entry by call id', async () => {
await request
.get(api('call-history.info'))
.set(credentials)
.query({
callId: 'something-random',
})
.expect('Content-Type', 'application/json')
.expect(404);
});
it('should fail when querying an entry from another user', async () => {
await request
.get(api('call-history.info'))
.set(userCredentials)
.query({
historyId: 'rocketchat.internal.history.test.outbound',
})
.expect('Content-Type', 'application/json')
.expect(404);
});
it('should fail when querying an entry from another user by call id', async () => {
await request
.get(api('call-history.info'))
.set(userCredentials)
.query({
callId: 'rocketchat.internal.call.test.2',
})
.expect('Content-Type', 'application/json')
.expect(404);
});
});
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,321 @@
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import type { Response } from 'supertest';
import { getCredentials, api, request, credentials } from '../../data/api-data';
import { updatePermission } from '../../data/permissions.helper';
describe('[Cloud]', function () {
this.retries(0);
before((done) => getCredentials(done));
describe('[/cloud.manualRegister]', () => {
before(async () => {
return updatePermission('register-on-cloud', ['admin']);
});
after(async () => {
return updatePermission('register-on-cloud', ['admin']);
});
it('should fail if user is not authenticated', async () => {
return request
.post(api('cloud.manualRegister'))
.expect('Content-Type', 'application/json')
.send({
cloudBlob: 'test-blob',
})
.expect(401)
.expect((res: Response) => {
expect(res.body).to.have.property('status', 'error');
expect(res.body).to.have.property('message', 'You must be logged in to do this.');
});
});
it('should fail when cloudBlob property is not provided', async () => {
return request
.post(api('cloud.manualRegister'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'invalid-params');
expect(res.body).to.have.property('error', "must have required property 'cloudBlob'");
});
});
it('should fail when user does not have the register-on-cloud permission', async () => {
await updatePermission('register-on-cloud', []);
return request
.post(api('cloud.manualRegister'))
.set(credentials)
.send({
cloudBlob: 'test-blob',
})
.expect('Content-Type', 'application/json')
.expect(403)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'User does not have the permissions required for this action [error-unauthorized]');
});
});
});
describe('[/cloud.createRegistrationIntent]', () => {
before(async () => {
return updatePermission('manage-cloud', ['admin']);
});
after(async () => {
return updatePermission('manage-cloud', ['admin']);
});
it('should fail if user is not authenticated', async () => {
return request
.post(api('cloud.createRegistrationIntent'))
.expect('Content-Type', 'application/json')
.send({
email: 'test-mail@example.com',
resend: true,
})
.expect(401)
.expect((res: Response) => {
expect(res.body).to.have.property('status', 'error');
expect(res.body).to.have.property('message', 'You must be logged in to do this.');
});
});
it('should fail when resend property is not provided', async () => {
return request
.post(api('cloud.createRegistrationIntent'))
.set(credentials)
.send({
email: 'test-mail@example.com',
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'invalid-params');
expect(res.body).to.have.property('error', "must have required property 'resend'");
});
});
it('should fail when email property is not provided', async () => {
return request
.post(api('cloud.createRegistrationIntent'))
.set(credentials)
.send({
resend: true,
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'invalid-params');
expect(res.body).to.have.property('error', "must have required property 'email'");
});
});
it('should fail when user does not have the manage-cloud permission', async () => {
await updatePermission('manage-cloud', []);
return request
.post(api('cloud.createRegistrationIntent'))
.set(credentials)
.send({
email: 'test-mail@example.com',
resend: true,
})
.expect('Content-Type', 'application/json')
.expect(403)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'User does not have the permissions required for this action [error-unauthorized]');
});
});
});
describe('[/cloud.confirmationPoll]', () => {
before(async () => {
return updatePermission('manage-cloud', ['admin']);
});
after(async () => {
return updatePermission('manage-cloud', ['admin']);
});
it('should fail if user is not authenticated', async () => {
return request
.get(api('cloud.confirmationPoll'))
.query({
deviceCode: 'test-code',
})
.expect('Content-Type', 'application/json')
.expect(401)
.expect((res: Response) => {
expect(res.body).to.have.property('status', 'error');
expect(res.body).to.have.property('message', 'You must be logged in to do this.');
});
});
it('should fail when deviceCode property is not provided', async () => {
return request
.get(api('cloud.confirmationPoll'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'error-invalid-params');
expect(res.body).to.have.property('error', "must have required property 'deviceCode'");
});
});
it('should fail when user does not have the manage-cloud permission', async () => {
await updatePermission('manage-cloud', []);
return request
.get(api('cloud.confirmationPoll'))
.set(credentials)
.query({
deviceCode: 'test-code',
})
.expect('Content-Type', 'application/json')
.expect(403)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'User does not have the permissions required for this action [error-unauthorized]');
});
});
});
describe('[/cloud.registrationStatus]', () => {
before(async () => {
return updatePermission('manage-cloud', ['admin']);
});
after(async () => {
return updatePermission('manage-cloud', ['admin']);
});
it('should fail if user is not authenticated', async () => {
return request
.get(api('cloud.registrationStatus'))
.expect('Content-Type', 'application/json')
.expect(401)
.expect((res: Response) => {
expect(res.body).to.have.property('status', 'error');
expect(res.body).to.have.property('message', 'You must be logged in to do this.');
});
});
it('should fail when user does not have the manage-cloud permission', async () => {
await updatePermission('manage-cloud', []);
return request
.get(api('cloud.registrationStatus'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(403)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
});
});
it('should return registration status when user has the manage-cloud permission', async () => {
await updatePermission('manage-cloud', ['admin']);
return request
.get(api('cloud.registrationStatus'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('registrationStatus');
});
});
});
describe('[/cloud.workspaceRegisterData]', () => {
before(async () => {
return updatePermission('manage-cloud', ['admin']);
});
after(async () => {
return updatePermission('manage-cloud', ['admin']);
});
it('should fail if user is not authenticated', async () => {
return request
.get(api('cloud.workspaceRegisterData'))
.expect('Content-Type', 'application/json')
.expect(401)
.expect((res: Response) => {
expect(res.body).to.have.property('status', 'error');
});
});
it('should fail when user does not have the manage-cloud permission', async () => {
await updatePermission('manage-cloud', []);
return request
.get(api('cloud.workspaceRegisterData'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(403)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
});
});
it('should return a base64 encoded registration payload when user has the manage-cloud permission', async () => {
await updatePermission('manage-cloud', ['admin']);
return request
.get(api('cloud.workspaceRegisterData'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('registerData').that.is.a('string');
const decoded = JSON.parse(Buffer.from(res.body.registerData, 'base64').toString());
expect(decoded).to.have.property('uniqueId');
expect(decoded).to.have.property('workspaceId');
});
});
});
describe('[/cloud.connectWorkspace]', () => {
before(async () => {
return updatePermission('manage-cloud', ['admin']);
});
after(async () => {
return updatePermission('manage-cloud', ['admin']);
});
it('should fail if user is not authenticated', async () => {
return request
.post(api('cloud.connectWorkspace'))
.send({ token: 'invalid-token' })
.expect('Content-Type', 'application/json')
.expect(401)
.expect((res: Response) => {
expect(res.body).to.have.property('status', 'error');
});
});
it('should fail when user does not have the manage-cloud permission', async () => {
await updatePermission('manage-cloud', []);
return request
.post(api('cloud.connectWorkspace'))
.set(credentials)
.send({ token: 'invalid-token' })
.expect('Content-Type', 'application/json')
.expect(403)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
});
});
});
});

View File

@@ -0,0 +1,581 @@
import type { Credentials } from '@rocket.chat/api-client';
import type { IRoom, IThreadMessage, IUser } from '@rocket.chat/core-typings';
import { Random } from '@rocket.chat/random';
import { expect } from 'chai';
import { before, describe, it, after } from 'mocha';
import { getCredentials, api, request, credentials } from '../../data/api-data';
import { sendSimpleMessage } from '../../data/chat.helper';
import { createRoom, deleteRoom } from '../../data/rooms.helper';
import { password } from '../../data/user';
import type { TestUser } from '../../data/users.helper';
import { createUser, deleteUser, login } from '../../data/users.helper';
describe('[Commands]', () => {
before((done) => getCredentials(done));
describe('[/commands.get]', () => {
it('should return an error when call the endpoint without "command" required parameter', async () => {
const res = await request.get(api('commands.get')).set(credentials).expect(400);
expect(res.body).to.have.property('success', false);
expect(res.body.error).to.be.equal(`must have required property 'command'`);
});
it('should return an error when call the endpoint with an invalid command', async () => {
const res = await request
.get(api('commands.get'))
.set(credentials)
.query({
command: 'invalid-command',
})
.expect(400);
expect(res.body).to.have.property('success', false);
expect(res.body.error).to.be.equal('There is no command in the system by the name of: invalid-command');
});
it('should return success when parameters are correct', async () => {
const res = await request
.get(api('commands.get'))
.set(credentials)
.query({
command: 'help',
})
.expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('command');
expect(res.body.command).to.have.property('command');
expect(res.body.command).to.have.property('description');
expect(res.body.command).to.have.property('clientOnly');
expect(res.body.command).to.have.property('providesPreview');
});
});
describe('[/commands.list]', () => {
it('should return a list of commands', async () => {
const res = await request.get(api('commands.list')).set(credentials).expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('offset');
expect(res.body).to.have.property('count');
expect(res.body).to.have.property('total');
expect(res.body).to.have.property('commands').and.to.be.an('array');
});
it('should return a list of commands even requested with count and offset params', async () => {
const res = await request
.get(api('commands.list'))
.set(credentials)
.query({
count: 5,
offset: 0,
})
.expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('offset');
expect(res.body).to.have.property('count');
expect(res.body).to.have.property('total');
expect(res.body).to.have.property('commands').and.to.be.an('array');
});
});
describe('[/commands.run]', () => {
let testChannel: IRoom;
let threadMessage: IThreadMessage;
before(async () => {
testChannel = (await createRoom({ type: 'c', name: `channel.test.commands.${Date.now()}` })).body.channel;
const { body: { message } = {} } = await sendSimpleMessage({
roomId: testChannel._id,
text: 'Message to create thread',
});
threadMessage = (
await sendSimpleMessage({
roomId: testChannel._id,
text: 'Thread Message',
tmid: message._id,
})
).body.message;
});
after(() => deleteRoom({ type: 'c', roomId: testChannel._id }));
it('should return an error when call the endpoint without "command" required parameter', async () => {
const res = await request.post(api('commands.run')).set(credentials).expect(400);
expect(res.body).to.have.property('success', false);
expect(res.body.error).to.be.equal("must have required property 'command'");
});
it('should coerce non-string "params" to string via ajv coercion', async () => {
const res = await request
.post(api('commands.run'))
.set(credentials)
.send({
command: 'help',
roomId: 'GENERAL',
params: true,
})
.expect(200);
expect(res.body).to.have.property('success', true);
});
it('should return an error when call the endpoint without "roomId" required parameter', async () => {
const res = await request
.post(api('commands.run'))
.set(credentials)
.send({
command: 'help',
params: 'params',
})
.expect(400);
expect(res.body).to.have.property('success', false);
expect(res.body.error).to.be.equal("must have required property 'roomId'");
});
it('should coerce non-string "tmid" to string via ajv coercion and fail with invalid thread', async () => {
const res = await request
.post(api('commands.run'))
.set(credentials)
.send({
command: 'help',
params: 'params',
roomId: 'GENERAL',
tmid: true,
})
.expect(400);
expect(res.body).to.have.property('success', false);
expect(res.body.error).to.be.equal('Invalid thread.');
});
it('should return an error when call the endpoint with the invalid "command" param', async () => {
const res = await request
.post(api('commands.run'))
.set(credentials)
.send({
command: 'invalid-command',
params: 'params',
roomId: 'GENERAL',
})
.expect(400);
expect(res.body).to.have.property('success', false);
expect(res.body.error).to.be.equal('The command provided does not exist (or is disabled).');
});
it('should return an error when call the endpoint with an invalid thread id', async () => {
const res = await request
.post(api('commands.run'))
.set(credentials)
.send({
command: 'tableflip',
params: 'params',
roomId: 'GENERAL',
tmid: 'invalid-thread',
})
.expect(400);
expect(res.body).to.have.property('success', false);
expect(res.body.error).to.be.equal('Invalid thread.');
});
it('should return an error when call the endpoint with a valid thread id of wrong channel', async () => {
const res = await request
.post(api('commands.run'))
.set(credentials)
.send({
command: 'tableflip',
params: 'params',
roomId: 'GENERAL',
tmid: threadMessage.tmid,
})
.expect(400);
expect(res.body).to.have.property('success', false);
expect(res.body.error).to.be.equal('Invalid thread.');
});
it('should return success when parameters are correct', async () => {
const res = await request
.post(api('commands.run'))
.set(credentials)
.send({
command: 'tableflip',
params: 'params',
roomId: threadMessage.rid,
tmid: threadMessage.tmid,
})
.expect(200);
expect(res.body).to.have.property('success', true);
});
});
describe('Command archive', function () {
describe('unauthorized cases', () => {
let user: TestUser<IUser>;
let credentials: Credentials;
this.beforeAll(async () => {
user = await createUser({
joinDefaultChannels: true,
});
credentials = await login(user.username, password);
});
this.afterAll(async () => {
await deleteUser(user);
});
it('should return an error when the user is not logged in', async () => {
await request
.post(api('commands.run'))
.send({ command: 'archive', roomId: 'GENERAL' })
.expect(401)
.expect((res) => {
expect(res.body).to.have.property('status', 'error');
});
});
it('should return an error when the user has not enough permissions', async () => {
await request
.post(api('commands.run'))
.set(credentials)
.send({
command: 'archive',
roomId: 'GENERAL',
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'error-not-authorized');
});
});
});
describe('authorized cases', function () {
this.afterAll(async () => {
await request
.post(api('commands.run'))
.set(credentials)
.send({
command: 'unarchive',
roomId: 'GENERAL',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
});
});
it('should return a success when the user has enough permissions', async () => {
await request
.post(api('commands.run'))
.set(credentials)
.send({
command: 'archive',
roomId: 'GENERAL',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
});
});
});
});
describe('Command unarchive', function () {
describe('unauthorized cases', () => {
let user: TestUser<IUser>;
let credentials: Credentials;
this.beforeAll(async () => {
user = await createUser({
joinDefaultChannels: true,
});
credentials = await login(user.username, password);
});
this.afterAll(async () => {
await deleteUser(user);
});
it('should return an error when the user is not logged in', async () => {
await request
.post(api('commands.run'))
.send({ command: 'unarchive', roomId: 'GENERAL' })
.expect(401)
.expect((res) => {
expect(res.body).to.have.property('status', 'error');
});
});
it('should return an error when the user has not enough permissions', async () => {
await request
.post(api('commands.run'))
.set(credentials)
.send({
command: 'unarchive',
roomId: 'GENERAL',
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'error-not-authorized');
});
});
});
describe('authorized cases', () => {
before(async () => {
await request
.post(api('commands.run'))
.set(credentials)
.send({
command: 'archive',
roomId: 'GENERAL',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
});
});
it('should return a success when the user has enough permissions', async () => {
await request
.post(api('commands.run'))
.set(credentials)
.send({
command: 'unarchive',
roomId: 'GENERAL',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
});
});
});
});
describe('Command "invite-all-from"', function () {
let group: IRoom;
let group1: IRoom;
let channel: IRoom;
let user1: TestUser<IUser>;
let user2: TestUser<IUser>;
let user1Credentials: Credentials;
let user2Credentials: Credentials;
this.beforeAll(async () => {
user1 = await createUser();
user2 = await createUser();
[user1Credentials, user2Credentials] = await Promise.all([login(user1.username, password), login(user2.username, password)]);
});
this.beforeAll(async () => {
const [response1, response2, response3] = await Promise.all([
createRoom({ type: 'p', name: `room1-${Date.now()}.${Random.id()}`, credentials: user1Credentials }),
createRoom({ type: 'c', name: `room2-${Date.now()}.${Random.id()}`, credentials: user2Credentials }),
createRoom({ type: 'p', name: `room3-${Date.now()}.${Random.id()}` }),
]);
group = response1.body.group;
channel = response2.body.channel;
group1 = response3.body.group;
});
this.afterAll(async () => {
await Promise.all([
deleteRoom({ type: 'p', roomId: group._id }),
deleteRoom({ type: 'c', roomId: channel._id }),
deleteRoom({ type: 'p', roomId: group1._id }),
]);
await Promise.all([deleteUser(user1), deleteUser(user2)]);
});
it('should not add users from group which is not accessible by current user', async () => {
await request
.post(api('commands.run'))
.set(user2Credentials)
.send({
roomId: channel._id,
command: 'invite-all-from',
params: `#${group.name}`,
triggerId: Random.id(),
})
.expect(200)
.expect(async (res) => {
expect(res.body).to.have.a.property('success', true);
});
await request
.get(api('channels.members'))
.query({ roomId: channel._id })
.set(user2Credentials)
.expect(200)
.expect((res) => {
const isUser1Added = res.body.members.some((member: IUser) => member.username === user1.username);
expect(isUser1Added).to.be.false;
});
});
it('should not add users to a room that is not accessible by the current user', async () => {
await request
.post(api('commands.run'))
.set(user1Credentials)
.send({
roomId: group1._id,
command: 'invite-all-from',
params: `#${group.name}`,
triggerId: Random.id(),
})
.expect(403)
.expect((res) => {
expect(res.body).to.have.a.property('error', 'unauthorized');
});
});
it('should add users from group which is accessible by current user', async () => {
await request
.post(api('groups.invite'))
.set(user1Credentials)
.send({
roomId: group._id,
userId: user2._id,
})
.expect(200);
await request
.post(api('commands.run'))
.set(user2Credentials)
.send({
roomId: channel._id,
command: 'invite-all-from',
params: `#${group.name}`,
triggerId: Random.id(),
})
.expect(200)
.expect(async (res) => {
expect(res.body).to.have.a.property('success', true);
});
await request
.get(api('channels.members'))
.set(user2Credentials)
.query({ roomId: channel._id })
.expect(200)
.expect((res) => {
const isUser1Added = res.body.members.some((member: IUser) => member.username === user1.username);
expect(isUser1Added).to.be.true;
});
});
});
describe('Command "kick"', function () {
let directMessageRoom: IRoom;
let user1: TestUser<IUser>;
let user1Credentials: Credentials;
this.beforeAll(async () => {
user1 = await createUser();
[user1Credentials] = await Promise.all([login(user1.username, password)]);
});
this.beforeAll(async () => {
const [response1] = await Promise.all([
createRoom({ type: 'd', name: `room1-${Date.now()}.${Random.id()}`, username: user1.username }),
]);
directMessageRoom = response1.body.room;
});
this.afterAll(async () => {
await Promise.all([deleteRoom({ type: 'd', roomId: directMessageRoom._id })]);
await Promise.all([deleteUser(user1)]);
});
it('should fail when trying to kick a user from a direct message room', async () => {
const res = await request
.post(api('commands.run'))
.set(user1Credentials)
.send({ command: 'kick', roomId: directMessageRoom._id, params: user1.username })
.expect(400);
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error').that.is.a('string');
});
});
describe('Command "leave"', function () {
let directMessageRoom: IRoom;
let user1: TestUser<IUser>;
let user2: TestUser<IUser>;
let user1Credentials: Credentials;
this.beforeAll(async () => {
user1 = await createUser();
user2 = await createUser();
[user1Credentials] = await Promise.all([login(user1.username, password)]);
});
this.beforeAll(async () => {
const [response1] = await Promise.all([
createRoom({ type: 'd', name: `room1-${Date.now()}.${Random.id()}`, username: user1.username }),
]);
directMessageRoom = response1.body.room;
});
this.afterAll(async () => {
await Promise.all([deleteRoom({ type: 'd', roomId: directMessageRoom._id })]);
await Promise.all([deleteUser(user1), deleteUser(user2)]);
});
it('should fail when trying to leave a direct message room', async () => {
const res = await request
.post(api('commands.run'))
.set(user1Credentials)
.send({ command: 'leave', roomId: directMessageRoom._id })
.expect(400);
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error').that.is.a('string');
});
});
// TODO: invite never fails
describe.skip('Command "invite"', function () {
let directMessageRoom: IRoom;
let user1: TestUser<IUser>;
let user2: TestUser<IUser>;
let user1Credentials: Credentials;
this.beforeAll(async () => {
user1 = await createUser();
user2 = await createUser();
[user1Credentials] = await Promise.all([login(user1.username, password)]);
});
this.beforeAll(async () => {
const [response1] = await Promise.all([
createRoom({ type: 'd', name: `room1-${Date.now()}.${Random.id()}`, username: user1.username }),
]);
directMessageRoom = response1.body.room;
});
this.afterAll(async () => {
await Promise.all([deleteRoom({ type: 'd', roomId: directMessageRoom._id })]);
await Promise.all([deleteUser(user1), deleteUser(user2)]);
});
it('should fail when trying to invite a user to a direct message room', async () => {
const res = await request
.post(api('commands.run'))
.set(user1Credentials)
.send({ command: 'invite', roomId: directMessageRoom._id, params: 'g1' })
.expect(403);
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error').that.is.a('string');
expect(res.body.error).to.equal('unauthorized');
});
});
});

View File

@@ -0,0 +1,72 @@
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import { getCredentials, request } from '../../data/api-data';
import { updateSetting } from '../../data/permissions.helper';
const getHash = () =>
request
.get('/')
.expect(200)
.expect('Content-Type', 'text/html; charset=utf-8')
.then((res) => {
const hashMatch = res.text.match(/meteor_runtime_config\.js\?hash=([^"']+)/);
expect(hashMatch).to.not.be.null;
const hash = hashMatch![1];
return hash;
});
describe('[CORS]', () => {
before((done) => getCredentials(done));
after(async () => {
await updateSetting('Site_Url', 'http://localhost:3000');
});
describe('[/meteor_runtime_config.js]', () => {
it('should return 404 when hash is missing', async () => {
await request.get('/meteor_runtime_config.js').expect(404);
});
it('should return 404 when hash is invalid', async () => {
await request.get('/meteor_runtime_config.js').query({ hash: 'invalid_hash' }).expect(404);
});
it('should return runtime config when hash matches', async () => {
// First get the root page to extract the hash
const hash = await getHash();
// Now request with the extracted hash
await request
.get('/meteor_runtime_config.js')
.query({ hash })
.expect(200)
.expect('Content-Type', 'application/javascript; charset=UTF-8')
.expect((res) => {
expect(res.text).to.include('__meteor_runtime_config__');
expect(res.text).to.include('http://localhost:3000');
});
});
it('should return 404 when hash does not match', async () => {
// First get the root page to extract the hash
await request.get('/meteor_runtime_config.js').query({ hash: 'invalidHash' }).expect(404);
});
it('should update hash when ROOT_URL changes', async () => {
const originalHash = getHash();
await updateSetting('Site_Url', 'http://new-url:3000');
const newHash = await getHash();
expect(newHash).to.not.equal(originalHash);
// it should return if the new hash is valid
await request
.get('/meteor_runtime_config.js')
.query({ hash: newHash })
.expect(200)
.expect('Content-Type', 'application/javascript; charset=UTF-8')
.expect((res) => {
expect(res.text).to.include('__meteor_runtime_config__');
expect(res.text).to.include('http://new-url:3000');
});
});
});
});

View File

@@ -0,0 +1,747 @@
import { randomUUID } from 'crypto';
import fs from 'fs';
import path from 'path';
import type { Credentials } from '@rocket.chat/api-client';
import type { IUser } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { before, describe, it, after } from 'mocha';
import { getCredentials, api, request, credentials } from '../../data/api-data';
import { updateSetting } from '../../data/permissions.helper';
import { password } from '../../data/user';
import { createUser, deleteUser, login } from '../../data/users.helper';
async function createCustomSound(fileName: string, filePath: string): Promise<string> {
let fileId = '';
await request
.post(api('custom-sounds.create'))
.set(credentials)
.attach('sound', filePath)
.field('name', fileName)
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('sound');
fileId = res.body.sound._id;
});
return fileId;
}
async function deleteCustomSound(_id: string) {
await request
.post(api('custom-sounds.delete'))
.set(credentials)
.send({ _id })
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
});
}
describe('[CustomSounds]', () => {
const fileName = `test-file-${randomUUID()}`;
const mockWavAudioPath = path.resolve(__dirname, '../../mocks/files/audio_mock.wav');
const mockMp3AudioPath = path.resolve(__dirname, '../../mocks/files/audio_mock.mp3');
let fileId: string;
let fileId2: string;
let uploadDate: string | undefined;
before((done) => getCredentials(done));
before(async () => {
fileId = await createCustomSound(fileName, mockWavAudioPath);
fileId2 = await createCustomSound(`${fileName}-2`, mockWavAudioPath);
});
after(async () => {
if (fileId) {
await deleteCustomSound(fileId);
}
if (fileId2) {
await deleteCustomSound(fileId2);
}
});
describe('[/custom-sounds.create]', () => {
let fileId3: string;
after(async () => {
if (fileId3) {
await deleteCustomSound(fileId3);
}
});
it('should successfully create a new custom sound and return its _id', async () => {
const response = await request
.post(api('custom-sounds.create'))
.set(credentials)
.attach('sound', mockWavAudioPath)
.field('name', `happy-path-sound-${randomUUID()}`)
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('sound').and.to.be.an('object');
expect(res.body.sound).to.have.property('_id').and.to.be.a('string');
});
fileId3 = response.body.sound._id;
});
it('should not be able to create two sounds with the same name', async () => {
await request
.post(api('custom-sounds.create'))
.set(credentials)
.attach('sound', mockWavAudioPath)
.field('name', fileName)
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'The custom sound name is already in use [Custom_Sound_Error_Name_Already_In_Use]');
});
});
it('should return unauthorized if not authenticated', async () => {
await request.post(api('custom-sounds.create')).expect(401);
});
it('should fail if the file exceeds the 5MB size limit', async () => {
const largeBuffer = Buffer.alloc(6 * 1024 * 1024, 'a');
await request
.post(api('custom-sounds.create'))
.set(credentials)
.attach('sound', largeBuffer, { filename: 'large.wav', contentType: 'audio/wav' })
.field('name', 'large-sound')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('error').and.to.be.equal('[error-file-too-large]');
});
});
it('should fail if the file is an invalid mime type', async () => {
await request
.post(api('custom-sounds.create'))
.set(credentials)
.attach('sound', Buffer.from('this is not audio'), { filename: 'test.txt', contentType: 'text/plain' })
.field('name', 'invalid-sound')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body.error).to.equal('MIME type not allowed');
});
});
it('should reject injection of invalid characters and symbols in name', async () => {
await request
.post(api('custom-sounds.create'))
.set(credentials)
.attach('sound', mockWavAudioPath)
.field('name', '<script>alert("xss")</script>')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body.error).to.include('is not a valid name');
});
});
it('should reject NoSQL regex injection in name', async () => {
await request
.post(api('custom-sounds.create'))
.set(credentials)
.attach('sound', mockWavAudioPath)
.field('name', '{"$regex":".*"}')
.expect(400);
});
describe('without manage-sounds permission', () => {
let unauthorizedUser: IUser;
let unauthorizedUserCredentials: Credentials;
before(async () => {
unauthorizedUser = await createUser();
unauthorizedUserCredentials = await login(unauthorizedUser.username, password);
});
after(async () => {
await deleteUser(unauthorizedUser);
});
it('should return forbidden if user does not have the manage-sounds permission', async () => {
await request
.post(api('custom-sounds.create'))
.set(unauthorizedUserCredentials)
.attach('sound', mockWavAudioPath)
.field('name', `forbidden-sound-${randomUUID()}`)
.expect(403);
});
});
});
describe('[/custom-sounds.update]', () => {
let previousFileName: string;
before(async () => {
await request
.get(api('custom-sounds.getOne'))
.set(credentials)
.query({ _id: fileId })
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('sound').and.to.be.an('object');
previousFileName = res.body.sound.name;
});
});
after(async () => {
await request
.post(api('custom-sounds.update'))
.set(credentials)
.field('_id', fileId)
.field('name', previousFileName)
.attach('sound', mockWavAudioPath)
.expect(200);
});
it('should successfully update only the name of the sound without sending the file again', async () => {
const newSoundName = `${fileName}-updated`;
await request
.post(api('custom-sounds.update'))
.set(credentials)
.field('_id', fileId)
.field('name', newSoundName)
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
});
await request
.get(api('custom-sounds.getOne'))
.set(credentials)
.query({ _id: fileId })
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('sound').and.to.be.an('object');
expect(res.body.sound).to.have.property('name').and.to.be.equal(newSoundName);
});
});
it('should successfully update the sound file and name', async () => {
const newSoundName = `${fileName}-2-updated`;
await request
.post(api('custom-sounds.update'))
.set(credentials)
.attach('sound', mockMp3AudioPath)
.field('_id', fileId)
.field('name', newSoundName)
.expect(200);
await request
.get(api('custom-sounds.getOne'))
.set(credentials)
.query({ _id: fileId })
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('sound').and.to.be.an('object');
expect(res.body.sound).to.have.property('name').and.to.be.equal(newSoundName);
expect(res.body.sound).to.have.property('extension').and.to.be.equal('mp3');
});
});
it('should not be able to update sounds name if the name was already taken', async () => {
await request
.post(api('custom-sounds.update'))
.set(credentials)
.attach('sound', mockWavAudioPath)
.field('_id', fileId)
.field('name', `${fileName}-2`)
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'The custom sound name is already in use [Custom_Sound_Error_Name_Already_In_Use]');
});
});
it('should return unauthorized if not authenticated', async () => {
await request.post(api('custom-sounds.update')).expect(401);
});
it('should reject injection of invalid characters and symbols in name', async () => {
await request
.post(api('custom-sounds.update'))
.set(credentials)
.attach('sound', mockWavAudioPath)
.field('_id', fileId)
.field('name', '<script>alert("xss")</script>')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body.error).to.include('is not a valid name');
});
});
it('should reject NoSQL regex injection in name', async () => {
await request
.post(api('custom-sounds.update'))
.set(credentials)
.attach('sound', mockWavAudioPath)
.field('_id', fileId)
.field('name', '{"$regex":".*"}')
.expect(400);
});
it('should fail if the file exceeds the 5MB size limit', async () => {
const largeBuffer = Buffer.alloc(6 * 1024 * 1024, 'a');
await request
.post(api('custom-sounds.update'))
.set(credentials)
.attach('sound', largeBuffer, { filename: 'large.wav', contentType: 'audio/wav' })
.field('_id', fileId)
.field('name', 'large-sound')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('error').and.to.be.equal('[error-file-too-large]');
});
});
it('should return an error when trying to update a non-existent sound', async () => {
await request
.post(api('custom-sounds.update'))
.set(credentials)
.field('_id', 'invalid-id-123')
.field('name', 'new-name')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body.error).to.equal('Custom Sound not found.');
});
});
it('should fail if attempting to update with an invalid mime type file', async () => {
await request
.post(api('custom-sounds.update'))
.set(credentials)
.attach('sound', Buffer.from('fake-audio'), { filename: 'test.mp4', contentType: 'video/mp4' })
.field('_id', fileId)
.field('name', fileName)
.expect(400)
.expect((res) => {
expect(res.body.error).to.equal('MIME type not allowed');
});
});
describe('without manage-sounds permission', async () => {
let unauthorizedUser: IUser;
let unauthorizedUserCredentials: Credentials;
before(async () => {
unauthorizedUser = await createUser();
unauthorizedUserCredentials = await login(unauthorizedUser.username, password);
});
after(async () => {
await deleteUser(unauthorizedUser);
});
it('should return forbidden if user does not have the manage-sounds permission', async () => {
await request
.post(api('custom-sounds.update'))
.set(unauthorizedUserCredentials)
.attach('sound', mockWavAudioPath)
.field('_id', fileId)
.field('name', `forbidden-case`)
.expect(403);
});
});
describe('mutating file on failed validation', () => {
let soundAId: string;
let soundBId: string;
const soundAName = `sound-a-${randomUUID()}`;
const soundBName = `sound-b-${randomUUID()}`;
before(async () => {
soundAId = await createCustomSound(soundAName, mockWavAudioPath);
soundBId = await createCustomSound(soundBName, mockMp3AudioPath);
});
after(async () => {
await deleteCustomSound(soundAId);
await deleteCustomSound(soundBId);
});
it('should not mutate the underlying file or metadata if the update fails validation (e.g., name collision)', async () => {
await request
.post(api('custom-sounds.update'))
.set(credentials)
.attach('sound', mockMp3AudioPath)
.field('_id', soundAId)
.field('name', soundBName)
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'The custom sound name is already in use [Custom_Sound_Error_Name_Already_In_Use]');
});
await request
.get(api('custom-sounds.getOne'))
.set(credentials)
.query({ _id: soundAId })
.expect(200)
.expect((res) => {
expect(res.body.sound).to.have.property('name', soundAName);
expect(res.body.sound).to.have.property('extension', 'wav');
});
const originalWavBuffer = fs.readFileSync(mockWavAudioPath);
await request
.get(`/custom-sounds/${soundAId}.wav`)
.set(credentials)
.expect(200)
.expect((res) => {
expect(res.headers).to.have.property('content-type', 'audio/wav');
expect(Buffer.isBuffer(res.body)).to.be.true;
expect(originalWavBuffer.equals(res.body)).to.be.true;
});
await request.get(`/custom-sounds/${soundAId}.mp3`).set(credentials).expect(404);
});
});
});
describe('[/custom-sounds.list]', () => {
it('should return custom sounds', async () => {
const res = await request.get(api('custom-sounds.list')).set(credentials).expect(200);
expect(res.body).to.have.property('sounds').and.to.be.an('array');
expect(res.body).to.have.property('total');
expect(res.body).to.have.property('offset');
expect(res.body).to.have.property('count');
});
it('should return custom sounds even requested with count and offset params', async () => {
const res = await request.get(api('custom-sounds.list')).set(credentials).expect(200).query({
count: 5,
offset: 0,
});
expect(res.body).to.have.property('sounds').and.to.be.an('array');
expect(res.body).to.have.property('total');
expect(res.body).to.have.property('offset');
expect(res.body).to.have.property('count');
});
it('should return custom sounds filtering it using the `name` parameter', async () => {
const res = await request
.get(api('custom-sounds.list'))
.set(credentials)
.expect(200)
.query({
name: `${fileName}-2`,
count: 5,
offset: 0,
});
expect(res.body).to.have.property('sounds').and.to.be.an('array');
expect(res.body).to.have.property('total').to.equal(1);
expect(res.body).to.have.property('offset').to.equal(0);
expect(res.body).to.have.property('count').to.equal(1);
expect(res.body.sounds[0]._id).to.be.equal(fileId2);
});
});
describe('[/custom-sounds.delete]', () => {
let soundToDeleteId: string;
let soundDeleted: boolean = false;
before(async () => {
soundToDeleteId = await createCustomSound(`sound-to-delete-${randomUUID()}`, mockWavAudioPath);
});
after(async () => {
if (soundToDeleteId && !soundDeleted) {
await deleteCustomSound(soundToDeleteId);
}
});
it('should return unauthorized if the user is not authenticated', async () => {
await request.post(api('custom-sounds.delete')).send({ _id: soundToDeleteId }).expect(401);
});
it('should return a 400 if attempting to delete a sound that does not exist', async () => {
await request
.post(api('custom-sounds.delete'))
.set(credentials)
.send({ _id: 'invalid-non-existent-id' })
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body.error).to.equal('Custom_Sound_Error_Invalid_Sound');
});
});
it('should reject requests with invalid parameter types', async () => {
await request
.post(api('custom-sounds.delete'))
.set(credentials)
.send({ _id: { $ne: null } })
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
});
});
describe('without manage-sounds permission', async () => {
let unauthorizedUser: IUser;
let unauthorizedUserCredentials: Credentials;
before(async () => {
unauthorizedUser = await createUser();
unauthorizedUserCredentials = await login(unauthorizedUser.username, password);
});
after(async () => {
await deleteUser(unauthorizedUser);
});
it('should return forbidden if user does not have the manage-sounds permission', async () => {
await request.post(api('custom-sounds.delete')).set(unauthorizedUserCredentials).send({ _id: soundToDeleteId }).expect(403);
});
});
it('should successfully delete a custom sound when providing a valid _id', async () => {
await request
.post(api('custom-sounds.delete'))
.set(credentials)
.send({ _id: soundToDeleteId })
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
});
soundDeleted = true;
});
});
describe('Accessing custom sounds', () => {
it('should return forbidden if the there is no fileId on the url', async () => {
const res = await request.get('/custom-sounds/').set(credentials).expect(403);
expect(res.text).to.be.equal('Forbidden');
});
it('should return not found if the the requested file does not exists', async () => {
const res = await request.get('/custom-sounds/invalid.mp3').set(credentials).expect(404);
expect(res.text).to.be.equal('Not found');
});
it('should return success if the the requested exists', async () => {
const res = await request.get(`/custom-sounds/${fileId}.wav`).set(credentials).expect(200);
expect(res.headers).to.have.property('last-modified');
expect(res.headers).to.have.property('content-type', 'audio/wav');
expect(res.headers).to.have.property('cache-control', 'public, max-age=0');
expect(res.headers).to.have.property('expires', '-1');
uploadDate = res.headers['last-modified'];
});
it('should return not modified if the the requested file contains a valid-since equal to the upload date', async () => {
const res = await request
.get(`/custom-sounds/${fileId}.wav`)
.set(credentials)
.set({
'if-modified-since': uploadDate,
})
.expect(304);
expect(res.headers).to.have.property('last-modified', uploadDate);
expect(res.headers).not.to.have.property('content-type');
expect(res.headers).not.to.have.property('cache-control');
expect(res.headers).not.to.have.property('expires');
});
});
describe('[/custom-sounds.delete]', () => {
let toDeleteFileId: string;
before(async () => {
toDeleteFileId = await createCustomSound(`${fileName}-delete-${randomUUID()}`, mockWavAudioPath);
});
it('should return unauthorized if not authenticated', async () => {
await request.post(api('custom-sounds.delete')).send({ _id: toDeleteFileId }).expect(401);
});
it('should return bad request when _id is missing', async () => {
await request.post(api('custom-sounds.delete')).set(credentials).send({}).expect(400);
});
it('should return bad request when _id is an empty string', async () => {
await request.post(api('custom-sounds.delete')).set(credentials).send({ _id: '' }).expect(400);
});
it('should fail when the user does not have manage-sounds permission', async () => {
const testUser = await createUser();
const testUserCredentials = await login(testUser.username, password);
try {
await request.post(api('custom-sounds.delete')).set(testUserCredentials).send({ _id: toDeleteFileId }).expect(403);
} finally {
await deleteUser(testUser);
}
});
it('should delete the sound successfully and remove it from the list', async () => {
await request
.post(api('custom-sounds.delete'))
.set(credentials)
.send({ _id: toDeleteFileId })
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
});
await request.get(api('custom-sounds.getOne')).set(credentials).query({ _id: toDeleteFileId }).expect(404);
});
it('should fail when the sound id does not exist', async () => {
await request
.post(api('custom-sounds.delete'))
.set(credentials)
.send({ _id: 'non-existent-sound-id' })
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
});
});
});
describe('[/custom-sounds.getOne]', () => {
it('should return unauthorized if not authenticated', async () => {
await request.get(api('custom-sounds.getOne')).query({ _id: fileId }).expect(401);
});
it('should return not found if custom sound does not exist', async () => {
await request.get(api('custom-sounds.getOne')).set(credentials).query({ _id: 'invalid-id' }).expect(404);
});
it('should return bad request if the _id length is not more than one', async () => {
await request.get(api('custom-sounds.getOne')).set(credentials).query({ _id: '' }).expect(400);
});
it('should return the custom sound successfully', async () => {
await request
.get(api('custom-sounds.getOne'))
.set(credentials)
.query({ _id: fileId })
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('sound').and.to.be.an('object');
expect(res.body.sound).to.have.property('_id', fileId);
expect(res.body.sound).to.have.property('name').and.to.be.a('string');
expect(res.body.sound).to.have.property('extension').and.to.be.a('string');
});
});
it('should reject regex injection via query object', async () => {
await request
.get(api('custom-sounds.getOne'))
.set(credentials)
.query({
_id: { $regex: '.*' },
})
.expect(400);
});
it('should reject regex injection via bracket syntax', async () => {
await request.get(api('custom-sounds.getOne')).set(credentials).query('_id[$regex]=.*').expect(400);
});
it('should reject encoded regex injection attempt', async () => {
await request
.get(api('custom-sounds.getOne'))
.set(credentials)
.query({
_id: '{"$regex":".*"}',
})
.expect(404); // valid string, but it doesn't exist
});
});
describe('Sounds storage settings reactivity', () => {
let fsFileId: string;
let gridFsFileId: string;
before(async () => {
await updateSetting('CustomSounds_FileSystemPath', '', false);
await updateSetting('CustomSounds_Storage_Type', 'FileSystem');
fsFileId = await createCustomSound(`${fileName}-3`, mockWavAudioPath);
await updateSetting('CustomSounds_Storage_Type', 'GridFS');
gridFsFileId = await createCustomSound(`${fileName}-4`, mockWavAudioPath);
});
after(async () => {
await updateSetting('CustomSounds_Storage_Type', 'FileSystem', false);
await updateSetting('CustomSounds_FileSystemPath', '');
await deleteCustomSound(fsFileId);
await updateSetting('CustomSounds_Storage_Type', 'GridFS');
await deleteCustomSound(gridFsFileId);
});
describe('CustomSounds_Storage_Type', () => {
describe('when storage is GridFS', () => {
before(async () => {
await updateSetting('CustomSounds_Storage_Type', 'GridFS');
});
it('should resolve GridFS files only', async () => {
await request.get(`/custom-sounds/${gridFsFileId}.wav`).set(credentials).expect(200);
await request.get(`/custom-sounds/${fsFileId}.wav`).set(credentials).expect(404);
});
});
describe('when storage is FileSystem', () => {
before(async () => {
await updateSetting('CustomSounds_Storage_Type', 'FileSystem');
});
it('should resolve FileSystem files only', async () => {
await request.get(`/custom-sounds/${gridFsFileId}.wav`).set(credentials).expect(404);
await request.get(`/custom-sounds/${fsFileId}.wav`).set(credentials).expect(200);
});
});
});
describe('CustomSounds_FileSystemPath', () => {
before(async () => {
await updateSetting('CustomSounds_Storage_Type', 'FileSystem');
});
describe('when file system path is the default one', () => {
it('should resolve files', async () => {
await request.get(`/custom-sounds/${fsFileId}.wav`).set(credentials).expect(200);
});
});
describe('when file system path is NOT the default one', () => {
before(async () => {
await updateSetting('CustomSounds_FileSystemPath', '~/sounds');
});
after(async () => {
await updateSetting('CustomSounds_FileSystemPath', '');
});
it('should NOT resolve files', async () => {
await request.get(`/custom-sounds/${fsFileId}.wav`).set(credentials).expect(404);
});
});
});
});
});

View File

@@ -0,0 +1,382 @@
import { expect } from 'chai';
import { after, afterEach, before, describe, it } from 'mocha';
import type { Response } from 'supertest';
import { getCredentials, api, request, credentials } from '../../data/api-data';
import { updatePermission } from '../../data/permissions.helper';
import { password } from '../../data/user';
import { createUser, deleteUser, login } from '../../data/users.helper';
async function createCustomUserStatus(name: string, statusType?: string): Promise<string> {
const res = await request.post(api('custom-user-status.create')).set(credentials).send({ name, statusType }).expect(200);
return res.body.customUserStatus._id;
}
async function deleteCustomUserStatus(id: string): Promise<void> {
await request.post(api('custom-user-status.delete')).set(credentials).send({ customUserStatusId: id }).expect(200);
}
describe('[CustomUserStatus]', () => {
let unauthorizedUser: any;
let unauthorizedUserCredentials: any;
before((done) => {
getCredentials(done);
});
before(async () => {
unauthorizedUser = await createUser();
unauthorizedUserCredentials = await login(unauthorizedUser.username, password);
});
after(() => Promise.all([updatePermission('manage-user-status', ['admin']), deleteUser(unauthorizedUser)]));
describe('[/custom-user-status.list]', () => {
let customUserStatusId: string;
let customUserStatusName: string;
before(async () => {
customUserStatusName = `test-${Date.now()}`;
customUserStatusId = await createCustomUserStatus(customUserStatusName);
});
after(async () => {
await deleteCustomUserStatus(customUserStatusId);
});
it('should return custom user status', async () => {
const res = await request.get(api('custom-user-status.list')).set(credentials).expect(200);
expect(res.body).to.have.property('statuses').and.to.be.an('array');
expect(res.body).to.have.property('total');
expect(res.body).to.have.property('offset');
expect(res.body).to.have.property('count');
});
it('should return custom user status even requested with count and offset params', async () => {
const res = await request.get(api('custom-user-status.list')).set(credentials).expect(200).query({
count: 5,
offset: 0,
});
expect(res.body).to.have.property('statuses').and.to.be.an('array');
expect(res.body).to.have.property('total');
expect(res.body).to.have.property('offset');
expect(res.body).to.have.property('count');
});
it('should return one custom user status when requested with id param', async () => {
const res = await request.get(api('custom-user-status.list')).set(credentials).expect(200).query({
_id: customUserStatusId,
});
expect(res.body).to.have.property('statuses').and.to.be.an('array').and.to.have.lengthOf(1);
expect(res.body).to.have.property('total').and.to.equal(1);
expect(res.body).to.have.property('offset').and.to.equal(0);
expect(res.body).to.have.property('count').and.to.equal(1);
});
it('should return empty array when requested with an existing name param', async () => {
const res = await request.get(api('custom-user-status.list')).set(credentials).expect(200).query({
name: customUserStatusName,
});
expect(res.body).to.have.property('statuses').and.to.be.an('array').and.to.have.lengthOf(1);
expect(res.body).to.have.property('total').and.to.equal(1);
expect(res.body).to.have.property('offset').and.to.equal(0);
expect(res.body).to.have.property('count').and.to.equal(1);
});
it('should return empty array when requested with unknown name param', async () => {
const res = await request.get(api('custom-user-status.list')).set(credentials).expect(200).query({
name: 'testxxx',
});
expect(res.body).to.have.property('statuses').and.to.be.an('array').and.to.have.lengthOf(0);
expect(res.body).to.have.property('total').and.to.equal(0);
expect(res.body).to.have.property('offset').and.to.equal(0);
expect(res.body).to.have.property('count').and.to.equal(0);
});
});
describe('[/custom-user-status.create]', () => {
let customUserStatusId: string;
afterEach(async () => {
await updatePermission('manage-user-status', ['admin']);
if (customUserStatusId) {
await deleteCustomUserStatus(customUserStatusId);
customUserStatusId = '';
}
});
it('should throw an error if not authenticated', async () => {
await request
.post(api('custom-user-status.create'))
.send({ name: 'test-status' })
.expect('Content-Type', 'application/json')
.expect(401)
.expect((res: Response) => {
expect(res.body).to.have.property('status', 'error');
});
});
it('should throw an error if user does not have manage-user-status permission', async () => {
await updatePermission('manage-user-status', []);
await request
.post(api('custom-user-status.create'))
.set(unauthorizedUserCredentials)
.send({ name: 'test-status' })
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'not_authorized');
});
});
it('should create a custom user status successfully', async () => {
const statusName = `test-create-${Date.now()}`;
await request
.post(api('custom-user-status.create'))
.set(credentials)
.send({ name: statusName, statusType: 'busy' })
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('customUserStatus');
expect(res.body.customUserStatus).to.have.property('_id');
expect(res.body.customUserStatus).to.have.property('name', statusName);
expect(res.body.customUserStatus).to.have.property('statusType', 'busy');
customUserStatusId = res.body.customUserStatus._id;
});
});
it('should throw an error if name already exists', async () => {
const statusName = `test-duplicate-${Date.now()}`;
customUserStatusId = await createCustomUserStatus(statusName);
await request
.post(api('custom-user-status.create'))
.set(credentials)
.send({ name: statusName })
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'Custom_User_Status_Error_Name_Already_In_Use');
});
});
it('should throw an error if statusType is invalid', async () => {
await request
.post(api('custom-user-status.create'))
.set(credentials)
.send({ name: `test-invalid-status-type-${Date.now()}`, statusType: 'invalid' })
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'error-input-is-not-a-valid-field');
});
});
});
describe('[/custom-user-status.update]', () => {
let customUserStatusId: string;
let customUserStatusName: string;
before(async () => {
customUserStatusName = `test-update-${Date.now()}`;
customUserStatusId = await createCustomUserStatus(customUserStatusName);
});
afterEach(async () => {
await updatePermission('manage-user-status', ['admin']);
});
after(async () => {
if (customUserStatusId) {
await deleteCustomUserStatus(customUserStatusId);
}
await updatePermission('manage-user-status', ['admin']);
});
it('should throw an error if not authenticated', async () => {
await request
.post(api('custom-user-status.update'))
.send({ _id: customUserStatusId, name: 'updated-name' })
.expect('Content-Type', 'application/json')
.expect(401)
.expect((res: Response) => {
expect(res.body).to.have.property('status', 'error');
});
});
it('should throw an error if user does not have manage-user-status permission', async () => {
await updatePermission('manage-user-status', []);
await request
.post(api('custom-user-status.update'))
.set(unauthorizedUserCredentials)
.send({ _id: customUserStatusId, name: 'updated-name' })
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'not_authorized');
});
});
it('should throw an error if custom user status does not exist', async () => {
await request
.post(api('custom-user-status.update'))
.set(credentials)
.send({ _id: 'invalid-id', name: 'updated-name' })
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
});
});
it('should update custom user status successfully', async () => {
const newName = `test-updated-${Date.now()}`;
await request
.post(api('custom-user-status.update'))
.set(credentials)
.send({ _id: customUserStatusId, name: newName, statusType: 'away' })
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('customUserStatus');
expect(res.body.customUserStatus).to.have.property('_id', customUserStatusId);
expect(res.body.customUserStatus).to.have.property('name', newName);
expect(res.body.customUserStatus).to.have.property('statusType', 'away');
});
customUserStatusName = newName;
});
it('should throw an error if status name already exists', async () => {
const existingStatusName = `test-update-duplicate-${Date.now()}`;
const existingStatusId = await createCustomUserStatus(existingStatusName);
await request
.post(api('custom-user-status.update'))
.set(credentials)
.send({ _id: customUserStatusId, name: existingStatusName })
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'Custom_User_Status_Error_Name_Already_In_Use');
});
await deleteCustomUserStatus(existingStatusId);
});
it('should throw an error if statusType is invalid', async () => {
await request
.post(api('custom-user-status.update'))
.set(credentials)
.send({ _id: customUserStatusId, name: customUserStatusName, statusType: 'invalid' })
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'error-input-is-not-a-valid-field');
});
});
});
describe('[/custom-user-status.delete]', () => {
let customUserStatusId: string;
beforeEach(async () => {
const statusName = `test-delete-${Date.now()}`;
customUserStatusId = await createCustomUserStatus(statusName);
});
afterEach(async () => {
await updatePermission('manage-user-status', ['admin']);
if (customUserStatusId) {
await deleteCustomUserStatus(customUserStatusId);
customUserStatusId = '';
}
});
it('should throw an error if not authenticated', async () => {
await request
.post(api('custom-user-status.delete'))
.send({ customUserStatusId })
.expect('Content-Type', 'application/json')
.expect(401)
.expect((res: Response) => {
expect(res.body).to.have.property('status', 'error');
});
});
it('should throw an error if user does not have manage-user-status permission', async () => {
await updatePermission('manage-user-status', []);
await request
.post(api('custom-user-status.delete'))
.set(unauthorizedUserCredentials)
.send({ customUserStatusId })
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'not_authorized');
});
});
it('should throw an error if customUserStatusId is not provided', async () => {
await request
.post(api('custom-user-status.delete'))
.set(credentials)
.send({})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', "must have required property 'customUserStatusId'");
});
});
it('should throw an error if custom user status does not exist', async () => {
await request
.post(api('custom-user-status.delete'))
.set(credentials)
.send({ customUserStatusId: 'invalid-id' })
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'Custom_User_Status_Error_Invalid_User_Status');
});
});
it('should delete custom user status successfully', async () => {
await request
.post(api('custom-user-status.delete'))
.set(credentials)
.send({ customUserStatusId })
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
});
customUserStatusId = '';
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,539 @@
import type { IEmojiCustom } from '@rocket.chat/core-typings';
import { assert, expect } from 'chai';
import { before, describe, it, after } from 'mocha';
import { getCredentials, api, request, credentials } from '../../data/api-data';
import { imgURL } from '../../data/interactions';
import { updateSetting } from '../../data/permissions.helper';
describe('[EmojiCustom]', () => {
const customEmojiName = `my-custom-emoji-${Date.now()}`;
let withoutAliases: IEmojiCustom;
before((done) => getCredentials(done));
after(() =>
request.post(api('emoji-custom.delete')).set(credentials).send({
emojiId: withoutAliases._id,
}),
);
describe('[/emoji-custom.create]', () => {
it('should create new custom emoji', async () => {
const res = await request
.post(api('emoji-custom.create'))
.set(credentials)
.attach('emoji', imgURL)
.field({
name: customEmojiName,
aliases: `${customEmojiName}-alias`,
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(res.body).to.have.property('success', true);
});
it('should create new custom emoji without optional parameter "aliases"', async () => {
const res = await request
.post(api('emoji-custom.create'))
.set(credentials)
.attach('emoji', imgURL)
.field({
name: `${customEmojiName}-without-aliases`,
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(res.body).to.have.property('success', true);
});
it('should throw an error when the filename is wrong', async () => {
const res = await request
.post(api('emoji-custom.create'))
.set(credentials)
.attach('emojiwrong', imgURL)
.field({
_id: 'invalid-id',
name: 'my-custom-emoji-without-aliases',
})
.expect('Content-Type', 'application/json')
.expect(400);
expect(res.body).to.have.property('success', false);
expect(res.body.errorType).to.be.equal('invalid-field');
});
});
let createdCustomEmoji: IEmojiCustom;
describe('[/emoji-custom.update]', () => {
before((done) => {
void request
.get(api('emoji-custom.list'))
.set(credentials)
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('emojis').and.to.be.a('object');
expect(res.body.emojis).to.have.property('update').and.to.be.a('array').and.to.not.have.lengthOf(0);
expect(res.body.emojis).to.have.property('remove').and.to.be.a('array').and.to.have.lengthOf(0);
const _createdCustomEmoji = (res.body.emojis.update as IEmojiCustom[]).find((emoji) => emoji.name === customEmojiName);
const _withoutAliases = (res.body.emojis.update as IEmojiCustom[]).find(
(emoji) => emoji.name === `${customEmojiName}-without-aliases`,
);
assert.isDefined(_createdCustomEmoji);
assert.isDefined(_withoutAliases);
createdCustomEmoji = _createdCustomEmoji;
withoutAliases = _withoutAliases;
})
.end(done);
});
describe('successfully:', () => {
it('should update the custom emoji without a file', async () => {
const res = await request
.post(api('emoji-custom.update'))
.set(credentials)
.field({
_id: createdCustomEmoji._id,
name: customEmojiName,
aliases: 'alias-my-custom-emoji',
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(res.body).to.have.property('success', true);
});
it('should update the custom emoji without optional parameter "aliases"', async () => {
const res = await request
.post(api('emoji-custom.update'))
.set(credentials)
.field({
_id: createdCustomEmoji._id,
name: customEmojiName,
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(res.body).to.have.property('success', true);
});
it('should update the custom emoji with all parameters and with a file', async () => {
const res = await request
.post(api('emoji-custom.update'))
.set(credentials)
.attach('emoji', imgURL)
.field({
_id: createdCustomEmoji._id,
name: customEmojiName,
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(res.body).to.have.property('success', true);
});
it('should change the etag when the custom emoji image is updated', async () => {
const prevEtag = createdCustomEmoji.etag;
await request
.post(api('emoji-custom.update'))
.set(credentials)
.attach('emoji', imgURL)
.field({
_id: createdCustomEmoji._id,
name: customEmojiName,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
});
const emojis = await request.get(api(`emoji-custom.all`)).set(credentials).expect(200);
const updatedCustomEmoji = emojis.body.emojis.find((emoji: IEmojiCustom) => emoji._id === createdCustomEmoji._id);
expect(updatedCustomEmoji.etag).not.to.be.equal(prevEtag);
});
});
describe('should throw error when:', () => {
it('the fields does not include "_id"', async () => {
const res = await request
.post(api('emoji-custom.update'))
.set(credentials)
.attach('emoji', imgURL)
.field({
name: 'my-custom-emoji-without-aliases',
})
.expect('Content-Type', 'application/json')
.expect(400);
expect(res.body).to.have.property('success', false);
expect(res.body.errorType).to.be.equal('The required "_id" query param is missing.');
});
it('the custom emoji does not exists', async () => {
const res = await request
.post(api('emoji-custom.update'))
.set(credentials)
.attach('emoji', imgURL)
.field({
_id: 'invalid-id',
name: 'my-custom-emoji-without-aliases',
})
.expect('Content-Type', 'application/json')
.expect(400);
expect(res.body).to.have.property('success', false);
expect(res.body.errorType).to.be.equal('Emoji not found.');
});
it('the emoji file field is wrong', async () => {
const res = await request
.post(api('emoji-custom.update'))
.set(credentials)
.attach('emojiwrong', imgURL)
.field({
_id: createdCustomEmoji._id,
name: 'my-custom-emoji-without-aliases',
})
.expect('Content-Type', 'application/json')
.expect(400);
expect(res.body).to.have.property('success', false);
expect(res.body.errorType).to.be.equal('invalid-field');
});
});
});
describe('[/emoji-custom.list]', () => {
it('should return emojis', async () => {
const res = await request.get(api('emoji-custom.list')).set(credentials).expect(200);
expect(res.body).to.have.property('emojis').and.to.be.a('object');
expect(res.body.emojis).to.have.property('update').and.to.be.a('array').and.to.not.have.lengthOf(0);
expect(res.body.emojis).to.have.property('remove').and.to.be.a('array').and.to.have.lengthOf(0);
});
it('should return emojis when use "query" query parameter', async () => {
const res = await request.get(api('emoji-custom.list')).query({ _updatedAt: new Date().toISOString() }).set(credentials).expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('emojis').and.to.be.a('object');
expect(res.body.emojis).to.have.property('update').and.to.be.a('array').and.to.have.lengthOf(0);
expect(res.body.emojis).to.have.property('remove').and.to.be.a('array').and.to.have.lengthOf(0);
});
it('should return emojis when use "updateSince" query parameter', async () => {
const res = await request
.get(api('emoji-custom.list'))
.query({ updatedSince: new Date().toISOString() })
.set(credentials)
.expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('emojis').and.to.be.a('object');
expect(res.body.emojis).to.have.property('update').and.to.be.a('array').and.to.have.lengthOf(0);
expect(res.body.emojis).to.have.property('remove').and.to.be.a('array').and.to.have.lengthOf(0);
});
it('should return emojis when use both, "updateSince" and "query" query parameter', async () => {
const res = await request
.get(api('emoji-custom.list'))
.query({ _updatedAt: new Date().toISOString(), updatedSince: new Date().toISOString() })
.set(credentials)
.expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('emojis').and.to.be.a('object');
expect(res.body.emojis).to.have.property('update').and.to.be.a('array').and.to.have.lengthOf(0);
expect(res.body.emojis).to.have.property('remove').and.to.be.a('array').and.to.have.lengthOf(0);
});
it('should return an error when the "updateSince" query parameter is a invalid date', async () => {
const res = await request.get(api('emoji-custom.list')).query({ updatedSince: 'invalid-date' }).set(credentials).expect(400);
expect(res.body).to.have.property('success', false);
expect(res.body.errorType).to.be.equal('error-roomId-param-invalid');
});
});
describe('[/emoji-custom.all]', () => {
it('should return emojis', async () => {
const res = await request.get(api('emoji-custom.all')).set(credentials).expect(200);
expect(res.body).to.have.property('emojis').and.to.be.an('array');
expect(res.body).to.have.property('total');
expect(res.body).to.have.property('offset');
expect(res.body).to.have.property('count');
});
it('should return emojis even requested with count and offset params', async () => {
const res = await request
.get(api('emoji-custom.all'))
.set(credentials)
.query({
count: 5,
offset: 0,
})
.expect(200);
expect(res.body).to.have.property('emojis').and.to.be.an('array');
expect(res.body).to.have.property('total');
expect(res.body).to.have.property('offset');
expect(res.body).to.have.property('count');
});
it('should return only filtered by name emojis', async () => {
const res = await request
.get(api('emoji-custom.all'))
.set(credentials)
.query({
name: `${customEmojiName}-without-aliases`,
})
.expect(200);
expect(res.body).to.have.property('emojis').and.to.be.an('array').and.to.have.lengthOf(1);
expect(res.body).to.have.property('total');
expect(res.body).to.have.property('offset');
expect(res.body).to.have.property('count');
});
});
describe('Accessing custom emojis', () => {
let uploadDate: string | undefined;
it('should return forbidden if the there is no fileId on the url', async () => {
const res = await request.get('/emoji-custom/').set(credentials).expect(403);
expect(res.text).to.be.equal('Forbidden');
});
it('should return success if the file does not exists with some specific headers', async () => {
const res = await request.get('/emoji-custom/invalid').set(credentials).expect(200);
expect(res.headers).to.have.property('last-modified', 'Thu, 01 Jan 2015 00:00:00 GMT');
expect(res.headers).to.have.property('content-type', 'image/svg+xml');
expect(res.headers).to.have.property('cache-control', 'public, max-age=0');
expect(res.headers).to.have.property('expires', '-1');
expect(res.headers).to.have.property('content-disposition', 'inline');
});
it('should return not modified if the file does not exists and if-modified-since is equal to the Thu, 01 Jan 2015 00:00:00 GMT', async () => {
const res = await request
.get('/emoji-custom/invalid')
.set(credentials)
.set({
'if-modified-since': 'Thu, 01 Jan 2015 00:00:00 GMT',
})
.expect(304);
expect(res.headers).to.have.property('last-modified', 'Thu, 01 Jan 2015 00:00:00 GMT');
});
it('should return success if the the requested exists', async () => {
const res = await request.get(`/emoji-custom/${customEmojiName}.png`).set(credentials).expect(200);
expect(res.headers).to.have.property('last-modified');
expect(res.headers).to.have.property('content-type', 'image/png');
expect(res.headers).to.have.property('cache-control', 'public, max-age=31536000');
expect(res.headers).to.have.property('content-disposition', 'inline');
uploadDate = res.headers['last-modified'];
});
it('should return not modified if the the requested file contains a valid-since equal to the upload date', async () => {
const res = await request
.get(`/emoji-custom/${customEmojiName}.png`)
.set(credentials)
.set({
'if-modified-since': uploadDate,
})
.expect(304);
expect(res.headers).to.have.property('last-modified', uploadDate);
expect(res.headers).not.to.have.property('content-type');
expect(res.headers).not.to.have.property('content-length');
expect(res.headers).not.to.have.property('cache-control');
});
it('should return the emoji even when no etag is passed (for old emojis)', async () => {
const res = await request.get(`/emoji-custom/${createdCustomEmoji.name}.png`).set(credentials).expect(200);
expect(res.headers).to.have.property('content-type', 'image/png');
expect(res.headers).to.have.property('cache-control', 'public, max-age=31536000');
expect(res.headers).to.have.property('content-disposition', 'inline');
});
it('should return success if the etag is invalid', async () => {
const res = await request
.get(`/emoji-custom/${createdCustomEmoji.name}.png?etag=1234`)
.set(credentials)
.set({
'if-none-match': 'invalid-etag',
})
.expect(200);
expect(res.headers).to.have.property('content-type', 'image/png');
expect(res.headers).to.have.property('cache-control', 'public, max-age=31536000');
expect(res.headers).to.have.property('content-disposition', 'inline');
});
});
describe('[/emoji-custom.delete]', () => {
it('should throw an error when trying delete custom emoji without the required param "emojid"', async () => {
const res = await request
.post(api('emoji-custom.delete'))
.set(credentials)
.send({})
.expect('Content-Type', 'application/json')
.expect(400);
expect(res.body).to.have.property('success', false);
expect(res.body.error).to.be.equal("must have required property 'emojiId'");
});
it('should throw an error when trying delete custom emoji that does not exists', async () => {
const res = await request
.post(api('emoji-custom.delete'))
.set(credentials)
.send({
emojiId: 'invalid-id',
})
.expect('Content-Type', 'application/json')
.expect(400);
expect(res.body).to.have.property('success', false);
expect(res.body.errorType).to.be.equal('Custom_Emoji_Error_Invalid_Emoji');
});
it('should delete the custom emoji created before successfully', async () => {
const res = await request
.post(api('emoji-custom.delete'))
.set(credentials)
.send({
emojiId: createdCustomEmoji._id,
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(res.body).to.have.property('success', true);
});
});
describe('Emoji storage settings reactivity', () => {
const normalizeSvg = (svg: string) => svg.replace(/\r\n/g, '\n').trim();
const now = Date.now();
const fsEmojiName = `emoji-fs-${now}`;
const gridFsEmojiName = `emoji-gridfs-${now}`;
const svgFallback = normalizeSvg(`<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" pointer-events="none" width="50" height="50" style="width: 50px; height: 50px; background-color: #000;">
<text text-anchor="middle" y="50%" x="50%" dy="0.36em" pointer-events="auto" fill="#ffffff" font-family="Helvetica, Arial, Lucida Grande, sans-serif" style="font-weight: 400; font-size: 28px;">
?
</text>
</svg>`);
before(async () => {
await updateSetting('EmojiUpload_FileSystemPath', '', false);
await updateSetting('EmojiUpload_Storage_Type', 'FileSystem');
await request.post(api('emoji-custom.create')).set(credentials).attach('emoji', imgURL).field({ name: fsEmojiName }).expect(200);
await updateSetting('EmojiUpload_Storage_Type', 'GridFS');
await request.post(api('emoji-custom.create')).set(credentials).attach('emoji', imgURL).field({ name: gridFsEmojiName }).expect(200);
});
after(async () => {
const list = await request.get(api('emoji-custom.all')).set(credentials);
const fsEmoji = list.body.emojis.find((e: IEmojiCustom) => e.name === fsEmojiName);
const gridEmoji = list.body.emojis.find((e: IEmojiCustom) => e.name === gridFsEmojiName);
await updateSetting('EmojiUpload_Storage_Type', 'FileSystem', false);
await updateSetting('EmojiUpload_FileSystemPath', '');
if (fsEmoji) {
await request.post(api('emoji-custom.delete')).set(credentials).send({ emojiId: fsEmoji._id });
}
await updateSetting('EmojiUpload_Storage_Type', 'GridFS');
if (gridEmoji) {
await request.post(api('emoji-custom.delete')).set(credentials).send({ emojiId: gridEmoji._id });
}
});
describe('EmojiUpload_Storage_Type', () => {
describe('when storage is GridFs', () => {
before(async () => {
await updateSetting('EmojiUpload_Storage_Type', 'GridFS');
});
it('should resolve GridFS files only', async () => {
await request
.get(`/emoji-custom/${fsEmojiName}.png`)
.set(credentials)
.expect(200)
.expect((res) => {
const received = normalizeSvg(res.body.toString());
expect(received).to.equal(svgFallback);
});
await request
.get(`/emoji-custom/${gridFsEmojiName}.png`)
.set(credentials)
.expect(200)
.expect((res) => expect(res.headers).to.have.property('content-type', 'image/png'));
});
});
describe('when storage is FileSystem', () => {
before(async () => {
await updateSetting('EmojiUpload_Storage_Type', 'FileSystem');
});
it('should resolve FileSystem files only', async () => {
await request
.get(`/emoji-custom/${fsEmojiName}.png`)
.set(credentials)
.expect(200)
.expect((res) => expect(res.headers).to.have.property('content-type', 'image/png'));
await request
.get(`/emoji-custom/${gridFsEmojiName}.png`)
.set(credentials)
.expect(200)
.expect((res) => {
const received = normalizeSvg(res.body.toString());
expect(received).to.equal(svgFallback);
});
});
});
});
describe('EmojiUpload_FileSystemPath', () => {
before(async () => {
await updateSetting('EmojiUpload_Storage_Type', 'FileSystem');
});
describe('when file system path is the default one', () => {
before(async () => {
await updateSetting('EmojiUpload_FileSystemPath', '');
});
it('should resolve files', async () => {
await request
.get(`/emoji-custom/${fsEmojiName}.png`)
.set(credentials)
.expect(200)
.expect((res) => expect(res.headers).to.have.property('content-type', 'image/png'));
});
});
describe('when file system path is NOT the default one', () => {
before(async () => {
await updateSetting('EmojiUpload_FileSystemPath', '~/emoji-test');
});
after(async () => {
await updateSetting('CustomSounds_FileSystemPath', '');
});
it('should NOT resolve files', async () => {
await request
.get(`/emoji-custom/${fsEmojiName}.png`)
.set(credentials)
.expect(200)
.expect((res) => {
const received = normalizeSvg(res.body.toString());
expect(received).to.equal(svgFallback);
});
});
});
});
});
});

View File

@@ -0,0 +1,217 @@
import type { IUser } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { after, before, beforeEach, afterEach, describe, it } from 'mocha';
import { sleep } from '../../../lib/utils/sleep';
import { getCredentials, api, request, credentials } from '../../data/api-data';
import { updateSetting, updatePermission } from '../../data/permissions.helper';
import { password } from '../../data/user';
import type { TestUser } from '../../data/users.helper';
import { createUser, deleteUser } from '../../data/users.helper';
describe('[Failed Login Attempts]', () => {
const maxAttemptsByUser = 2;
const maxAttemptsByIp = 4;
const userBlockSeconds = 3;
const ipBlockSeconds = 8;
before((done) => getCredentials(done));
before(() =>
Promise.all([
updateSetting('Block_Multiple_Failed_Logins_Enabled', true),
updateSetting('Block_Multiple_Failed_Logins_By_Ip', true),
updateSetting('Block_Multiple_Failed_Logins_By_User', true),
updateSetting('Block_Multiple_Failed_Logins_Attempts_Until_Block_by_User', maxAttemptsByUser),
updateSetting('Block_Multiple_Failed_Logins_Time_To_Unblock_By_User_In_Minutes', userBlockSeconds / 60),
updateSetting('Block_Multiple_Failed_Logins_Attempts_Until_Block_By_Ip', maxAttemptsByIp),
updateSetting('Block_Multiple_Failed_Logins_Time_To_Unblock_By_Ip_In_Minutes', ipBlockSeconds / 60),
updatePermission('logout-other-user', ['admin']),
]),
);
after(() =>
Promise.all([
updateSetting('Block_Multiple_Failed_Logins_Attempts_Until_Block_by_User', 10),
updateSetting('Block_Multiple_Failed_Logins_Time_To_Unblock_By_User_In_Minutes', 5),
updateSetting('Block_Multiple_Failed_Logins_Attempts_Until_Block_By_Ip', 50),
updateSetting('Block_Multiple_Failed_Logins_Time_To_Unblock_By_Ip_In_Minutes', 5),
updateSetting('Block_Multiple_Failed_Logins_Enabled', true),
updateSetting('Block_Multiple_Failed_Logins_By_Ip', true),
updateSetting('Block_Multiple_Failed_Logins_By_User', true),
updatePermission('logout-other-user', ['admin']),
]),
);
async function shouldFailLoginWithUser(username: string, password: string) {
await request
.post(api('login'))
.send({
user: username,
password,
})
.expect('Content-Type', 'application/json')
.expect(401)
.expect((res) => {
expect(res.body).to.have.property('status', 'error');
expect(res.body).to.have.property('message', 'Unauthorized');
});
}
async function shouldSuccesfullyLoginWithUser(username: string, password: string) {
await request
.post(api('login'))
.send({
user: username,
password,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('status', 'success');
expect(res.body).to.have.property('data').and.to.be.an('object');
expect(res.body.data).to.have.property('userId');
expect(res.body.data).to.have.property('authToken');
});
}
async function shouldLogoutUser(uid: string) {
await request.post(api('users.logout')).set(credentials).send({ userId: uid }).expect('Content-Type', 'application/json').expect(200);
}
async function shouldBlockLogin(username: string, password: string, reason: 'user' | 'ip') {
await request
.post(api('login'))
.send({
user: username,
password,
})
.expect('Content-Type', 'application/json')
.expect(401)
.expect((res) => {
expect(res.body).to.have.property('status', 'error');
expect(res.body).to.have.property('error', `error-login-blocked-for-${reason}`);
});
}
async function failMaxAttempts(username: string, password: string) {
const promises = [];
for (let i = 0; i < maxAttemptsByUser; i++) {
promises.push(shouldFailLoginWithUser(username, password));
}
await Promise.all(promises);
}
describe('[Block by User]', () => {
let user: TestUser<IUser>;
before(async () => {
await updateSetting('Block_Multiple_Failed_Logins_By_Ip', false);
});
after(async () => {
await updateSetting('Block_Multiple_Failed_Logins_By_Ip', true);
});
beforeEach(async () => {
user = await createUser();
});
afterEach(async () => {
await deleteUser(user);
});
it('should block by User when the limit amount of failed attempts is reached', async () => {
await failMaxAttempts(user.username, `${password}-incorrect`);
await shouldBlockLogin(user.username, password, 'user');
});
it('should unblock user after block time', async () => {
await failMaxAttempts(user.username, `${password}-incorrect`);
await shouldBlockLogin(user.username, password, 'user');
await sleep(userBlockSeconds * 1000);
await shouldSuccesfullyLoginWithUser(user.username, password);
});
it('should reset counter of failed attempts after a successful login', async () => {
await failMaxAttempts(user.username, `${password}-incorrect`);
await shouldBlockLogin(user.username, password, 'user');
await sleep(userBlockSeconds * 1000);
await shouldFailLoginWithUser(user.username, `${password}-incorrect`);
await shouldSuccesfullyLoginWithUser(user.username, password);
await shouldLogoutUser(user._id);
await failMaxAttempts(user.username, `${password}-incorrect`);
});
it('should count failed attempts by user', async () => {
const newUser = await createUser();
await failMaxAttempts(user.username, `${password}-incorrect`);
await shouldFailLoginWithUser(newUser.username, `${password}-incorrect`);
await shouldSuccesfullyLoginWithUser(newUser.username, password);
await deleteUser(newUser);
});
});
describe('[Block by IP]', () => {
let user: TestUser<IUser>;
let user2: TestUser<IUser>;
let userLogin: TestUser<IUser>;
beforeEach(async () => {
user = await createUser();
user2 = await createUser();
userLogin = await createUser();
});
afterEach(() => Promise.all([deleteUser(user), deleteUser(user2), deleteUser(userLogin)]));
afterEach(async () => {
// reset counter
await sleep(ipBlockSeconds * 1000);
});
it('should block by IP when trying to login with one user and the limit amount of failed attempts is reached', async () => {
await failMaxAttempts(user.username, `${password}-incorrect`);
await sleep(userBlockSeconds * 1000);
await failMaxAttempts(user.username, `${password}-incorrect`);
await shouldBlockLogin(user.username, password, 'ip');
});
it('should block by IP when trying to login with multiple users and the limit amount of failed attempts is reached', async () => {
await failMaxAttempts(user.username, `${password}-incorrect`);
await failMaxAttempts(user2.username, `${password}-incorrect`);
await shouldBlockLogin(userLogin.username, password, 'ip');
});
it('should unblock IP after block time', async () => {
await failMaxAttempts(user.username, `${password}-incorrect`);
await failMaxAttempts(user2.username, `${password}-incorrect`);
await shouldBlockLogin(userLogin.username, password, 'ip');
await sleep(ipBlockSeconds * 1000);
await shouldSuccesfullyLoginWithUser(userLogin.username, password);
}).timeout(20000);
it('should reset counter of failed attempts after a successful login', async () => {
await failMaxAttempts(user.username, `${password}-incorrect`);
await failMaxAttempts(user2.username, `${password}-incorrect`);
await sleep(ipBlockSeconds * 1000);
await shouldFailLoginWithUser(userLogin.username, `${password}-incorrect`);
await shouldSuccesfullyLoginWithUser(userLogin.username, password);
await shouldLogoutUser(userLogin._id);
await failMaxAttempts(user.username, `${password}-incorrect`);
await failMaxAttempts(user2.username, `${password}-incorrect`);
await shouldBlockLogin(userLogin.username, password, 'ip');
}).timeout(20000);
});
});

View File

@@ -0,0 +1,49 @@
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import { getCredentials, request } from '../../data/api-data';
import { updateSetting } from '../../data/permissions.helper';
import { IS_EE } from '../../e2e/config/constants';
(IS_EE ? describe : describe.skip)('federation', () => {
before((done) => getCredentials(done));
describe('well-known', () => {
describe('when matrix disabled', () => {
before(async () => {
await updateSetting('Federation_Service_Enabled', false, false);
await updateSetting('Federation_Service_Domain', 'localhost', false);
});
after(async () => {
await updateSetting('Federation_Service_Enabled', false, false);
});
it('should return 404 not found', async () => {
await request.get('/.well-known/matrix/server').expect(403);
});
});
describe('when enabled', () => {
before(async () => {
await updateSetting('Federation_Service_Enabled', true);
await updateSetting('Federation_Service_Domain', 'rc.host');
});
after(async () => {
await updateSetting('Federation_Service_Enabled', false);
await updateSetting('Federation_Service_Domain', '');
});
it('should return matrix information', async () => {
await request
.get('/.well-known/matrix/server')
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('m.server', 'rc.host:443');
});
});
});
});
});

View File

@@ -0,0 +1,145 @@
import path from 'path';
import type { Credentials } from '@rocket.chat/api-client';
import type { ImageAttachmentProps, IRoom, IUser, SettingValue } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import sharp from 'sharp';
import { getCredentials, request } from '../../data/api-data';
import { uploadFileToRC } from '../../data/file.helper';
import { getSettingValueById, updateSetting } from '../../data/permissions.helper';
import { createRoom, deleteRoom } from '../../data/rooms.helper';
import type { IRequestConfig, TestUser } from '../../data/users.helper';
import { createUser, deleteUser, login } from '../../data/users.helper';
const testImagePath = path.join(__dirname, '../../mocks/files/exif-orientation-6.jpg');
const testImageName = 'exif-orientation-6.jpg';
const downloadBuffer = async (url: string, auth: Credentials): Promise<Buffer> => {
const response = await request.get(url).set(auth).buffer(true).expect(200);
return response.body as Buffer;
};
describe('[File Upload - Image Rotation]', () => {
before((done) => getCredentials(done));
let user: TestUser<IUser>;
const userPassword = `pass${Date.now()}`;
let userCredentials: Credentials;
let testRoom: IRoom;
let rotateImagesSetting: SettingValue;
let stripExifSetting: SettingValue;
let thumbnailsEnabledSetting: SettingValue;
let thumbnailsWidthSetting: SettingValue;
let thumbnailsHeightSetting: SettingValue;
before(async () => {
user = await createUser({ joinDefaultChannels: false, password: userPassword });
userCredentials = await login(user.username, userPassword);
testRoom = (await createRoom({ type: 'p', name: `rotate-upload-${Date.now()}`, members: [user.username] })).body.group;
rotateImagesSetting = await getSettingValueById('FileUpload_RotateImages');
stripExifSetting = await getSettingValueById('Message_Attachments_Strip_Exif');
thumbnailsEnabledSetting = await getSettingValueById('Message_Attachments_Thumbnails_Enabled');
thumbnailsWidthSetting = await getSettingValueById('Message_Attachments_Thumbnails_Width');
thumbnailsHeightSetting = await getSettingValueById('Message_Attachments_Thumbnails_Height');
await updateSetting('FileUpload_RotateImages', true, false);
await updateSetting('Message_Attachments_Strip_Exif', true, false);
await updateSetting('Message_Attachments_Thumbnails_Enabled', true);
});
after(async () => {
await Promise.all([
updateSetting('FileUpload_RotateImages', rotateImagesSetting),
updateSetting('Message_Attachments_Strip_Exif', stripExifSetting),
updateSetting('Message_Attachments_Thumbnails_Enabled', thumbnailsEnabledSetting),
deleteRoom({ type: 'p', roomId: testRoom._id }),
deleteUser(user),
]);
});
it('should rotate pixels and strip EXIF orientation', async () => {
const fixtureMetadata = await sharp(testImagePath).metadata();
expect(fixtureMetadata.width).to.equal(719);
expect(fixtureMetadata.height).to.equal(479);
expect(fixtureMetadata.orientation).to.equal(6);
const requestConfig: IRequestConfig = { request, credentials: userCredentials };
const { message } = await uploadFileToRC(testRoom._id, testImagePath, 'rotation-exif-test', requestConfig);
expect(message).to.have.property('attachments');
const attachment = message.attachments?.find((item) => item.title === testImageName);
expect(attachment).to.be.an('object');
const fileUrl = (attachment as { title_link?: string }).title_link;
const thumbUrl = (attachment as ImageAttachmentProps).image_url;
expect(fileUrl).to.be.a('string');
expect(thumbUrl).to.be.a('string');
const originalBuffer = await downloadBuffer(fileUrl as string, userCredentials);
const originalMetadata = await sharp(originalBuffer).metadata();
expect(originalMetadata.width).to.equal(479);
expect(originalMetadata.height).to.equal(719);
expect(originalMetadata.exif).to.be.undefined;
});
it('should generate thumbnail from rotated image', async () => {
const requestConfig: IRequestConfig = { request, credentials: userCredentials };
const { message } = await uploadFileToRC(testRoom._id, testImagePath, 'rotation-thumb-test', requestConfig);
expect(message).to.have.property('attachments');
const attachment = message.attachments?.find((item) => item.title === testImageName);
expect(attachment).to.be.an('object');
const fileUrl = (attachment as { title_link?: string }).title_link;
const thumbUrl = (attachment as ImageAttachmentProps).image_url;
expect(fileUrl).to.be.a('string');
expect(thumbUrl).to.be.a('string');
const originalBuffer = await downloadBuffer(fileUrl as string, userCredentials);
const originalMetadata = await sharp(originalBuffer).metadata();
const thumbBuffer = await downloadBuffer(thumbUrl as string, userCredentials);
const thumbMetadata = await sharp(thumbBuffer).metadata();
const thumbWidthSetting = Number(thumbnailsWidthSetting);
const thumbHeightSetting = Number(thumbnailsHeightSetting);
const originalWidth = originalMetadata.width as number;
const originalHeight = originalMetadata.height as number;
const resizeFactor = Math.min(thumbWidthSetting / originalWidth, thumbHeightSetting / originalHeight);
expect(thumbMetadata.width).to.be.closeTo(Math.round(originalWidth * resizeFactor), 1);
expect(thumbMetadata.height).to.be.closeTo(Math.round(originalHeight * resizeFactor), 1);
});
describe('when FileUpload_RotateImages is disabled', () => {
before(async () => {
await updateSetting('FileUpload_RotateImages', false);
});
after(async () => {
await updateSetting('FileUpload_RotateImages', rotateImagesSetting);
});
it('should NOT rotate pixels', async () => {
const requestConfig: IRequestConfig = { request, credentials: userCredentials };
const { message } = await uploadFileToRC(testRoom._id, testImagePath, 'no-rotation-test', requestConfig);
expect(message).to.have.property('attachments');
const attachment = message.attachments?.find((item) => item.title === testImageName);
expect(attachment).to.be.an('object');
const fileUrl = (attachment as { title_link?: string }).title_link;
expect(fileUrl).to.be.a('string');
const originalBuffer = await downloadBuffer(fileUrl as string, userCredentials);
const originalMetadata = await sharp(originalBuffer).metadata();
expect(originalMetadata.width).to.equal(719);
expect(originalMetadata.height).to.equal(479);
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,118 @@
import { expect } from 'chai';
import { before, describe, it, after } from 'mocha';
import { getCredentials, api, request, credentials, methodCall } from '../../data/api-data';
import { restorePermissionToRoles } from '../../data/permissions.helper';
import { IS_EE } from '../../e2e/config/constants';
(IS_EE ? describe : describe.skip)('[Guest Permissions]', () => {
const guestPermissions = ['view-d-room', 'view-joined-room', 'view-p-room', 'start-discussion', 'mobile-upload-file'];
before((done) => getCredentials(done));
after(() => Promise.all(guestPermissions.map((permissionName) => restorePermissionToRoles(permissionName))));
function succeedRemoveGuestPermission(permissionName: string) {
it(`should allow removing the whitelisted permission ${permissionName} from the guest role`, async () => {
const res = await request
.post(methodCall('authorization:removeRoleFromPermission'))
.set(credentials)
.send({
message: JSON.stringify({
method: 'authorization:removeRoleFromPermission',
params: [permissionName, 'guest'],
id: 'id',
msg: 'method',
}),
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('message');
const message = JSON.parse(res.body.message);
expect(message).to.not.have.property('error');
const permissionsListRes = await request
.get(api('permissions.listAll'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(permissionsListRes.body).to.have.property('success', true);
expect(permissionsListRes.body).to.have.property('update').and.to.be.an('array');
expect(permissionsListRes.body).to.have.property('remove').and.to.be.an('array');
const updatedPermission = permissionsListRes.body.update.find((permission: any) => permission._id === permissionName);
expect(updatedPermission).to.have.property('_id', permissionName);
expect(updatedPermission).to.have.property('roles').and.to.be.an('array').that.does.not.include('guest');
});
}
function succeedAddGuestPermission(permissionName: string) {
it(`should allow granting the whitelisted permission ${permissionName} to the guest role`, async () => {
const res = await request
.post(methodCall('authorization:addPermissionToRole'))
.set(credentials)
.send({
message: JSON.stringify({
method: 'authorization:addPermissionToRole',
params: [permissionName, 'guest'],
id: 'id',
msg: 'method',
}),
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('message');
const message = JSON.parse(res.body.message);
expect(message).to.not.have.property('error');
const permissionsListRes = await request
.get(api('permissions.listAll'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(permissionsListRes.body).to.have.property('success', true);
expect(permissionsListRes.body).to.have.property('update').and.to.be.an('array');
expect(permissionsListRes.body).to.have.property('remove').and.to.be.an('array');
const updatedPermission = permissionsListRes.body.update.find((permission: any) => permission._id === permissionName);
expect(updatedPermission).to.have.property('_id', permissionName);
expect(updatedPermission).to.have.property('roles').and.to.be.an('array').that.includes('guest');
});
}
describe('Default guest roles', () => {
guestPermissions.forEach((permissionName) => {
succeedRemoveGuestPermission(permissionName);
});
guestPermissions.forEach((permissionName) => {
succeedAddGuestPermission(permissionName);
});
it('should not allow adding a non whitelisted permission to the guest role', async () => {
const res = await request
.post(methodCall('authorization:addPermissionToRole'))
.set(credentials)
.send({
message: JSON.stringify({
method: 'authorization:addPermissionToRole',
params: ['create-c', 'guest'],
id: 'id',
msg: 'method',
}),
})
.expect('Content-Type', 'application/json')
.expect(400);
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('message');
const message = JSON.parse(res.body.message);
expect(message).to.have.property('error');
expect(message.error).to.have.property('reason', 'Permission is restricted');
});
});
});

View File

@@ -0,0 +1,31 @@
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
/**
* Retry a given function N times until it succeeds.
* It should not be as workaround for eventual consistency, but in cases api calls have to be retried because of async tasks, it must be intentionally used.
*
* @param {Function} fn - Function that performs the assert/check.
* @param {number} retries - Number of retries.
* @param {number} delayMs - Delay between retries (ms).
*/
export async function retry(_description: string, fn: () => Promise<void> | void, options: { retries?: number; delayMs?: number } = {}) {
const { retries = 3, delayMs = 10 } = options;
let lastError;
await delay(delayMs);
for (let i = 0; i <= retries; i++) {
try {
const result = fn();
if (result instanceof Promise) {
await result;
}
return;
} catch (err) {
lastError = err;
if (i < retries && delayMs > 0) {
await delay(delayMs);
}
}
}
throw lastError;
}

View File

@@ -0,0 +1,86 @@
import { expect } from 'chai';
import { describe, it } from 'mocha';
import { apiUrl } from '../../data/api-data';
// Regression test for the dynamic-import response truncation flake (PR #41399).
//
// Through traefik with `serverstransport.maxidleconnsperhost=-1` (upstream
// keep-alive disabled), Go's transport rarely closes its upstream connection
// while the ReverseProxy is still copying a chunked gzip response body
// ("use of closed network connection"), truncating the response mid-stream:
// the browser gets 200 headers and then ERR_INCOMPLETE_CHUNKED_ENCODING, and
// the meteor module loader hangs the page. Rate is ~1.6e-4 per request under
// load, so this test replays the exact request shape (real dynamic-import
// bodies captured from a failing CI run — the fatal one is the same
// AppLayoutThemeWrapper.tsx request seen in every reproduction) at high
// volume and concurrency: red on the broken proxy config, green once
// upstream keep-alive is restored.
//
// The bodies are build-dependent fixtures: if the app tree renames these
// modules the responses shrink below the gzip threshold and lose the
// chunked+gzip shape the race needs, so the test asserts that shape first.
const FETCH_URL = `${apiUrl}/__meteor__/dynamic-import/fetch`;
const BODIES = [
'{"client":{"components":{"AppLayoutThemeWrapper.tsx":1}}}',
'{"client":{"meteor":{"login":{"index.ts":1,"cas.ts":1,"crowd.ts":1,"facebook.ts":1,"oauth.ts":1,"LoginCancelledError.ts":1,"google.ts":1,"ldap.ts":1,"meteorDeveloperAccount.ts":1,"password.ts":1,"saml.ts":1,"twitter.ts":1}},"lib":{"2fa":{"overrideLoginMethod.ts":1},"wrapRequestCredentialFn.ts":1,"loginServices.ts":1}},"node_modules":{"@rocket.chat":{"string-helpers":{"package.json":1,"dist":{"esm":{"index.js":1}}}}}}',
];
const TOTAL_REQUESTS = 30000;
const CONCURRENCY = 64;
const fetchModuleTree = async (body: string): Promise<{ error?: string }> => {
try {
const response = await fetch(FETCH_URL, {
method: 'POST',
headers: { 'Content-Type': 'text/plain;charset=UTF-8' },
body,
});
if (response.status !== 200) {
return { error: `unexpected status ${response.status}` };
}
await response.text();
return {};
} catch (error) {
return { error: `${error} (cause: ${(error as { cause?: unknown }).cause})` };
}
};
describe('dynamic-import response delivery through the proxy', () => {
it('should keep the chunked gzip response shape the race depends on', async () => {
const response = await fetch(FETCH_URL, {
method: 'POST',
headers: { 'Content-Type': 'text/plain;charset=UTF-8' },
body: BODIES[1],
});
const text = await response.text();
expect(response.status).to.equal(200);
expect(
text.length,
'fixture went stale: response too small to be gzip-chunked, update BODIES from a fresh browser trace',
).to.be.greaterThan(10000);
});
it(`should deliver ${TOTAL_REQUESTS} concurrent dynamic-import responses without a single truncation`, async function () {
this.timeout(10 * 60 * 1000);
const failures: string[] = [];
let issued = 0;
const worker = async (): Promise<void> => {
while (issued < TOTAL_REQUESTS && failures.length === 0) {
const current = issued++;
const { error } = await fetchModuleTree(BODIES[current % BODIES.length]);
if (error) {
failures.push(`request #${current}: ${error}`);
}
}
};
await Promise.all(Array.from({ length: CONCURRENCY }, worker));
expect(failures).to.deep.equal([]);
});
});

View File

@@ -0,0 +1,216 @@
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import type { Response } from 'supertest';
import { sleep } from '../../../lib/utils/sleep';
import { getCredentials, api, request, credentials } from '../../data/api-data';
import { mockServerHealthy, mockServerReset, mockServerSet } from '../../data/mock-server.helper';
import { getSettingValueById, updateSetting } from '../../data/permissions.helper';
import { password } from '../../data/user';
import { createUser, login, deleteUser } from '../../data/users.helper';
import { withTimeout } from '../../data/utils';
const IMPORT_MOCK_SERVER_URL = process.env.IMPORT_MOCK_SERVER_URL ?? 'http://mock-server.dev:8080';
describe('Imports', () => {
before((done) => getCredentials(done));
describe('[/getCurrentImportOperation]', () => {
it('should return the current import operation', async () => {
await request
.get(api('getCurrentImportOperation'))
.set(credentials)
.expect(200)
.expect((res: Response) => {
expect(res.body.success).to.be.true;
expect(res.body.operation).not.be.null;
});
});
});
describe('[/downloadPendingFiles]', () => {
it('should return the number of pending files', async () => {
await request
.post(api('downloadPendingFiles'))
.set(credentials)
.expect(200)
.expect((res: Response) => {
expect(res.body.success).to.be.true;
expect(res.body.count).to.be.greaterThanOrEqual(0);
});
});
});
describe('[/downloadPendingAvatars]', () => {
it('should return the number of pending avatars', async () => {
await request
.post(api('downloadPendingAvatars'))
.set(credentials)
.expect(200)
.expect((res: Response) => {
expect(res.body.success).to.be.true;
expect(res.body.count).to.be.greaterThanOrEqual(0);
});
});
});
describe('[/getLatestImportOperations]', () => {
let testUser: any = {};
before(async () => {
testUser = await createUser();
});
let testCredentials: any = {};
before(async () => {
testCredentials = await login(testUser.username, password);
});
after(async () => {
await deleteUser(testUser);
testUser = undefined;
});
it('should fail if the user is not authorized', async () => {
await request
.get(api('getLatestImportOperations'))
.set(testCredentials)
.expect(403)
.expect((res: Response) => {
expect(res.body.success).to.be.false;
expect(res.body.error).to.equal('User does not have the permissions required for this action [error-unauthorized]');
});
});
it('should return the latest import operation', async () => {
await request
.get(api('getLatestImportOperations'))
.set(credentials)
.expect(200)
.expect((res: Response) => {
expect(res.body).to.be.an('array');
});
});
});
describe('[/getImportProgress]', () => {
it('should return the import progress', async () => {
await request
.get(api('getImportProgress'))
.set(credentials)
.expect(200)
.expect((res: Response) => {
expect(res.body.success).to.be.true;
expect(res.body.key).to.be.an('string');
expect(res.body.name).to.be.an('string');
expect(res.body.step).to.be.an('string');
expect(res.body.count).to.be.an('object');
});
});
});
describe('[/getImportFileData]', () => {
it('should return the import file data', async () => {
await request
.get(api('getImportFileData'))
.set(credentials)
.expect(200)
.expect((res: Response) => {
expect(res.body.success).to.be.true;
expect(res.body.users).to.be.an('array');
expect(res.body.channels).to.be.an('array');
expect(res.body.message_count).to.greaterThanOrEqual(0);
});
});
});
describe('[/downloadPublicImportFile]', () => {
let previousSsrfAllowlist: Awaited<ReturnType<typeof getSettingValueById>>;
const waitForImportStep = (expectedStep: string): Promise<string> =>
withTimeout(async (signal) => {
let step = '';
while (!signal.aborted) {
const res = await request.get(api('getImportProgress')).set(credentials).expect(200);
step = res.body.step;
if (step === expectedStep || step === 'importer_import_failed') {
return step;
}
await sleep(100);
}
return step;
}, 10_000);
before(async () => {
expect(await mockServerHealthy(), 'mock-server is not reachable — ensure it is running').to.be.true;
previousSsrfAllowlist = await getSettingValueById('SSRF_Allowlist');
await Promise.all([mockServerReset(), updateSetting('SSRF_Allowlist', '')]);
});
after(async () => {
await Promise.all([
mockServerReset(),
previousSsrfAllowlist !== undefined ? updateSetting('SSRF_Allowlist', previousSsrfAllowlist) : Promise.resolve(),
]);
});
it('should reject a private target that is not on the SSRF allowlist and mark the operation as failed', async () => {
await request
.post(api('downloadPublicImportFile'))
.set(credentials)
.send({ fileUrl: 'http://127.0.0.1:3000/api/v1/info', importerKey: 'slack-users' })
.expect(400)
.expect((res: Response) => {
expect(res.body.success).to.be.false;
expect(res.body.error).to.equal('error-ssrf-validation-failed');
});
const progress = await request.get(api('getImportProgress')).set(credentials).expect(200);
expect(progress.body.step).to.equal('importer_import_failed');
});
it('should download a file from an allowlisted private target', async () => {
await Promise.all([
mockServerSet('GET', '/import-test.zip', { marker: 'downloaded-by-server-fetch' }),
updateSetting('SSRF_Allowlist', new URL(IMPORT_MOCK_SERVER_URL).host),
]);
await request
.post(api('downloadPublicImportFile'))
.set(credentials)
.send({ fileUrl: `${IMPORT_MOCK_SERVER_URL}/import-test.zip`, importerKey: 'csv' })
.expect(200)
.expect((res: Response) => {
expect(res.body.success).to.be.true;
});
expect(await waitForImportStep('importer_file_loaded')).to.equal('importer_file_loaded');
});
});
describe('[/uploadImportFile]', () => {
let testUser: any = {};
before(async () => {
testUser = await createUser();
});
let testCredentials: any = {};
before(async () => {
testCredentials = await login(testUser.username, password);
});
after(async () => {
await deleteUser(testUser);
testUser = undefined;
});
it('should fail if the user is not authorized', async () => {
await request
.post(api('uploadImportFile'))
.set(testCredentials)
.send({
binaryContent: 'ZXJzLmNzdlBLBQYAAAAAAQABADcAAAAmAQAAAAA=',
contentType: 'application/zip',
fileName: 'users11.zip',
importerKey: 'csv',
})
.expect(403)
.expect((res: Response) => {
expect(res.body.success).to.be.false;
expect(res.body.error).to.equal('User does not have the permissions required for this action [error-unauthorized]');
});
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,260 @@
import type { Credentials } from '@rocket.chat/api-client';
import type { IInvite, IRoom, IUser } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import { getCredentials, api, request, credentials } from '../../data/api-data';
import { createRoom, deleteRoom } from '../../data/rooms.helper';
import { password } from '../../data/user';
import type { TestUser } from '../../data/users.helper';
import { createUser, deleteUser, login } from '../../data/users.helper';
describe('Invites', () => {
let testInviteID: IInvite['_id'];
before((done) => getCredentials(done));
describe('POST [/findOrCreateInvite]', () => {
it('should fail if not logged in', async () => {
const res = await request
.post(api('findOrCreateInvite'))
.send({
rid: 'GENERAL',
days: 1,
maxUses: 10,
})
.expect(401);
expect(res.body).to.have.property('status', 'error');
expect(res.body).to.have.property('message');
});
it('should fail if invalid roomid', async () => {
const res = await request
.post(api('findOrCreateInvite'))
.set(credentials)
.send({
rid: 'invalid',
days: 1,
maxUses: 10,
})
.expect(400);
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'error-invalid-room');
});
it('should create an invite for GENERAL', async () => {
const res = await request
.post(api('findOrCreateInvite'))
.set(credentials)
.send({
rid: 'GENERAL',
days: 1,
maxUses: 10,
})
.expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('days', 1);
expect(res.body).to.have.property('maxUses', 10);
expect(res.body).to.have.property('uses');
expect(res.body).to.have.property('_id');
testInviteID = res.body._id;
});
it('should return an existing invite for GENERAL', async () => {
const res = await request
.post(api('findOrCreateInvite'))
.set(credentials)
.send({
rid: 'GENERAL',
days: 1,
maxUses: 10,
})
.expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('days', 1);
expect(res.body).to.have.property('maxUses', 10);
expect(res.body).to.have.property('uses');
expect(res.body).to.have.property('_id', testInviteID);
});
});
describe('GET [/listInvites]', () => {
it('should fail if not logged in', async () => {
const res = await request.get(api('listInvites')).expect(401);
expect(res.body).to.have.property('status', 'error');
expect(res.body).to.have.property('message');
});
it('should return the existing invite for GENERAL', async () => {
const res = await request.get(api('listInvites')).set(credentials).expect(200);
expect(res.body[0]).to.have.property('_id', testInviteID);
});
});
describe('POST [/useInviteToken]', () => {
it('should fail if not logged in', async () => {
const res = await request.post(api('useInviteToken')).expect(401);
expect(res.body).to.have.property('status', 'error');
expect(res.body).to.have.property('message');
});
it('should fail if invalid token', async () => {
const res = await request
.post(api('useInviteToken'))
.set(credentials)
.send({
token: 'invalid',
})
.expect(400);
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'error-invalid-token');
});
it('should fail if missing token', async () => {
const res = await request.post(api('useInviteToken')).set(credentials).send({}).expect(400);
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'invalid-params');
});
it('should use the existing invite for GENERAL', async () => {
const res = await request
.post(api('useInviteToken'))
.set(credentials)
.send({
token: testInviteID,
})
.expect(200);
expect(res.body).to.have.property('success', true);
});
});
describe('POST [/validateInviteToken]', () => {
it('should warn if invalid token', async () => {
const res = await request
.post(api('validateInviteToken'))
.set(credentials)
.send({
token: 'invalid',
})
.expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('valid', false);
});
it('should succeed when valid token', async () => {
const res = await request
.post(api('validateInviteToken'))
.set(credentials)
.send({
token: testInviteID,
})
.expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('valid', true);
});
});
describe('POST [/useInviteToken] - banned user', () => {
let room: IRoom;
let bannedUser: TestUser<IUser>;
let bannedUserCredentials: Credentials;
let inviteId: IInvite['_id'];
before(async () => {
bannedUser = await createUser();
bannedUserCredentials = await login(bannedUser.username, password);
const result = await createRoom({ type: 'p', name: `invite-ban-test-${Date.now()}` });
room = result.body.group;
// Add user then ban them
await request.post(api('groups.invite')).set(credentials).send({ roomId: room._id, userId: bannedUser._id }).expect(200);
await request.post(api('rooms.banUser')).set(credentials).send({ roomId: room._id, userId: bannedUser._id }).expect(200);
// Create invite link for the room
const invite = await request
.post(api('findOrCreateInvite'))
.set(credentials)
.send({ rid: room._id, days: 1, maxUses: 10 })
.expect(200);
inviteId = invite.body._id;
});
after(async () => {
await deleteRoom({ type: 'p', roomId: room._id });
await deleteUser(bannedUser);
});
it('should fail if user is banned from the room', async () => {
await request
.post(api('useInviteToken'))
.set(bannedUserCredentials)
.send({ token: inviteId })
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'error-user-is-banned');
});
});
it('should succeed after the user is unbanned', async () => {
await request.post(api('rooms.unbanUser')).set(credentials).send({ roomId: room._id, userId: bannedUser._id }).expect(200);
await request
.post(api('useInviteToken'))
.set(bannedUserCredentials)
.send({ token: inviteId })
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('room').and.to.have.property('rid', room._id);
});
});
});
describe('DELETE [/removeInvite]', () => {
it('should fail if not logged in', async () => {
const res = await request.delete(api(`removeInvite/${testInviteID}`)).expect(401);
expect(res.body).to.have.property('status', 'error');
expect(res.body).to.have.property('message');
});
it('should fail if invalid token', async () => {
const res = await request.delete(api('removeInvite/invalid')).set(credentials).expect(400);
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'invalid-invitation-id');
});
it('should succeed when valid token', async () => {
const res = await request
.delete(api(`removeInvite/${testInviteID}`))
.set(credentials)
.expect(200);
expect(res.body).to.equal(true);
});
it('should fail when deleting the same invite again', async () => {
const res = await request
.delete(api(`removeInvite/${testInviteID}`))
.set(credentials)
.expect(400);
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'invalid-invitation-id');
});
});
});

View File

@@ -0,0 +1,99 @@
import type { Credentials } from '@rocket.chat/api-client';
import type { IUser } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { before, describe, it, after } from 'mocha';
import { getCredentials, api, request, credentials } from '../../data/api-data';
import { password } from '../../data/user';
import type { TestUser } from '../../data/users.helper';
import { createUser, deleteUser, login } from '../../data/users.helper';
describe('licenses', () => {
let createdUser: TestUser<IUser>;
before((done) => getCredentials(done));
let unauthorizedUserCredentials: Credentials;
before(async () => {
createdUser = await createUser();
unauthorizedUserCredentials = await login(createdUser.username, password);
});
after(() => deleteUser(createdUser));
describe('[/licenses.add]', () => {
it('should fail if not logged in', async () => {
const res = await request
.post(api('licenses.add'))
.send({
license: '',
})
.expect('Content-Type', 'application/json')
.expect(401);
expect(res.body).to.have.property('status', 'error');
expect(res.body).to.have.property('message');
});
it('should fail if user is unauthorized', async () => {
const res = await request
.post(api('licenses.add'))
.set(unauthorizedUserCredentials)
.send({
license: '',
})
.expect('Content-Type', 'application/json')
.expect(403);
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'User does not have the permissions required for this action [error-unauthorized]');
});
it('should fail if license is invalid', async () => {
const res = await request
.post(api('licenses.add'))
.set(credentials)
.send({
license: '',
})
.expect('Content-Type', 'application/json')
.expect(400);
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error');
});
});
describe('[/licenses.info]', () => {
it('should fail if not logged in', async () => {
const res = await request.get(api('licenses.info')).expect('Content-Type', 'application/json').expect(401);
expect(res.body).to.have.property('status', 'error');
expect(res.body).to.have.property('message');
});
it('should return limited information if user is unauthorized', async () => {
const res = await request
.get(api('licenses.info'))
.set(unauthorizedUserCredentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('license').and.to.be.an('object');
expect(res.body.license).to.not.have.property('license');
expect(res.body.license).to.have.property('tags').and.to.be.an('array');
});
it('should return unrestricted info if user is logged in and is authorized', async () => {
const res = await request.get(api('licenses.info')).set(credentials).expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('license').and.to.be.an('object');
if (process.env.IS_EE) {
expect(res.body.license).to.have.property('license').and.to.be.an('object');
}
expect(res.body.license).to.have.property('tags').and.to.be.an('array');
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,788 @@
import type { Credentials } from '@rocket.chat/api-client';
import { UserStatus } from '@rocket.chat/core-typings';
import type { ILivechatVisitor, IOmnichannelRoom, ILivechatAgent, ILivechatDepartment, IRoom, IUser } from '@rocket.chat/core-typings';
import { Random } from '@rocket.chat/random';
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import type { Response } from 'supertest';
import { getCredentials, api, request, credentials } from '../../../data/api-data';
import { disableDefaultBusinessHour, makeDefaultBusinessHourActiveAndClosed } from '../../../data/livechat/businessHours';
import { createDepartment, deleteDepartment } from '../../../data/livechat/department';
import {
createAgent,
createManager,
createVisitor,
createLivechatRoom,
takeInquiry,
fetchInquiry,
makeAgentAvailable,
startANewLivechatRoomAndTakeIt,
moveBackToQueue,
closeOmnichannelRoom,
deleteVisitor,
} from '../../../data/livechat/rooms';
import {
removePermissionFromAllRoles,
restorePermissionToRoles,
updateEESetting,
updatePermission,
updateSetting,
} from '../../../data/permissions.helper';
import { password } from '../../../data/user';
import { createUser, deleteUser, getMe, login, setUserStatus } from '../../../data/users.helper';
import { IS_EE } from '../../../e2e/config/constants';
describe('LIVECHAT - Agents', () => {
let agent: ILivechatAgent;
let manager: ILivechatAgent;
let agent2: { user: IUser; credentials: Credentials };
before((done) => getCredentials(done));
before(async () => {
await updateSetting('Livechat_enabled', true);
await updateSetting('Livechat_Routing_Method', 'Manual_Selection');
await updateEESetting('Livechat_Require_Contact_Verification', 'never');
await updateSetting('Omnichannel_enable_department_removal', true);
agent = await createAgent();
manager = await createManager();
});
before(async () => {
const user = await createUser();
const userCredentials = await login(user.username, password);
await createAgent(user.username);
await makeAgentAvailable(userCredentials);
agent2 = {
user,
credentials: userCredentials,
};
});
after(async () => {
await updateSetting('Omnichannel_enable_department_removal', false);
await deleteUser(agent2.user);
});
// TODO: missing test cases for POST method
describe('GET livechat/users/:type', () => {
describe('with no permissions', () => {
before(async () => {
await updatePermission('edit-omnichannel-contact', []);
await updatePermission('transfer-livechat-guest', []);
await updatePermission('manage-livechat-agents', []);
});
after(async () => {
await updatePermission('edit-omnichannel-contact', ['admin']);
await updatePermission('transfer-livechat-guest', ['admin']);
await updatePermission('manage-livechat-agents', ['admin']);
});
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await request.get(api('livechat/users/agent')).set(credentials).expect('Content-Type', 'application/json').expect(403);
});
});
it('should throw an error when the type is invalid', async () => {
await request
.get(api('livechat/users/invalid-type'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body.error).to.be.equal('Invalid type');
});
});
it('should return an array of agents', async () => {
await request
.get(api('livechat/users/agent'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body.users).to.be.an('array');
expect(res.body).to.have.property('offset');
expect(res.body).to.have.property('total');
expect(res.body).to.have.property('count');
const agentRecentlyCreated = (res.body.users as ILivechatAgent[]).find((user) => agent._id === user._id);
expect(agentRecentlyCreated?._id).to.be.equal(agent._id);
});
});
it('should return an array of available agents', async () => {
await request
.get(api('livechat/users/agent'))
.set(credentials)
.expect('Content-Type', 'application/json')
.query({ onlyAvailable: true })
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body.users).to.be.an('array');
expect(res.body).to.have.property('offset');
expect(res.body).to.have.property('total');
expect(res.body).to.have.property('count');
expect(res.body.users.every((u: { statusLivechat: string }) => u.statusLivechat === 'available')).to.be.true;
});
});
it('should return an array of available/unavailable agents when onlyAvailable is false', async () => {
await request
.get(api('livechat/users/agent'))
.set(credentials)
.expect('Content-Type', 'application/json')
.query({ onlyAvailable: false })
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body.users).to.be.an('array');
expect(res.body).to.have.property('offset');
expect(res.body).to.have.property('total');
expect(res.body).to.have.property('count');
expect(
res.body.users.every(
(u: { statusLivechat: string }) => !u.statusLivechat || ['available', 'not-available'].includes(u.statusLivechat),
),
).to.be.true;
});
});
it('should return offline agents when showIdleAgents is true', async () => {
await setUserStatus(agent2.credentials, UserStatus.OFFLINE);
await request
.get(api('livechat/users/agent'))
.set(credentials)
.expect('Content-Type', 'application/json')
.query({ showIdleAgents: true })
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body.users).to.be.an('array');
expect(res.body).to.have.property('offset');
expect(res.body).to.have.property('total');
expect(res.body).to.have.property('count');
expect(
res.body.users.every(
(u: { status: UserStatus }) =>
!u.status || [UserStatus.ONLINE, UserStatus.OFFLINE, UserStatus.AWAY, UserStatus.BUSY].includes(u.status),
),
).to.be.true;
});
});
it('should return only online agents when showIdleAgents is false', async () => {
await setUserStatus(agent2.credentials, UserStatus.ONLINE);
await request
.get(api('livechat/users/agent'))
.set(credentials)
.expect('Content-Type', 'application/json')
.query({ showIdleAgents: false })
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body.users).to.be.an('array');
expect(res.body).to.have.property('offset');
expect(res.body).to.have.property('total');
expect(res.body).to.have.property('count');
expect(res.body.users.every((u: { status: UserStatus }) => u.status !== UserStatus.OFFLINE)).to.be.true;
});
});
it('should return an array of managers', async () => {
await request
.get(api('livechat/users/manager'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body.users).to.be.an('array');
expect(res.body).to.have.property('offset');
expect(res.body).to.have.property('total');
expect(res.body).to.have.property('count');
const managerRecentlyCreated = (res.body.users as ILivechatAgent[]).find((user) => manager._id === user._id);
expect(managerRecentlyCreated?._id).to.be.equal(manager._id);
});
});
});
describe('POST livechat/users/:type', () => {
describe('with no permission', () => {
before(async () => {
await updatePermission('view-livechat-manager', []);
});
after(async () => {
await updatePermission('view-livechat-manager', ['admin']);
});
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await request
.post(api('livechat/users/agent'))
.set(credentials)
.send({
username: 'test-agent',
})
.expect('Content-Type', 'application/json')
.expect(403);
});
});
it('should return an error when type is invalid', async () => {
await request
.post(api('livechat/users/invalid-type'))
.set(credentials)
.send({
username: 'test-agent',
})
.expect('Content-Type', 'application/json')
.expect(400);
});
it('should return an error when username is invalid', async () => {
await request
.post(api('livechat/users/agent'))
.set(credentials)
.send({
username: 'mr-not-valid',
})
.expect('Content-Type', 'application/json')
.expect(400);
});
it('should return a valid user when all goes fine', async () => {
const user = await createUser();
await request
.post(api('livechat/users/agent'))
.set(credentials)
.send({
username: user.username,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('user');
expect(res.body.user).to.have.property('_id');
expect(res.body.user).to.have.property('username');
});
// cleanup
await deleteUser(user);
});
it('should properly create a manager', async () => {
const user = await createUser();
await request
.post(api('livechat/users/manager'))
.set(credentials)
.send({
username: user.username,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('user');
expect(res.body.user).to.have.property('_id');
expect(res.body.user).to.have.property('username');
});
// cleanup
await deleteUser(user);
});
});
describe('GET livechat/users/:type/:_id', () => {
describe('with no permission', () => {
before(async () => {
await updatePermission('view-livechat-manager', []);
});
after(async () => {
await updatePermission('view-livechat-manager', ['admin']);
});
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await request
.get(api(`livechat/users/agent/id${agent._id}`))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(403);
});
});
describe('with new user', () => {
let user: IUser;
before(async () => {
user = await createUser();
});
after(async () => {
await deleteUser(user);
});
it('should return an error when type is invalid', async () => {
await request
.get(api(`livechat/users/invalid-type/${agent._id}`))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(400);
}).timeout(5000);
it('should return a valid user when all goes fine', async () => {
await request
.get(api(`livechat/users/agent/${agent._id}`))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('user');
expect(res.body.user).to.have.property('_id');
expect(res.body.user).to.have.property('username');
expect(res.body.user).to.not.have.property('roles');
});
});
it('should return { user: null } when user is not an agent', async () => {
await request
.get(api(`livechat/users/agent/${user._id}`))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('user');
expect(res.body.user).to.be.null;
});
});
it('should return { user: null } when user is not a manager', async () => {
await request
.get(api(`livechat/users/manager/${user._id}`))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('user');
expect(res.body.user).to.be.null;
});
});
});
});
describe('DELETE livechat/users/:type/:_id', () => {
describe('with no permission', () => {
before(async () => {
await updatePermission('view-livechat-manager', []);
});
after(async () => {
await updatePermission('view-livechat-manager', ['admin']);
});
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await request.delete(api(`livechat/users/agent/id`)).set(credentials).expect('Content-Type', 'application/json').expect(403);
}).timeout(5000);
});
it('should return an error when type is invalid', async () => {
await request.delete(api(`livechat/users/invalid-type/id`)).set(credentials).expect('Content-Type', 'application/json').expect(400);
}).timeout(5000);
it('should return an error when _id is invalid', async () => {
await request.delete(api('livechat/users/agent/invalid-id')).set(credentials).expect('Content-Type', 'application/json').expect(400);
}).timeout(5000);
it('should return a valid user when all goes fine', async () => {
const agent = await createAgent();
await request
.delete(api(`livechat/users/agent/${agent._id}`))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
});
});
(IS_EE ? describe : describe.skip)('livechat/agents/:agentId/departments', () => {
let dep1: ILivechatDepartment;
let dep2: ILivechatDepartment;
before(async () => {
dep1 = await createDepartment(
{
enabled: true,
name: Random.id(),
showOnRegistration: true,
email: `${Random.id()}@example.com`,
showOnOfflineForm: true,
},
[{ agentId: credentials['X-User-Id'] }],
);
dep2 = await createDepartment(
{
enabled: false,
name: Random.id(),
email: `${Random.id()}@example.com`,
showOnRegistration: true,
showOnOfflineForm: true,
},
[{ agentId: credentials['X-User-Id'] }],
);
});
after(async () => {
await deleteDepartment(dep1._id);
await deleteDepartment(dep2._id);
});
describe('with no permission', () => {
before(async () => {
await updatePermission('view-l-room', []);
});
after(async () => {
await updatePermission('view-l-room', ['livechat-manager', 'livechat-agent', 'admin']);
});
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await request
.get(api(`livechat/agents/${agent._id}/departments`))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(403);
});
});
it('should return an empty array of departments when the agentId is invalid', async () => {
await request
.get(api('livechat/agents/invalid-id/departments'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('departments').and.to.be.an('array');
});
});
it('should return an array of departments when the agentId is valid', async () => {
await request
.get(api(`livechat/agents/${agent._id}/departments`))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('departments').and.to.be.an('array');
expect(res.body.departments.length).to.be.equal(2);
(res.body.departments as ILivechatDepartment[]).forEach((department) => {
expect(department.agentId).to.be.equal(agent._id);
expect(department).to.have.property('departmentName').that.is.a('string');
});
});
});
it('should return only enabled departments when param `enabledDepartmentsOnly` is true ', async () => {
await request
.get(api(`livechat/agents/${agent._id}/departments`))
.set(credentials)
.query({ enabledDepartmentsOnly: true })
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('departments').and.to.be.an('array');
expect(res.body.departments.length).to.be.equal(1);
expect(res.body.departments[0].departmentEnabled).to.be.true;
});
});
});
describe('livechat/agent.info/:rid/:token', () => {
let room: IOmnichannelRoom;
let visitor: ILivechatVisitor;
before(async () => {
visitor = await createVisitor();
room = await createLivechatRoom(visitor.token);
});
after(async () => {
await closeOmnichannelRoom(room._id);
await deleteVisitor(visitor.token);
});
it('should fail when token in url params is not valid', async () => {
await request.get(api(`livechat/agent.info/soemthing/invalid-token`)).expect(400);
});
it('should fail when token is valid but rid isnt', async () => {
const visitor = await createVisitor();
await request.get(api(`livechat/agent.info/invalid-rid/${visitor.token}`)).expect(400);
await deleteVisitor(visitor.token);
});
/* it('should fail when room is not being served by any agent', async () => {
await updateSetting('Livechat_Routing_Method', 'Manual_Selection');
const visitor = await createVisitor();
const room = await createLivechatRoom(visitor.token);
await request.get(api(`livechat/agent.info/${room._id}/${visitor.token}`)).expect(400);
}); */
it('should return a valid agent when the room is being served and the room belongs to visitor', async () => {
const inq = await fetchInquiry(room._id);
await takeInquiry(inq._id, agent2.credentials);
const { body } = await request.get(api(`livechat/agent.info/${room._id}/${visitor.token}`));
expect(body).to.have.property('success', true);
expect(body).to.have.property('agent');
expect(body.agent).to.have.property('_id', agent2.user._id);
});
});
describe('livechat/agent.next/:token', () => {
let visitor: ILivechatVisitor;
let room: IOmnichannelRoom;
before(async () => {
visitor = await createVisitor();
room = await createLivechatRoom(visitor.token);
});
after(async () => {
await closeOmnichannelRoom(room._id);
await deleteVisitor(visitor.token);
});
it('should fail when token in url params is not valid', async () => {
await request.get(api(`livechat/agent.next/invalid-token`)).expect(400);
});
it('should return success when visitor with token has an open room', async () => {
await request.get(api(`livechat/agent.next/${visitor.token}`)).expect(200);
});
// TODO: test cases when algo is Auto_Selection
});
describe('livechat/agent.next/:token - with manual selection', () => {
before(async () => {
await updateSetting('Livechat_Routing_Method', 'Manual_Selection');
});
after(async () => {
await updateSetting('Livechat_Routing_Method', 'Auto_Selection');
});
it('should fail if theres no open room for visitor and algo is manual selection', async () => {
const visitor = await createVisitor();
await request.get(api(`livechat/agent.next/${visitor.token}`)).expect(400);
await deleteVisitor(visitor.token);
});
});
describe('livechat/agent.status', () => {
describe('with no view-l permission', () => {
before(async () => {
await removePermissionFromAllRoles('view-l-room');
});
after(async () => {
await restorePermissionToRoles('view-l-room');
});
it('should return an "unauthorized error" when the user does not have the necessary permission to change its own status', async () => {
await request.post(api('livechat/agent.status')).set(credentials).send({ status: 'not-available' }).expect(403);
});
});
describe('with no manage-agent permission', () => {
before(async () => {
await request
.post(api('livechat/agent.status'))
.set(credentials)
.send({ status: 'not-available', agentId: agent2.user._id })
.expect(200);
await removePermissionFromAllRoles('manage-livechat-agents');
});
after(async () => {
await restorePermissionToRoles('manage-livechat-agents');
});
it('should return an "unauthorized error" when the user does not have the necessary permission to change other status', async () => {
await request
.post(api('livechat/agent.status'))
.set(credentials)
.send({ status: 'available', agentId: agent2.user._id })
.expect(403);
});
});
it('should return an error if user is not an agent', async () => {
const user = await createUser({ roles: ['livechat-manager'] });
const userCredentials = await login(user.username, password);
await request
.post(api('livechat/agent.status'))
.set(userCredentials)
.send({ status: 'available', agentId: user._id })
.expect(404)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'Agent not found');
});
// cleanup
await deleteUser(user);
});
it('should return an error if status is not valid', async () => {
await request
.post(api('livechat/agent.status'))
.set(agent2.credentials)
.send({ status: 'invalid-status', agentId: agent2.user._id })
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error');
});
});
it('should return an error if agentId param is not valid', async () => {
await request
.post(api('livechat/agent.status'))
.set(agent2.credentials)
.send({ status: 'available', agentId: 'invalid-agent-id' })
.expect(404)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'Agent not found');
});
});
describe('cases for valid agents', () => {
let currentUser: ILivechatAgent;
before(async () => {
currentUser = await getMe(agent2.credentials);
});
afterEach(async () => {
await request
.post(api('livechat/agent.status'))
.set(agent2.credentials)
.send({ status: 'not-available', agentId: currentUser._id })
.expect(200);
});
it('should be able to set a logged in users status to available', async () => {
await request
.post(api('livechat/agent.status'))
.set(agent2.credentials)
.send({ status: 'available', agentId: currentUser._id })
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('status', 'available');
});
});
it('should allow managers to set other agents status to available', async () => {
await request
.post(api('livechat/agent.status'))
.set(credentials)
.send({ status: 'available', agentId: currentUser._id })
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('status', 'available');
});
});
describe('outside of business hours', () => {
before(async () => {
await makeDefaultBusinessHourActiveAndClosed();
await request
.post(api('livechat/agent.status'))
.set(agent2.credentials)
.send({ status: 'not-available', agentId: currentUser._id })
.expect(200);
});
after(async () => {
await disableDefaultBusinessHour();
});
it('should throw an error if agent tries to make themselves available outside of Business hour', async () => {
await request
.post(api('livechat/agent.status'))
.set(agent2.credentials)
.send({ status: 'available', agentId: currentUser._id })
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'error-business-hours-are-closed');
});
});
it('should return success but not change the agent when admin make try other agents available outside business hour', async () => {
await request
.post(api('livechat/agent.status'))
.set(credentials)
.send({ status: 'available', agentId: currentUser._id })
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('status', 'not-available');
});
});
});
});
});
describe('Agent sidebar', () => {
let testUser: { user: IUser; credentials: Credentials };
before(async () => {
const user = await createUser();
await createAgent(user.username);
const credentials2 = await login(user.username, password);
await makeAgentAvailable(credentials2);
testUser = {
user,
credentials: credentials2,
};
});
after(async () => {
await deleteUser(testUser.user);
});
it('should return an empty list of rooms for a newly created agent', async () => {
const { body } = await request.get(api('rooms.get')).set(testUser.credentials).send({}).expect(200);
expect(body).to.have.property('success', true);
expect(body.update.filter((r: IRoom) => r.t === 'l')).to.have.lengthOf(0);
});
describe('with a room', () => {
let room: IOmnichannelRoom;
let visitor: ILivechatVisitor;
before(async () => {
const { room: r, visitor: v } = await startANewLivechatRoomAndTakeIt({ agent: testUser.credentials });
room = r;
visitor = v;
});
after(async () => {
await closeOmnichannelRoom(room._id);
await deleteVisitor(visitor.token);
});
it('should have a new room in his sidebar after taking a conversation from the queue', async () => {
const { body } = await request.get(api('rooms.get')).set(testUser.credentials).send({}).expect(200);
expect(body).to.have.property('success', true);
const livechatRooms = body.update.filter((r: IRoom) => r.t === 'l');
expect(livechatRooms).to.have.lengthOf(1);
expect(body.update.find((r: { _id: string }) => r._id === room._id)).to.be.an('object');
expect(body.update.find((r: { _id: string }) => r._id === 'GENERAL')).to.be.an('object');
});
it('should not have the room if user moves room back to queue', async () => {
await moveBackToQueue(room._id, testUser.credentials);
const { body } = await request
.get(api('rooms.get'))
.set(testUser.credentials)
.query({ updatedSince: new Date(new Date().getTime() - 2000) })
.expect(200);
expect(body).to.have.property('success', true);
expect(body.update.find((r: { _id: string }) => r._id === room._id)).to.be.undefined;
});
});
it('should not have the room if the user closes the room', async () => {
const { room, visitor } = await startANewLivechatRoomAndTakeIt({ agent: testUser.credentials });
await closeOmnichannelRoom(room._id);
const { body } = await request.get(api('rooms.get')).set(testUser.credentials).expect(200);
expect(body.update.find((r: { _id: string }) => r._id === room._id)).to.be.undefined;
await deleteVisitor(visitor.token);
});
});
});

View File

@@ -0,0 +1,265 @@
import { expect } from 'chai';
import { before, describe, it } from 'mocha';
import type { Response } from 'supertest';
import { getCredentials, api, request, credentials } from '../../../data/api-data';
import { sleep } from '../../../data/livechat/utils';
import { removePermissionFromAllRoles, restorePermissionToRoles, updatePermission, updateSetting } from '../../../data/permissions.helper';
import { IS_EE } from '../../../e2e/config/constants';
describe('LIVECHAT - appearance', () => {
before((done) => getCredentials(done));
before(async () => {
await updateSetting('Livechat_enabled', true);
});
describe('livechat/appearance', () => {
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await updatePermission('view-livechat-manager', []);
await request.get(api('livechat/appearance')).set(credentials).expect('Content-Type', 'application/json').expect(403);
});
it('should return an array of settings', async () => {
await updatePermission('view-livechat-manager', ['admin']);
await request
.get(api('livechat/appearance'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body.appearance).to.be.an('array');
});
});
});
describe('POST livechat/appearance', () => {
it('should fail if user is not logged in', async () => {
await request.post(api('livechat/appearance')).send({}).expect(401);
});
it('should fail if body is not an array', async () => {
await request.post(api('livechat/appearance')).set(credentials).send({}).expect(400);
});
it('should fail if body is an empty array', async () => {
await request.post(api('livechat/appearance')).set(credentials).send([]).expect(400);
});
it('should fail if body does not contain value', async () => {
await request
.post(api('livechat/appearance'))
.set(credentials)
.send([{ name: 'Livechat_title' }])
.expect(400);
});
it('should fail if body does not contain name', async () => {
await request
.post(api('livechat/appearance'))
.set(credentials)
.send([{ value: 'test' }])
.expect(400);
});
it('should fail if user does not have the necessary permission', async () => {
await removePermissionFromAllRoles('view-livechat-manager');
await request
.post(api('livechat/appearance'))
.set(credentials)
.send([{ _id: 'invalid', value: 'test' }])
.expect(403);
});
it('should fail if body contains invalid _id', async () => {
await restorePermissionToRoles('view-livechat-manager');
await request
.post(api('livechat/appearance'))
.set(credentials)
.send([{ _id: 'invalid', value: 'test' }])
.expect(400);
});
it('should update the settings', async () => {
await request
.post(api('livechat/appearance'))
.set(credentials)
.send([{ _id: 'Livechat_title', value: 'test' }])
.expect(200);
});
// Test for: https://github.com/ajv-validator/ajv/issues/1140
it('should update a boolean setting and keep it as boolean', async () => {
await request
.post(api('livechat/appearance'))
.set(credentials)
.send([{ _id: 'Livechat_registration_form', value: true }])
.expect(200);
// Just enough to get the stream to update cached settings
await sleep(500);
// Get data from livechat/config
const { body } = await request.get(api('livechat/config')).set(credentials).expect(200);
expect(body.config.settings.registrationForm).to.be.true;
});
it('should update a boolean setting and keep it as boolean', async () => {
await request
.post(api('livechat/appearance'))
.set(credentials)
.send([{ _id: 'Livechat_registration_form', value: false }])
.expect(200);
// Just enough to get the stream to update cached settings
await sleep(500);
// Get data from livechat/config
const { body } = await request.get(api('livechat/config')).set(credentials).expect(200);
expect(body.config.settings.registrationForm).to.be.false;
});
it('should update a number setting and keep it as number', async () => {
await updateSetting('Livechat_enable_message_character_limit', true);
await request
.post(api('livechat/appearance'))
.set(credentials)
.send([{ _id: 'Livechat_message_character_limit', value: 100 }])
.expect(200);
// Just enough to get the stream to update cached settings
await sleep(500);
// Get data from livechat/config
const { body } = await request.get(api('livechat/config')).set(credentials).expect(200);
expect(body.config.settings.limitTextLength).to.be.equal(100);
await updateSetting('Livechat_enable_message_character_limit', false);
});
it('should coerce the value of a setting based on its stored datatype (int)', async () => {
await updateSetting('Livechat_enable_message_character_limit', true);
await request
.post(api('livechat/appearance'))
.set(credentials)
.send([{ _id: 'Livechat_message_character_limit', value: '100' }])
.expect(200);
// Just enough to get the stream to update cached settings
await sleep(500);
// Get data from livechat/config
const { body } = await request.get(api('livechat/config')).set(credentials).expect(200);
expect(body.config.settings.limitTextLength).to.be.equal(100);
await updateSetting('Livechat_enable_message_character_limit', false);
});
it('should coerce the value of a setting based on its stored datatype (boolean)', async () => {
await request
.post(api('livechat/appearance'))
.set(credentials)
.send([{ _id: 'Livechat_registration_form', value: 'true' }])
.expect(200);
// Just enough to get the stream to update cached settings
await sleep(500);
// Get data from livechat/config
const { body } = await request.get(api('livechat/config')).set(credentials).expect(200);
expect(body.config.settings.registrationForm).to.be.true;
});
it('should coerce an invalid number value to zero', async () => {
await request
.post(api('livechat/appearance'))
.set(credentials)
.send([
{ _id: 'Livechat_message_character_limit', value: 'xxxx' },
{ _id: 'Livechat_enable_message_character_limit', value: true },
])
.expect(200);
// Just enough to get the stream to update cached settings
await sleep(500);
// Get data from livechat/config
const { body } = await request.get(api('livechat/config')).set(credentials).expect(200);
// When setting is 0, we default to Message_MaxAllowedSize value
expect(body.config.settings.limitTextLength).to.be.equal(5000);
});
it('should coerce a boolean value on an int setting to 0', async () => {
await request
.post(api('livechat/appearance'))
.set(credentials)
.send([
{ _id: 'Livechat_message_character_limit', value: true },
{ _id: 'Livechat_enable_message_character_limit', value: true },
])
.expect(200);
// Just enough to get the stream to update cached settings
await sleep(500);
// Get data from livechat/config
const { body } = await request.get(api('livechat/config')).set(credentials).expect(200);
expect(body.config.settings.limitTextLength).to.be.equal(5000);
});
it('should coerce a non boolean value on a boolean setting to false', async () => {
await request
.post(api('livechat/appearance'))
.set(credentials)
.send([{ _id: 'Livechat_enable_message_character_limit', value: 'xxxx' }])
.expect(200);
// Just enough to get the stream to update cached settings
await sleep(500);
// Get data from livechat/config
const { body } = await request.get(api('livechat/config')).set(credentials).expect(200);
expect(body.config.settings.limitTextLength).to.be.false;
});
(IS_EE ? it : it.skip)('should accept an array setting', async () => {
await request
.post(api('livechat/appearance'))
.set(credentials)
.send([{ _id: 'Livechat_hide_system_messages', value: ['livechat-started'] }])
.expect(200);
await sleep(500);
// Get data from livechat/config
const { body } = await request.get(api('livechat/config')).set(credentials).expect(200);
expect(body.config.settings.hiddenSystemMessages).to.be.an('array');
expect(body.config.settings.hiddenSystemMessages).to.include('livechat-started');
});
(IS_EE ? it : it.skip)('should accept an array setting with multiple values', async () => {
await request
.post(api('livechat/appearance'))
.set(credentials)
.send([{ _id: 'Livechat_hide_system_messages', value: ['uj', 'livechat_transfer_history'] }])
.expect(200);
await sleep(500);
// Get data from livechat/config
const { body } = await request.get(api('livechat/config')).set(credentials).expect(200);
expect(body.config.settings.hiddenSystemMessages).to.be.an('array');
expect(body.config.settings.hiddenSystemMessages).to.include('uj');
expect(body.config.settings.hiddenSystemMessages).to.include('livechat_transfer_history');
expect(body.config.settings.hiddenSystemMessages).not.to.include('livechat-started');
});
(IS_EE ? it : it.skip)('should not update an array setting with a value other than array', async () => {
await request
.post(api('livechat/appearance'))
.set(credentials)
.send([{ _id: 'Livechat_hide_system_messages', value: 'uj' }])
.expect(200);
await sleep(500);
// Get data from livechat/config
const { body } = await request.get(api('livechat/config')).set(credentials).expect(200);
expect(body.config.settings.hiddenSystemMessages).to.be.an('array');
expect(body.config.settings.hiddenSystemMessages).to.include('uj');
expect(body.config.settings.hiddenSystemMessages).to.include('livechat_transfer_history');
});
(IS_EE ? it : it.skip)('should not update an array setting with values that are not valid setting values', async () => {
await request
.post(api('livechat/appearance'))
.set(credentials)
.send([{ _id: 'Livechat_hide_system_messages', value: ['livechat-started', 'invalid'] }])
.expect(200);
await sleep(500);
// Get data from livechat/config
const { body } = await request.get(api('livechat/config')).set(credentials).expect(200);
expect(body.config.settings.hiddenSystemMessages).to.be.an('array');
expect(body.config.settings.hiddenSystemMessages).to.include('uj');
expect(body.config.settings.hiddenSystemMessages).to.include('livechat_transfer_history');
expect(body.config.settings.hiddenSystemMessages).to.not.include('livechat-started');
expect(body.config.settings.hiddenSystemMessages).to.not.include('invalid');
});
});
});

View File

@@ -0,0 +1,746 @@
import type { ILivechatVisitor, IOmnichannelRoom } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import type { Response } from 'supertest';
import { getCredentials, api, request, credentials } from '../../../data/api-data';
import { createCustomField, deleteCustomField } from '../../../data/livechat/custom-fields';
import { closeOmnichannelRoom, createLivechatRoom, createVisitor, deleteVisitor, getLivechatRoomInfo } from '../../../data/livechat/rooms';
import { updatePermission, updateSetting } from '../../../data/permissions.helper';
describe('LIVECHAT - custom fields', () => {
before((done) => getCredentials(done));
before(async () => {
await updateSetting('Livechat_enabled', true);
});
describe('livechat/custom-fields', () => {
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await updatePermission('view-l-room', []);
await request.get(api('livechat/custom-fields')).set(credentials).expect('Content-Type', 'application/json').expect(403);
});
it('should return an array of custom fields', async () => {
await updatePermission('view-l-room', ['admin']);
await request
.get(api('livechat/custom-fields'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body.customFields).to.be.an('array');
expect(res.body).to.have.property('offset');
expect(res.body).to.have.property('total');
expect(res.body).to.have.property('count');
});
});
it('should return an array of custom fields even requested with count and offset params', async () => {
await updatePermission('view-l-room', ['admin']);
await request
.get(api('livechat/custom-fields'))
.set(credentials)
.query({
count: 5,
offset: 0,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body.customFields).to.be.an('array');
expect(res.body).to.have.property('offset');
expect(res.body).to.have.property('total');
expect(res.body).to.have.property('count');
});
});
});
describe('livechat/custom-fields/id', () => {
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await updatePermission('view-l-room', []);
await request.get(api('livechat/custom-fields/invalid-id')).set(credentials).expect('Content-Type', 'application/json').expect(403);
await updatePermission('view-l-room', ['admin']);
});
});
describe('livechat/custom.field', () => {
it('should fail when token is not on body params', async () => {
await request.post(api('livechat/custom.field')).expect(400);
});
it('should fail when key is not on body params', async () => {
await request.post(api('livechat/custom.field')).send({ token: 'invalid-token' }).expect(400);
});
it('should fail when value is not on body params', async () => {
await request.post(api('livechat/custom.field')).send({ token: 'invalid-token', key: 'invalid-key' }).expect(400);
});
it('should fail when overwrite is not on body params', async () => {
await request
.post(api('livechat/custom.field'))
.send({ token: 'invalid-token', key: 'invalid-key', value: 'invalid-value' })
.expect(400);
});
it('should fail when token is invalid', async () => {
await request
.post(api('livechat/custom.field'))
.send({ token: 'invalid-token', key: 'invalid-key', value: 'invalid-value', overwrite: true })
.expect(400);
});
it('should fail when key is invalid', async () => {
const visitor = await createVisitor();
await request
.post(api('livechat/custom.field'))
.send({ token: visitor.token, key: 'invalid-key', value: 'invalid-value', overwrite: true })
.expect(400);
await deleteVisitor(visitor.token);
});
it('should save a custom field on visitor', async () => {
const visitor = await createVisitor();
const customFieldName = `new_custom_field_${Date.now()}`;
await createCustomField({
searchable: true,
field: customFieldName,
label: customFieldName,
defaultValue: 'test_default_address',
scope: 'visitor',
visibility: 'public',
regexp: '',
});
const { body } = await request
.post(api('livechat/custom.field'))
.send({ token: visitor.token, key: customFieldName, value: 'test_address', overwrite: true })
.expect(200);
expect(body).to.have.property('success', true);
expect(body).to.have.property('field');
expect(body.field).to.have.property('value', 'test_address');
await deleteVisitor(visitor.token);
});
});
describe('livechat/custom.fields', () => {
const customFieldName = `new_custom_field_${Date.now()}_1`;
const customFieldName2 = `new_custom_field_${Date.now()}_2`;
const customFieldName3 = `new_custom_field_${Date.now()}_3`;
const roomCustomField = `new_custom_field_${Date.now()}_4`;
let visitor: ILivechatVisitor;
let visitorRoom: IOmnichannelRoom;
before(async () => {
await updateSetting('Livechat_accept_chats_with_no_agents', true);
await createCustomField({
searchable: true,
field: customFieldName,
label: customFieldName,
defaultValue: 'test_default_address',
scope: 'visitor',
visibility: 'public',
regexp: '',
});
await createCustomField({
searchable: true,
field: customFieldName2,
label: customFieldName2,
defaultValue: 'test_default_address',
scope: 'visitor',
visibility: 'public',
regexp: '',
});
await createCustomField({
searchable: true,
field: customFieldName3,
label: customFieldName3,
defaultValue: 'test_default_address',
scope: 'visitor',
visibility: 'public',
regexp: '',
});
await createCustomField({
searchable: true,
field: roomCustomField,
label: roomCustomField,
defaultValue: 'test_default_address',
scope: 'room',
visibility: 'public',
regexp: '',
});
visitor = await createVisitor();
// start a room for visitor2
visitorRoom = await createLivechatRoom(visitor.token);
});
after(async () => {
await Promise.all([
deleteCustomField(customFieldName),
deleteCustomField(customFieldName2),
deleteCustomField(customFieldName3),
deleteCustomField(roomCustomField),
closeOmnichannelRoom(visitorRoom._id),
deleteVisitor(visitor.token),
updateSetting('Livechat_accept_chats_with_no_agents', false),
]);
});
it('should fail when token is not on body params', async () => {
await request.post(api('livechat/custom.fields')).expect(400);
});
it('should fail if customFields is not on body params', async () => {
await request.post(api('livechat/custom.fields')).send({ token: 'invalid-token' }).expect(400);
});
it('should fail if customFields is not an array', async () => {
await request
.post(api('livechat/custom.fields'))
.send({
token: 'invalid-token',
customFields: 'invalid-custom-fields',
})
.expect(400);
});
it('should fail if customFields is an empty array', async () => {
await request.post(api('livechat/custom.fields')).send({ token: 'invalid-token', customFields: [] }).expect(400);
});
it('should fail if customFields is an array with invalid objects', async () => {
await request
.post(api('livechat/custom.fields'))
.send({ token: 'invalid-token', customFields: [{}] })
.expect(400);
});
it('should fail if token is not a valid token', async () => {
await request
.post(api('livechat/custom.fields'))
.send({
token: 'invalid-token',
customFields: [{ key: 'invalid-key', value: 'invalid-value', overwrite: true }],
})
.expect(400);
});
it('should fail when customFields.key is invalid', async () => {
await request
.post(api('livechat/custom.fields'))
.send({
token: visitor.token,
customFields: [{ key: 'invalid-key', value: 'invalid-value', overwrite: true }],
})
.expect(400);
});
it('should save a custom field on visitor', async () => {
const visitor = await createVisitor();
const { body } = await request
.post(api('livechat/custom.fields'))
.send({
token: visitor.token,
customFields: [{ key: customFieldName, value: 'test_address', overwrite: true }],
})
.expect(200);
expect(body).to.have.property('success', true);
expect(body).to.have.property('fields');
expect(body.fields).to.be.an('array');
expect(body.fields).to.have.lengthOf(1);
expect(body.fields[0]).to.have.property('value', 'test_address');
await deleteVisitor(visitor.token);
});
it('should save multiple custom fields on a visitor', async () => {
const visitor = await createVisitor();
const { body } = await request
.post(api('livechat/custom.fields'))
.send({
token: visitor.token,
customFields: [
{ key: customFieldName, value: 'test_address', overwrite: true },
{ key: customFieldName2, value: 'test_address2', overwrite: true },
{ key: customFieldName3, value: 'test_address3', overwrite: true },
],
})
.expect(200);
expect(body).to.have.property('success', true);
expect(body).to.have.property('fields');
expect(body.fields).to.be.an('array');
expect(body.fields).to.have.lengthOf(3);
expect(body.fields[0]).to.have.property('value', 'test_address');
expect(body.fields[1]).to.have.property('value', 'test_address2');
expect(body.fields[2]).to.have.property('value', 'test_address3');
await deleteVisitor(visitor.token);
});
it('should save multiple custom fields on contact when visitor already has custom fields and an update with multiple fields is issued', async () => {
const { body } = await request
.post(api('livechat/custom.fields'))
.send({
token: visitor.token,
customFields: [{ key: customFieldName, value: 'test_address', overwrite: true }],
})
.expect(200);
expect(body).to.have.property('success', true);
expect(body).to.have.property('fields');
expect(body.fields).to.be.an('array');
expect(body.fields).to.have.lengthOf(1);
expect(body.fields[0]).to.have.property('value', 'test_address');
await request
.post(api('livechat/custom.fields'))
.send({
token: visitor.token,
customFields: [
{ key: customFieldName2, value: 'test_address2', overwrite: true },
{ key: customFieldName3, value: 'test_address3', overwrite: true },
],
})
.expect(200);
await request
.get(api(`omnichannel/contacts.get`))
.set(credentials)
.query({ contactId: visitorRoom.contactId })
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('contact');
expect(res.body.contact).to.have.property('customFields');
expect(res.body.contact.customFields).to.have.property(customFieldName, 'test_address');
expect(res.body.contact.customFields).to.have.property(customFieldName2, 'test_address2');
expect(res.body.contact.customFields).to.have.property(customFieldName3, 'test_address3');
});
});
it('should mark a conflict on a contact custom fields when overwrite is true and visitor already has the custom field set', async () => {
await request
.post(api('livechat/custom.fields'))
.send({
token: visitor.token,
customFields: [{ key: customFieldName, value: 'test_address_conflict', overwrite: false }],
})
.expect(200);
await request
.get(api(`omnichannel/contacts.get`))
.set(credentials)
.query({ contactId: visitorRoom.contactId })
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('contact');
expect(res.body.contact).to.have.property('customFields');
expect(res.body.contact.customFields).to.have.property(customFieldName, 'test_address');
expect(res.body.contact.customFields).to.have.property(customFieldName2, 'test_address2');
expect(res.body.contact.customFields).to.have.property(customFieldName3, 'test_address3');
expect(res.body.contact).to.have.property('conflictingFields').that.is.an('array');
expect(res.body.contact.conflictingFields[0]).to.deep.equal({
field: `customFields.${customFieldName}`,
value: 'test_address_conflict',
});
});
});
it('should overwrite the contact custom field when overwrite is true', async () => {
await request
.post(api('livechat/custom.fields'))
.send({
token: visitor.token,
customFields: [{ key: customFieldName2, value: 'test_new_add', overwrite: true }],
})
.expect(200);
await request
.get(api(`omnichannel/contacts.get`))
.set(credentials)
.query({ contactId: visitorRoom.contactId })
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('contact');
expect(res.body.contact).to.have.property('customFields');
expect(res.body.contact.customFields).to.have.property(customFieldName, 'test_address');
expect(res.body.contact.customFields).to.have.property(customFieldName2, 'test_new_add');
expect(res.body.contact.customFields).to.have.property(customFieldName3, 'test_address3');
expect(res.body.contact).to.have.property('conflictingFields').that.is.an('array');
expect(res.body.contact.conflictingFields[0]).to.deep.equal({
field: `customFields.${customFieldName}`,
value: 'test_address_conflict',
});
});
});
describe('with a room', () => {
let visitor: ILivechatVisitor;
let room: IOmnichannelRoom;
before(async () => {
visitor = await createVisitor();
room = await createLivechatRoom(visitor.token);
});
after(async () => {
await closeOmnichannelRoom(room._id);
await deleteVisitor(visitor.token);
});
it('should save both room & vistor custom fields on one call', async () => {
await request
.post(api('livechat/custom.fields'))
.send({
token: visitor.token,
customFields: [
{ key: customFieldName, value: 'test_address', overwrite: true },
{ key: roomCustomField, value: 'test_address2', overwrite: true },
{ key: customFieldName3, value: 'test_address3', overwrite: true },
],
})
.expect(200);
await request
.get(api(`omnichannel/contacts.get`))
.set(credentials)
.query({ contactId: visitorRoom.contactId })
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('contact');
expect(res.body.contact).to.have.property('customFields');
expect(res.body.contact.customFields).to.have.property(customFieldName, 'test_address');
expect(res.body.contact.customFields).to.have.property(customFieldName3, 'test_address3');
});
const roomInfo = await getLivechatRoomInfo(room._id);
expect(roomInfo).to.have.property('livechatData').that.is.an('object');
expect(roomInfo.livechatData).to.have.property(roomCustomField, 'test_address2');
});
it('should ignore a room custom field when room already has a value for it and overwrite is false', async () => {
await request
.post(api('livechat/custom.fields'))
.send({
token: visitor.token,
customFields: [{ key: roomCustomField, value: 'test_value_overriden', overwrite: false }],
})
.expect(200);
const roomInfo = await getLivechatRoomInfo(room._id);
expect(roomInfo).to.have.property('livechatData').that.is.an('object');
expect(roomInfo.livechatData).to.have.property(roomCustomField, 'test_address2');
});
});
});
describe('livechat/custom.field [with Contacts]', () => {
let visitor: ILivechatVisitor;
let contactId: string;
let room: IOmnichannelRoom;
const customFieldName = `custom_field_${Date.now()}`;
const customFieldValue = 'custom-field-value';
before(async () => {
await updatePermission('create-livechat-contact', ['admin']);
await updatePermission('view-livechat-contact', ['admin']);
await updateSetting('Livechat_accept_chats_with_no_agents', true);
// Create a Visitor
visitor = await createVisitor();
// Create a Contact and store id on var
await request
.post(api('omnichannel/contacts'))
.set(credentials)
.send({
name: visitor.name,
emails: [visitor.visitorEmails?.[0].address],
phones: [visitor.phone?.[0].phoneNumber],
})
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('contactId');
contactId = res.body.contactId;
});
room = await createLivechatRoom(visitor.token);
// Create Custom Field
await createCustomField({
searchable: true,
field: customFieldName,
label: customFieldName,
defaultValue: 'test_default_address',
scope: 'visitor',
visibility: 'public',
regexp: '',
});
await createCustomField({
searchable: true,
field: `${customFieldName}_2`,
label: `${customFieldName}_2`,
defaultValue: 'test_default_address',
scope: 'visitor',
visibility: 'public',
regexp: '',
});
});
after(async () => {
// TODO: add clean up for contacts, visitors, etc
await Promise.all([
deleteCustomField(customFieldName),
deleteCustomField(`${customFieldName}_2`),
deleteCustomField(`${customFieldName}_3`),
deleteVisitor(visitor.token),
closeOmnichannelRoom(room._id),
updateSetting('Livechat_accept_chats_with_no_agents', false),
]);
});
it('should save the custom field on Contact when available', async () => {
// Save the custom field on Visitor/Contact
await request
.post(api('livechat/custom.field'))
.send({ token: visitor.token, key: customFieldName, value: customFieldValue, overwrite: true })
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
});
// Fetch the visitor to validate custom fields are properly set.
await request
.get(api(`livechat/visitor/${visitor.token}`))
.set(credentials)
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body.visitor).to.have.property('livechatData');
expect(res.body.visitor.livechatData).to.have.property(customFieldName, customFieldValue);
});
// Fetch the visitor's contact to validate custom fields are properly set.
await request
.get(api(`omnichannel/contacts.get`))
.set(credentials)
.query({ contactId })
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('contact');
expect(res.body.contact).to.have.property('customFields');
expect(res.body.contact.customFields).to.have.property(customFieldName, customFieldValue);
});
});
it('Should save multiple custom fields on Contact when available', async () => {
// Save the custom field on Visitor/Contact
await request
.post(api('livechat/custom.field'))
.send({ token: visitor.token, key: customFieldName, value: customFieldValue, overwrite: true })
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
});
await request
.post(api('livechat/custom.field'))
.send({ token: visitor.token, key: `${customFieldName}_2`, value: customFieldValue, overwrite: true })
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
});
// Fetch the visitor to validate custom fields are properly set.
await request
.get(api(`livechat/visitor/${visitor.token}`))
.set(credentials)
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body.visitor).to.have.property('livechatData');
expect(res.body.visitor.livechatData).to.have.property(customFieldName, customFieldValue);
expect(res.body.visitor.livechatData).to.have.property(`${customFieldName}_2`, customFieldValue);
});
// Fetch the visitor's contact to validate custom fields are properly set.
await request
.get(api(`omnichannel/contacts.get`))
.set(credentials)
.query({ contactId })
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('contact');
expect(res.body.contact).to.have.property('customFields');
expect(res.body.contact.customFields).to.have.property(customFieldName, customFieldValue);
expect(res.body.contact.customFields).to.have.property(`${customFieldName}_2`, customFieldValue);
});
});
it('should add the custom field as conflict on Contact when overwrite is false', async () => {
const conflictingFieldValue = 'conflicting-custom-field-value';
// Save the custom field on Contact
await request
.post(api('livechat/custom.field'))
.send({ token: visitor.token, key: customFieldName, value: conflictingFieldValue, overwrite: false })
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
});
// Fetch the visitor to validate custom fields are properly set.
await request
.get(api(`livechat/visitor/${visitor.token}`))
.set(credentials)
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body.visitor).to.have.property('livechatData');
expect(res.body.visitor.livechatData).to.have.property(customFieldName, customFieldValue);
});
// Fetch the visitor's contact to validate custom fields are properly set.
await request
.get(api(`omnichannel/contacts.get`))
.set(credentials)
.query({ contactId })
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('contact');
expect(res.body.contact).to.have.property('customFields');
expect(res.body.contact.customFields).to.have.property(customFieldName, customFieldValue);
// Validate custom fields contain both entries, indicating conflict criteria
expect(res.body.contact.conflictingFields).to.have.lengthOf(1);
expect(res.body.contact.conflictingFields[0]).to.have.property('field', `customFields.${customFieldName}`);
expect(res.body.contact.conflictingFields[0]).to.have.property('value', conflictingFieldValue);
});
});
it('should not save the custom field as a conflict on Contact when overwrite is false but custom field is not registered yet', async () => {
await createCustomField({
searchable: true,
field: `${customFieldName}_3`,
label: `${customFieldName}_3`,
defaultValue: 'test_default_address',
scope: 'visitor',
visibility: 'public',
regexp: '',
});
// Save the custom field on Contact
await request
.post(api('livechat/custom.field'))
.send({ token: visitor.token, key: `${customFieldName}_3`, value: customFieldValue, overwrite: false })
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
});
// Fetch the visitor to validate custom fields are properly set.
await request
.get(api(`livechat/visitor/${visitor.token}`))
.set(credentials)
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body.visitor).to.have.property('livechatData');
expect(res.body.visitor.livechatData).to.have.property(`${customFieldName}_3`, customFieldValue);
});
// Fetch the visitor's contact to validate custom fields are properly set.
await request
.get(api(`omnichannel/contacts.get`))
.set(credentials)
.query({ contactId })
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('contact');
expect(res.body.contact).to.have.property('customFields');
expect(res.body.contact.customFields).to.have.property(`${customFieldName}_3`, customFieldValue);
// Validate custom fields contain both entries, indicating conflict criteria
expect(res.body.contact.conflictingFields).to.have.lengthOf(1);
expect(res.body.contact.conflictingFields[0]).to.not.have.property('field', `customFields.${customFieldName}_3`);
});
});
describe('conflictingField not nullish', () => {
let room: IOmnichannelRoom;
let visitor: ILivechatVisitor;
let contactId2: string | undefined;
before(async () => {
visitor = await createVisitor();
await request
.post(api('omnichannel/contacts'))
.set(credentials)
.send({
name: visitor.name,
emails: [visitor.visitorEmails?.[0].address],
phones: [visitor.phone?.[0].phoneNumber],
})
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('contactId');
contactId2 = res.body.contactId;
});
room = await createLivechatRoom(visitor.token);
});
after(async () => {
await closeOmnichannelRoom(room._id);
await deleteVisitor(visitor.token);
});
it('should not save contact conflictingFields as nullish if not modified', async () => {
await request
.post(api('livechat/custom.field'))
.send({ token: visitor.token, key: `${customFieldName}`, value: customFieldValue, overwrite: true })
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
});
// Fetch the visitor's contact to validate custom fields are properly set.
await request
.get(api(`omnichannel/contacts.get`))
.set(credentials)
.query({ contactId: contactId2 })
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('contact');
expect(res.body.contact).to.have.property('customFields');
expect(res.body.contact.customFields).to.have.property(`${customFieldName}`, customFieldValue);
// Validate conflictingFields was not saved as null
expect(res.body.contact).to.not.have.property('conflictingFields');
});
});
it('should not save contact conflictingFields as nullish if not modified (through visitor update)', async () => {
await request
.post(api('livechat/visitor'))
.set(credentials)
.send({
visitor: {
token: visitor.token,
customFields: [{ key: `${customFieldName}`, value: customFieldValue, overwrite: true }],
},
});
// Fetch the visitor's contact to validate custom fields are properly set.
await request
.get(api(`omnichannel/contacts.get`))
.set(credentials)
.query({ contactId: contactId2 })
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('contact');
expect(res.body.contact).to.have.property('customFields');
expect(res.body.contact.customFields).to.have.property(`${customFieldName}`, customFieldValue);
// Validate conflictingFields was not saved as null
expect(res.body.contact).to.not.have.property('conflictingFields');
});
});
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,611 @@
import type { Credentials } from '@rocket.chat/api-client';
import type { ILivechatDepartment, ILivechatInquiryRecord, ILivechatVisitor, IOmnichannelRoom, IUser } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { before, describe, it, after } from 'mocha';
import type { Response } from 'supertest';
import { getCredentials, api, request, credentials } from '../../../data/api-data';
import { deleteDepartment } from '../../../data/livechat/department';
import {
closeOmnichannelRoom,
createAgent,
createDepartment,
createLivechatRoom,
createVisitor,
deleteVisitor,
fetchInquiry,
getLivechatRoomInfo,
makeAgentAvailable,
startANewLivechatRoomAndTakeIt,
takeInquiry,
} from '../../../data/livechat/rooms';
import {
removePermissionFromAllRoles,
restorePermissionToRoles,
updateEESetting,
updatePermission,
updateSetting,
} from '../../../data/permissions.helper';
import { password } from '../../../data/user';
import { createUser, login, deleteUser } from '../../../data/users.helper';
import { IS_EE } from '../../../e2e/config/constants';
import { retry } from '../helpers/retry';
describe('LIVECHAT - inquiries', () => {
before((done) => getCredentials(done));
before(async () => {
await updateSetting('Livechat_enabled', true);
await updateSetting('Livechat_Routing_Method', 'Manual_Selection');
await updateEESetting('Livechat_Require_Contact_Verification', 'never');
});
describe('livechat/inquiries.list', () => {
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await updatePermission('view-livechat-manager', []);
await request.get(api('livechat/inquiries.list')).set(credentials).expect('Content-Type', 'application/json').expect(403);
});
it('should return an array of inquiries', async () => {
await updatePermission('view-livechat-manager', ['admin']);
await request
.get(api('livechat/inquiries.list'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body.inquiries).to.be.an('array');
expect(res.body).to.have.property('offset');
expect(res.body).to.have.property('total');
expect(res.body).to.have.property('count');
});
});
});
describe('livechat/inquiries.getOne', () => {
let room: IOmnichannelRoom;
let visitor: ILivechatVisitor;
before(async () => {
await createAgent();
await makeAgentAvailable();
visitor = await createVisitor();
room = await createLivechatRoom(visitor.token);
});
after(async () => {
await closeOmnichannelRoom(room._id);
await deleteVisitor(visitor.token);
});
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await updatePermission('view-l-room', []);
await request
.get(api('livechat/inquiries.getOne'))
.query({ roomId: 'room-id' })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(403);
});
it('should return a inquiry', async () => {
await updatePermission('view-l-room', ['admin']);
await request
.get(api('livechat/inquiries.getOne'))
.query({ roomId: 'room-id' })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('inquiry');
});
});
it('should get an inquiry by room id', async () => {
const inquiry = await fetchInquiry(room._id);
await request
.get(api(`livechat/inquiries.getOne`))
.query({ roomId: room._id })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('inquiry');
expect(res.body.inquiry).to.have.property('_id', inquiry._id);
expect(res.body.inquiry).to.have.property('rid', room._id);
expect(res.body.inquiry).to.have.property('ts');
expect(res.body.inquiry.ts).to.be.a('string');
expect(res.body.inquiry).to.have.property('status', 'queued');
expect(res.body.inquiry).to.have.property('name', visitor.name);
expect(res.body.inquiry).to.have.property('t', 'l');
expect(res.body.inquiry).to.have.property('priorityWeight');
expect(res.body.inquiry).to.have.property('estimatedWaitingTimeQueue');
expect(res.body.inquiry.source).to.have.property('type', 'api');
expect(res.body.inquiry).to.have.property('_updatedAt');
expect(res.body.inquiry).to.have.property('v').and.be.an('object');
expect(res.body.inquiry.v).to.have.property('_id', visitor._id);
});
});
});
describe('POST livechat/inquiries.take', () => {
let takenRoom: IOmnichannelRoom;
let servedByRoom: IOmnichannelRoom;
let visitor: ILivechatVisitor;
let visitor2: ILivechatVisitor;
before(async () => {
visitor = await createVisitor();
visitor2 = await createVisitor();
takenRoom = await createLivechatRoom(visitor.token);
servedByRoom = await createLivechatRoom(visitor2.token);
});
after(async () => {
await closeOmnichannelRoom(takenRoom._id);
await closeOmnichannelRoom(servedByRoom._id);
await deleteVisitor(visitor.token);
await deleteVisitor(visitor2.token);
});
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await updatePermission('view-l-room', []);
await request
.post(api('livechat/inquiries.take'))
.set(credentials)
.send({ inquiryId: 'room-id' })
.expect('Content-Type', 'application/json')
.expect(403);
});
it('should throw an error when userId is provided but is invalid', async () => {
await updatePermission('view-l-room', ['admin', 'livechat-agent']);
await request
.post(api('livechat/inquiries.take'))
.set(credentials)
.send({ inquiryId: 'room-id', userId: 'invalid-user-id' })
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
});
});
it('should throw an error if inquiryId is not an string', async () => {
await updatePermission('view-l-room', ['admin', 'livechat-agent']);
await request
.post(api('livechat/inquiries.take'))
.set(credentials)
.send({ inquiryId: { regexxxx: 'bla' }, userId: 'user-id' })
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
});
});
it('should take an inquiry if all params are good', async () => {
await updatePermission('view-l-room', ['admin', 'livechat-agent']);
const agent = await createAgent();
await makeAgentAvailable();
const inquiry = await fetchInquiry(takenRoom._id);
await request
.post(api('livechat/inquiries.take'))
.set(credentials)
.send({
inquiryId: inquiry._id,
userId: agent._id,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
});
const inquiry2 = (await fetchInquiry(takenRoom._id)) as ILivechatInquiryRecord;
expect(inquiry2.source?.type).to.equal('api');
expect(inquiry2.status).to.equal('taken');
});
it('should mark a taken room as servedBy me', async () => {
const agent = await createAgent();
await makeAgentAvailable();
const inquiry = await fetchInquiry(servedByRoom._id);
await request
.post(api('livechat/inquiries.take'))
.set(credentials)
.send({
inquiryId: inquiry._id,
userId: agent._id,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
});
const roomInfo = await getLivechatRoomInfo(servedByRoom._id);
expect(roomInfo).to.have.property('servedBy').that.is.an('object');
expect(roomInfo.servedBy).to.have.property('_id', 'rocketchat.internal.admin.test');
});
});
describe('livechat/inquiries.queuedForUser', () => {
let testUser: { user: IUser; credentials: { [key: string]: string } };
const departmentsToDelete: ILivechatDepartment[] = [];
const visitorsToDelete: ILivechatVisitor[] = [];
const roomsToClose: IOmnichannelRoom[] = [];
before(async () => {
await updateSetting('Livechat_accept_chats_with_no_agents', true);
await updateSetting('Omnichannel_enable_department_removal', true);
const user = await createUser();
await createAgent(user.username);
const credentials2 = await login(user.username, password);
await makeAgentAvailable(credentials2);
testUser = {
user,
credentials: credentials2,
};
});
after(async () => {
await Promise.allSettled(roomsToClose.map((room) => closeOmnichannelRoom(room._id)));
await Promise.allSettled(visitorsToDelete.map((visitor) => deleteVisitor(visitor.token)));
await Promise.allSettled(departmentsToDelete.map((dep) => deleteDepartment(dep._id)));
await updateSetting('Livechat_accept_chats_with_no_agents', false);
await updateSetting('Omnichannel_enable_department_removal', false);
await deleteUser(testUser.user);
});
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await updatePermission('view-l-room', []);
await request.get(api('livechat/inquiries.queuedForUser')).set(credentials).expect('Content-Type', 'application/json').expect(403);
});
it('should return an array of inquiries', async () => {
await restorePermissionToRoles('view-l-room');
await request
.get(api('livechat/inquiries.queuedForUser'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body.inquiries).to.be.an('array');
expect(res.body).to.have.property('offset');
expect(res.body).to.have.property('total');
expect(res.body).to.have.property('count');
});
});
it('should validate all returned inquiries are queued', async () => {
await request
.get(api('livechat/inquiries.queuedForUser'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect(async (res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body.inquiries).to.be.an('array');
for (const inquiry of res.body.inquiries) {
expect(inquiry).to.have.property('status', 'queued');
}
});
});
it('should return only public inquiries for a user with no departments', async () => {
const { body } = await request
.get(api('livechat/inquiries.queuedForUser'))
.set(testUser.credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(body).to.have.property('success', true);
expect(body.inquiries).to.be.an('array');
for (const inq of body.inquiries) {
expect(inq).to.not.have.property('department');
expect(inq).to.have.property('status', 'queued');
}
});
(IS_EE ? it : it.skip)('should return inquiries of the same department as the user', async () => {
const dep = await createDepartment([{ agentId: testUser.user._id }]);
departmentsToDelete.push(dep);
const visitor = await createVisitor(dep._id);
visitorsToDelete.push(visitor);
const room = await createLivechatRoom(visitor.token);
roomsToClose.push(room);
const { body } = await request
.get(api('livechat/inquiries.queuedForUser'))
.set(testUser.credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(body).to.have.property('success', true);
expect(body.inquiries).to.be.an('array');
const depInq = body.inquiries.filter((inq: { department: string }) => inq.department === dep._id);
expect(depInq.length).to.be.equal(1);
});
(IS_EE ? it : it.skip)('should not return an inquiry of a department the user is not part of', async () => {
const dep = await createDepartment();
departmentsToDelete.push(dep);
const visitor = await createVisitor(dep._id);
visitorsToDelete.push(visitor);
const room = await createLivechatRoom(visitor.token);
roomsToClose.push(room);
const { body } = await request
.get(api('livechat/inquiries.queuedForUser'))
.set(testUser.credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(body).to.have.property('success', true);
expect(body.inquiries).to.be.an('array');
const depInq = body.inquiries.filter((inq: { department: string }) => inq.department === dep._id);
expect(depInq.length).to.be.equal(0);
});
});
describe('livechat/inquiries.returnAsInquiry', () => {
let testUser: { user: IUser; credentials: Credentials };
before(async () => {
const user = await createUser();
await createAgent(user.username);
const credentials2 = await login(user.username, password);
await makeAgentAvailable(credentials2);
testUser = {
user,
credentials: credentials2,
};
});
after(async () => {
await deleteUser(testUser.user);
});
it('should throw an error if user doesnt have view-l-room permission', async () => {
await removePermissionFromAllRoles('view-l-room');
const { body } = await request
.post(api('livechat/inquiries.returnAsInquiry'))
.set(credentials)
.send({ roomId: 'test' })
.expect('Content-Type', 'application/json')
.expect(403);
expect(body).to.have.property('success', false);
expect(body.error).to.have.equal('User does not have the permissions required for this action [error-unauthorized]');
});
it('should fail if provided room doesnt exists', async () => {
await restorePermissionToRoles('view-l-room');
const { body } = await request
.post(api('livechat/inquiries.returnAsInquiry'))
.set(credentials)
.send({
roomId: 'test',
})
.expect('Content-Type', 'application/json')
.expect(400);
expect(body).to.have.property('success', false);
expect(body).to.have.property('error', 'error-room-not-found');
});
it('should fail if room is not a livechat room', async () => {
const { body } = await request
.post(api('livechat/inquiries.returnAsInquiry'))
.set(credentials)
.send({ roomId: 'GENERAL' })
.expect('Content-Type', 'application/json')
.expect(400);
expect(body).to.have.property('success', false);
expect(body).to.have.property('error', 'error-room-not-found');
});
it('should fail if room is closed', async () => {
const visitor = await createVisitor();
const room = await createLivechatRoom(visitor.token);
await closeOmnichannelRoom(room._id);
const { body } = await request
.post(api('livechat/inquiries.returnAsInquiry'))
.set(credentials)
.send({ roomId: room._id })
.expect('Content-Type', 'application/json')
.expect(400);
expect(body).to.have.property('success', false);
expect(body).to.have.property('error', 'room-closed');
await deleteVisitor(visitor.token);
});
describe('no serving', () => {
let room: IOmnichannelRoom;
let visitor: ILivechatVisitor;
before(async () => {
visitor = await createVisitor();
room = await createLivechatRoom(visitor.token);
});
after(async () => {
await closeOmnichannelRoom(room._id);
await deleteVisitor(visitor.token);
});
it('should fail if no one is serving the room', async () => {
const { body } = await request
.post(api('livechat/inquiries.returnAsInquiry'))
.set(credentials)
.send({ roomId: room._id })
.expect('Content-Type', 'application/json')
.expect(200);
expect(body).to.have.property('success', true);
expect(body).to.have.property('result', false);
});
});
let inquiry: ILivechatInquiryRecord;
let room: IOmnichannelRoom;
let returnAsInquiryDep: ILivechatDepartment;
let returnAsInquiryVisitor: ILivechatVisitor;
(IS_EE ? it : it.skip)('should move a room back to queue', async () => {
await updateSetting('Omnichannel_enable_department_removal', true);
returnAsInquiryDep = await createDepartment([{ agentId: testUser.user._id }]);
returnAsInquiryVisitor = await createVisitor(returnAsInquiryDep._id);
room = await createLivechatRoom(returnAsInquiryVisitor.token);
const inq = await fetchInquiry(room._id);
inquiry = inq;
await takeInquiry(inq._id, testUser.credentials);
const { body } = await request
.post(api('livechat/inquiries.returnAsInquiry'))
.set(testUser.credentials)
.send({ roomId: room._id })
.expect('Content-Type', 'application/json')
.expect(200);
expect(body).to.have.property('success', true);
expect(body).to.have.property('result', true);
});
(IS_EE ? it : it.skip)('should appear on users queued elements', async () => {
const { body } = await request
.get(api('livechat/inquiries.queuedForUser'))
.set(testUser.credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(body.inquiries).to.be.an('array');
const depInq = body.inquiries.filter((inq: { _id: string }) => inq._id === inquiry._id);
expect(depInq.length).to.be.equal(1);
await closeOmnichannelRoom(room._id);
await deleteVisitor(returnAsInquiryVisitor.token);
await deleteDepartment(returnAsInquiryDep._id);
await updateSetting('Omnichannel_enable_department_removal', false);
});
});
describe('keep inquiry last message updated', () => {
let room: any;
let visitor: any;
let agent: any;
before(async () => {
agent = await createAgent();
visitor = await createVisitor();
await makeAgentAvailable();
room = await createLivechatRoom(visitor.token);
});
after(async () => {
await closeOmnichannelRoom(room._id);
await deleteVisitor(visitor.token);
});
it('should update inquiry last message', async () => {
const msgText = `update inquiry ${Date.now()}`;
await request.post(api('livechat/message')).send({ token: visitor.token, rid: room._id, msg: msgText }).expect(200);
const inquiry = await fetchInquiry(room._id);
expect(inquiry).to.have.property('_id', inquiry._id);
expect(inquiry).to.have.property('rid', room._id);
expect(inquiry).to.have.property('lastMessage');
expect(inquiry.lastMessage).to.have.property('msg', msgText);
});
it('should update room last message after inquiry is taken', async () => {
const msgText = `update room ${Date.now()}`;
const inquiry = await fetchInquiry(room._id);
await request
.post(api('livechat/inquiries.take'))
.set(credentials)
.send({
inquiryId: inquiry._id,
userId: agent._id,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
});
await request.post(api('livechat/message')).send({ token: visitor.token, rid: room._id, msg: msgText }).expect(200);
// check room
const roomInfo = await getLivechatRoomInfo(room._id);
expect(roomInfo).to.have.property('lastMessage');
expect(roomInfo.lastMessage).to.have.property('msg', msgText);
});
it('should have the correct last message when room is returned to queue', async () => {
const msgText = `return to queue ${Date.now()}`;
await request.post(api('livechat/message')).send({ token: visitor.token, rid: room._id, msg: msgText }).expect(200);
await request
.post(api('livechat/inquiries.returnAsInquiry'))
.set(credentials)
.send({ roomId: room._id })
.expect('Content-Type', 'application/json')
.expect(200);
const inquiry = await fetchInquiry(room._id);
expect(inquiry).to.have.property('_id', inquiry._id);
expect(inquiry).to.have.property('rid', room._id);
expect(inquiry).to.have.property('lastMessage');
expect(inquiry.lastMessage).to.have.property('msg', msgText);
});
});
(IS_EE ? describe : describe.skip)('Auto Transfer Scheduler - Manual_Selection', () => {
let testRoom: IOmnichannelRoom;
let testDepartment: ILivechatDepartment;
let testVisitor: ILivechatVisitor;
before(async () => {
// seconds
await Promise.all([
updateSetting('Livechat_auto_transfer_chat_timeout', 3),
createAgent(),
updateSetting('Omnichannel_enable_department_removal', true),
]);
testDepartment = await createDepartment([{ agentId: 'rocketchat.internal.admin.test' }]);
});
after(async () => {
await deleteVisitor(testVisitor.token);
await deleteDepartment(testDepartment._id);
await Promise.all([
updateSetting('Livechat_auto_transfer_chat_timeout', 0),
updateSetting('Omnichannel_enable_department_removal', false),
]);
});
it('should create a room and schedule it for transfer', async () => {
const { room, visitor } = await startANewLivechatRoomAndTakeIt({ departmentId: testDepartment._id });
testVisitor = visitor;
// The room returned is not updated :(
const updatedRoom = await getLivechatRoomInfo(room._id);
expect(updatedRoom).to.have.property('servedBy').that.is.an('object');
testRoom = updatedRoom;
});
it('should return a chat to the queue when not answered after 3 seconds', async () => {
await retry(
`The agenda has a 'heartbeat' of 3 seconds (for testing environment), so its likely that the first fetch will not have the room updated`,
async () => {
const inquiry = await fetchInquiry(testRoom._id);
const room = await getLivechatRoomInfo(testRoom._id);
expect(room).to.not.have.property('servedBy');
expect(inquiry).to.have.property('status', 'queued');
await closeOmnichannelRoom(testRoom._id);
},
{
delayMs: 3000,
},
);
});
});
});

View File

@@ -0,0 +1,300 @@
import type { ILivechatVisitor, IOmnichannelRoom, ISetting } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import { getCredentials, api, request, credentials } from '../../../data/api-data';
import {
closeOmnichannelRoom,
createAgent,
createLivechatRoom,
createVisitor,
deleteVisitor,
getLivechatRoomInfo,
startANewLivechatRoomAndTakeIt,
} from '../../../data/livechat/rooms';
import { sleep } from '../../../data/livechat/utils';
import { getLivechatVisitorByToken } from '../../../data/livechat/visitor';
import { updatePermission, updateSetting } from '../../../data/permissions.helper';
describe('LIVECHAT - Integrations', () => {
before((done) => getCredentials(done));
before(async () => {
await updateSetting('Livechat_enabled', true);
});
describe('livechat/integrations.settings', () => {
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await updatePermission('view-livechat-manager', []);
await request.get(api('livechat/integrations.settings')).set(credentials).expect('Content-Type', 'application/json').expect(403);
});
it('should return an array of settings', (done) => {
updatePermission('view-livechat-manager', ['admin'])
.then(async () => {
const res = await request
.get(api('livechat/integrations.settings'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body.settings).to.be.an('array');
const settingIds = res.body.settings.map((setting: ISetting) => setting._id);
expect(settingIds).to.include.members([
'Livechat_webhookUrl',
'Livechat_secret_token',
'Livechat_http_timeout',
'Livechat_webhook_on_start',
'Livechat_webhook_on_close',
'Livechat_webhook_on_chat_taken',
'Livechat_webhook_on_chat_queued',
'Livechat_webhook_on_forward',
'Livechat_webhook_on_offline_msg',
'Livechat_webhook_on_visitor_message',
'Livechat_webhook_on_agent_message',
]);
})
.then(done)
.catch(done);
});
});
describe('Incoming SMS', () => {
let smsVisitor: ILivechatVisitor;
let smsRoom: string;
before(async () => {
await updateSetting('SMS_Enabled', true);
await updateSetting('SMS_Service', '');
});
after(async () => {
await closeOmnichannelRoom(smsRoom);
await deleteVisitor(smsVisitor.token);
await updateSetting('SMS_Default_Omnichannel_Department', '');
await updateSetting('SMS_Service', 'twilio');
});
// Twilio sends the body as a x-www-form-urlencoded, the tests should do the same
describe('POST livechat/sms-incoming/:service', () => {
it('should throw an error if SMS is disabled', async () => {
await updateSetting('SMS_Enabled', false);
await request
.post(api('livechat/sms-incoming/twilio'))
.set(credentials)
.send('from=%2B123456789&body=Hello')
.expect('Content-Type', 'application/json')
.expect(400);
});
it('should return an error when SMS service is not configured', async () => {
await updateSetting('SMS_Enabled', true);
await request
.post(api('livechat/sms-incoming/twilio'))
.set(credentials)
.send('From=%2B123456789&To=%2B123456789&Body=Hello')
.expect('Content-Type', 'application/json')
.expect(400);
});
it('should throw an error if SMS_Default_Omnichannel_Department does not exists', async () => {
await updateSetting('SMS_Default_Omnichannel_Department', '123');
await request
.post(api('livechat/sms-incoming/twilio'))
.set(credentials)
.send('From=%2B123456789&To=%2B123456789&Body=Hello')
.expect('Content-Type', 'application/json')
.expect(400);
});
it('should return headers and <Response> as body on success', async () => {
await updateSetting('SMS_Default_Omnichannel_Department', '');
await updateSetting('SMS_Service', 'twilio');
// Create visitor with the phone number that will be used in SMS
smsVisitor = await createVisitor(undefined, 'sms-visitor', undefined, '+12345678910');
const response = await request
.post(api('livechat/sms-incoming/twilio'))
.set(credentials)
.send('From=%2B12345678910&To=%2B123456789&Body=Hello')
.expect('Content-Type', 'text/xml')
.expect(200);
expect(response).to.have.property('text', '<Response></Response>');
const lastChat = (await getLivechatVisitorByToken(smsVisitor.token)).lastChat?._id;
expect(lastChat).to.exist;
smsRoom = lastChat as string;
});
});
});
describe('Livechat - Webhooks', () => {
const webhookUrl = process.env.WEBHOOK_TEST_URL || 'https://httpbin.org';
describe('livechat/webhook.test', () => {
it('should fail when user doesnt have view-livechat-webhooks permission', async () => {
await updatePermission('view-livechat-webhooks', []);
const response = await request.post(api('livechat/webhook.test')).set(credentials).expect(403);
expect(response.body).to.have.property('success', false);
});
it('should fail if setting Livechat_webhookUrl is not set', async () => {
await updateSetting('Livechat_webhookUrl', '');
await updatePermission('view-livechat-webhooks', ['admin', 'livechat-manager']);
const response = await request.post(api('livechat/webhook.test')).set(credentials).expect(400);
expect(response.body).to.have.property('success', false);
});
it('should return true if webhook test went good', async () => {
await updateSetting('Livechat_webhookUrl', `${webhookUrl}/status/200`);
const response = await request.post(api('livechat/webhook.test')).set(credentials).expect(200);
expect(response.body.success).to.be.true;
});
it('should fail if webhook test went bad', async () => {
await updateSetting('Livechat_webhookUrl', `${webhookUrl}/status/400`);
await request.post(api('livechat/webhook.test')).set(credentials).expect(400);
});
});
describe('Webhook notifications', () => {
const visitorsToDelete: ILivechatVisitor[] = [];
const roomsToClose: IOmnichannelRoom[] = [];
before(async () => {
await updateSetting('Livechat_webhookUrl', `${webhookUrl}/anything`);
await updateSetting('Livechat_Routing_Method', 'Manual_Selection');
await createAgent();
});
after(async () => {
await Promise.allSettled(roomsToClose.map((room) => closeOmnichannelRoom(room._id)));
await Promise.allSettled(visitorsToDelete.map((visitor) => deleteVisitor(visitor.token)));
await updateSetting('Livechat_webhookUrl', '');
await updateSetting('Livechat_Routing_Method', 'Auto_Selection');
await updateSetting('Livechat_webhook_on_start', false);
await updateSetting('Livechat_webhook_on_close', false);
await updateSetting('Livechat_webhook_on_chat_taken', false);
await updateSetting('Livechat_webhook_on_chat_queued', false);
});
it('should send a notification on chat start', async () => {
await updateSetting('Livechat_webhook_on_start', true);
const { room, visitor } = await startANewLivechatRoomAndTakeIt();
roomsToClose.push(room);
visitorsToDelete.push(visitor);
await sleep(1000);
const roomInfo = await getLivechatRoomInfo(room._id);
expect(roomInfo.crmData).to.be.an('string');
expect(JSON.parse(roomInfo.crmData as string))
.to.have.property('json')
.that.has.property('type', 'LivechatSessionStart');
await updateSetting('Livechat_webhook_on_start', false);
});
it('should send a notification on chat taken', async () => {
await updateSetting('Livechat_webhook_on_chat_taken', true);
const { room, visitor } = await startANewLivechatRoomAndTakeIt();
roomsToClose.push(room);
visitorsToDelete.push(visitor);
await sleep(1000);
const roomInfo = await getLivechatRoomInfo(room._id);
expect(roomInfo.crmData).to.be.an('string');
expect(JSON.parse(roomInfo.crmData as string))
.to.have.property('json')
.that.has.property('type', 'LivechatSessionTaken');
await updateSetting('Livechat_webhook_on_chat_taken', false);
});
let queuedRoom: IOmnichannelRoom;
let queuedVisitor: ILivechatVisitor;
it('should send a notification on chat queued', async () => {
await updateSetting('Livechat_webhook_on_chat_queued', true);
queuedVisitor = await createVisitor();
queuedRoom = await createLivechatRoom(queuedVisitor.token);
roomsToClose.push(queuedRoom);
visitorsToDelete.push(queuedVisitor);
await sleep(1000);
const roomInfo = await getLivechatRoomInfo(queuedRoom._id);
expect(roomInfo.crmData).to.be.an('string');
expect(JSON.parse(roomInfo.crmData as string))
.to.have.property('json')
.that.has.property('type', 'LivechatSessionQueued');
await updateSetting('Livechat_webhook_on_chat_queued', false);
});
it('should send a notification on chat close', async () => {
await updateSetting('Livechat_webhook_on_close', true);
await closeOmnichannelRoom(queuedRoom._id);
const idx = roomsToClose.indexOf(queuedRoom);
if (idx > -1) {
roomsToClose.splice(idx, 1);
}
await sleep(1000);
const roomInfo = await getLivechatRoomInfo(queuedRoom._id);
expect(roomInfo.crmData).to.be.an('string');
expect(JSON.parse(roomInfo.crmData as string))
.to.have.property('json')
.that.has.property('type', 'LivechatSession');
await updateSetting('Livechat_webhook_on_close', false);
});
});
});
describe('omnichannel/integrations', () => {
describe('POST', () => {
it('should update the integration settings if the required parameters are provided', async () => {
const response = await request
.post(api('omnichannel/integrations'))
.set(credentials)
.send({
LivechatWebhookUrl: 'http://localhost:8080',
LivechatSecretToken: 'asdfasdf',
LivechatHttpTimeout: 3000,
LivechatWebhookOnStart: false,
LivechatWebhookOnClose: false,
LivechatWebhookOnChatTaken: false,
LivechatWebhookOnChatQueued: false,
LivechatWebhookOnForward: false,
LivechatWebhookOnOfflineMsg: false,
LivechatWebhookOnVisitorMessage: false,
LivechatWebhookOnAgentMessage: false,
})
.expect(200);
expect(response.body).to.have.property('success', true);
});
it('should fail if a wrong type is provided', async () => {
const response = await request
.post(api('omnichannel/integrations'))
.set(credentials)
.send({
LivechatWebhookUrl: 8000,
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(response.body).to.have.property('success', true);
});
it('should fail if a wrong setting is provided', async () => {
const response = await request
.post(api('omnichannel/integrations'))
.set(credentials)
.send({
LivechatWebhook_url: 'http://localhost:8000',
})
.expect(400);
expect(response.body).to.have.property('success', false);
});
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,426 @@
import { expect } from 'chai';
import { before, describe, it, after } from 'mocha';
import type { Response } from 'supertest';
import { getCredentials, api, request, credentials } from '../../../data/api-data';
import { createTrigger, fetchTriggers } from '../../../data/livechat/triggers';
import { removePermissionFromAllRoles, restorePermissionToRoles, updatePermission, updateSetting } from '../../../data/permissions.helper';
import { IS_EE } from '../../../e2e/config/constants';
describe('LIVECHAT - triggers', () => {
before((done) => getCredentials(done));
before(async () => {
await updateSetting('Livechat_enabled', true);
});
describe('livechat/triggers', () => {
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await removePermissionFromAllRoles('view-livechat-manager');
await request.get(api('livechat/triggers')).set(credentials).expect('Content-Type', 'application/json').expect(403);
});
it('should return an array of triggers', async () => {
await restorePermissionToRoles('view-livechat-manager');
await createTrigger(`test${Date.now()}`);
await request
.get(api('livechat/triggers'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body.triggers).to.be.an('array');
expect(res.body).to.have.property('count').to.be.greaterThan(0);
expect(res.body.triggers[0]).to.have.property('_id');
expect(res.body.triggers[0]).to.have.property('name');
expect(res.body.triggers[0]).to.have.property('description');
expect(res.body.triggers[0]).to.have.property('enabled', true);
expect(res.body.triggers[0]).to.have.property('runOnce').that.is.a('boolean');
expect(res.body.triggers[0]).to.have.property('conditions').that.is.an('array').with.lengthOf.greaterThan(0);
expect(res.body.triggers[0]).to.have.property('actions').that.is.an('array').with.lengthOf.greaterThan(0);
expect(res.body).to.have.property('offset');
expect(res.body).to.have.property('total');
});
});
});
describe('POST livechat/triggers', () => {
it('should fail if user is not logged in', async () => {
await request.post(api('livechat/triggers')).send({}).expect(401);
});
it('should fail if no data is sent', async () => {
await request.post(api('livechat/triggers')).set(credentials).send({}).expect(400);
});
it('should fail if invalid data is sent', async () => {
await request.post(api('livechat/triggers')).set(credentials).send({ name: 'test' }).expect(400);
});
it('should fail if name is not an string', async () => {
await request
.post(api('livechat/triggers'))
.set(credentials)
.send({ name: 1, description: 'test', enabled: true, runOnce: true, conditions: [], actions: [] })
.expect(400);
});
it('should fail if description is not an string', async () => {
await request
.post(api('livechat/triggers'))
.set(credentials)
.send({ name: 'test', description: 1, enabled: true, runOnce: true, conditions: [], actions: [] })
.expect(400);
});
it('should fail if enabled is not an boolean', async () => {
await request
.post(api('livechat/triggers'))
.set(credentials)
.send({ name: 'test', description: 'test', enabled: 1, runOnce: true, conditions: [], actions: [] })
.expect(400);
});
it('should fail if runOnce is not an boolean', async () => {
await request
.post(api('livechat/triggers'))
.set(credentials)
.send({ name: 'test', description: 'test', enabled: true, runOnce: 1, conditions: [], actions: [] })
.expect(400);
});
it('should fail if conditions is not an array', async () => {
await request
.post(api('livechat/triggers'))
.set(credentials)
.send({ name: 'test', description: 'test', enabled: true, runOnce: true, conditions: 1, actions: [] })
.expect(400);
});
it('should fail if actions is not an array', async () => {
await request
.post(api('livechat/triggers'))
.set(credentials)
.send({ name: 'test', description: 'test', enabled: true, runOnce: true, conditions: [], actions: 1 })
.expect(400);
});
it('should fail if conditions is an array with invalid data', async () => {
await request
.post(api('livechat/triggers'))
.set(credentials)
.send({ name: 'test', description: 'test', enabled: true, runOnce: true, conditions: [1], actions: [] })
.expect(400);
});
it('should fail if conditions is an array of objects, but name is not a valid value', async () => {
await request
.post(api('livechat/triggers'))
.set(credentials)
.send({ name: 'test', description: 'test', enabled: true, runOnce: true, conditions: [{ name: 'invalid' }], actions: [] })
.expect(400);
});
it('should fail if actions is an array of invalid values', async () => {
await request
.post(api('livechat/triggers'))
.set(credentials)
.send({ name: 'test', description: 'test', enabled: true, runOnce: true, conditions: [{ name: 'page-url' }], actions: [1] })
.expect(400);
});
it('should fail if actions is an array of objects, but name is not a valid value', async () => {
await request
.post(api('livechat/triggers'))
.set(credentials)
.send({
name: 'test',
description: 'test',
enabled: true,
runOnce: true,
conditions: [{ name: 'page-url', value: 'http://localhost:3000' }],
actions: [{ name: 'invalid' }],
})
.expect(400);
});
it('should fail if actions is an array of objects, but sender is not a valid value', async () => {
await request
.post(api('livechat/triggers'))
.set(credentials)
.send({
name: 'test',
description: 'test',
enabled: true,
runOnce: true,
conditions: [{ name: 'page-url' }],
actions: [{ name: 'send-message', params: { sender: 'invalid' } }],
})
.expect(400);
});
it('should fail if actions is an array of objects, but msg is not a valid value', async () => {
await request
.post(api('livechat/triggers'))
.set(credentials)
.send({
name: 'test',
description: 'test',
enabled: true,
runOnce: true,
conditions: [{ name: 'page-url', value: 'http://localhost:3000' }],
actions: [{ name: 'send-message', params: { sender: 'custom' } }],
})
.expect(400);
});
it('should fail if actions is an array of objects, but name is not a valid value', async () => {
await request
.post(api('livechat/triggers'))
.set(credentials)
.send({
name: 'test',
description: 'test',
enabled: true,
runOnce: true,
conditions: [{ name: 'page-url', value: 'http://localhost:3000' }],
actions: [{ name: 'send-message', params: { sender: 'custom', msg: 'test', name: {} } }],
})
.expect(400);
});
it('should fail if user doesnt have view-livechat-manager permission', async () => {
await removePermissionFromAllRoles('view-livechat-manager');
await request
.post(api('livechat/triggers'))
.set(credentials)
.send({
name: 'test',
description: 'test',
enabled: true,
runOnce: true,
conditions: [{ name: 'page-url', value: 'http://localhost:3000' }],
actions: [{ name: 'send-message', params: { sender: 'custom', msg: 'test', name: 'test' } }],
})
.expect(403);
});
it('should save a new trigger of type send-message', async () => {
await restorePermissionToRoles('view-livechat-manager');
await request
.post(api('livechat/triggers'))
.set(credentials)
.send({
name: 'test',
description: 'test',
enabled: true,
runOnce: true,
conditions: [{ name: 'page-url', value: 'http://localhost:3000' }],
actions: [{ name: 'send-message', params: { sender: 'custom', msg: 'test', name: 'test' } }],
})
.expect(200);
});
it('should fail if type is use-external-service but serviceUrl is not a present', async () => {
await request
.post(api('livechat/triggers'))
.set(credentials)
.send({
name: 'test2',
description: 'test2',
enabled: true,
runOnce: true,
conditions: [{ name: 'page-url', value: 'http://localhost:3000' }],
actions: [
{
name: 'use-external-service',
params: {
serviceTimeout: 5000,
serviceFallbackMessage: 'Were sorry, we cannot complete your request',
},
},
],
})
.expect(400);
});
it('should fail if type is use-external-service but serviceTimeout is not a present', async () => {
await request
.post(api('livechat/triggers'))
.set(credentials)
.send({
name: 'test2',
description: 'test2',
enabled: true,
runOnce: true,
conditions: [{ name: 'page-url', value: 'http://localhost:3000' }],
actions: [
{
name: 'use-external-service',
params: {
serviceUrl: 'http://localhost:3000/api/vX',
serviceFallbackMessage: 'Were sorry, we cannot complete your request',
},
},
],
})
.expect(400);
});
it('should fail if type is use-external-service but serviceFallbackMessage is not a present', async () => {
await request
.post(api('livechat/triggers'))
.set(credentials)
.send({
name: 'test2',
description: 'test2',
enabled: true,
runOnce: true,
conditions: [{ name: 'page-url', value: 'http://localhost:3000' }],
actions: [
{
name: 'use-external-service',
params: {
serviceUrl: 'http://localhost:3000/api/vX',
serviceTimeout: 5000,
},
},
],
})
.expect(400);
});
it('should save a new trigger of type use-external-service', async () => {
await request
.post(api('livechat/triggers'))
.set(credentials)
.send({
name: 'test3',
description: 'test3',
enabled: true,
runOnce: true,
conditions: [{ name: 'page-url', value: 'http://localhost:3000' }],
actions: [
{
name: 'use-external-service',
params: {
serviceUrl: 'http://localhost:3000/api/vX',
serviceTimeout: 5000,
serviceFallbackMessage: 'Were sorry, we cannot complete your request',
},
},
],
})
.expect(200);
});
});
(IS_EE ? describe : describe.skip)('POST livechat/triggers/external-service/test', () => {
const webhookUrl = process.env.WEBHOOK_TEST_URL || 'https://httpbin.org';
after(() => Promise.all([updateSetting('Livechat_secret_token', ''), restorePermissionToRoles('view-livechat-manager')]));
it('should fail if user is not logged in', async () => {
await request.post(api('livechat/triggers/external-service/test')).send({}).expect(401);
});
it('should fail if no data is sent', async () => {
await request.post(api('livechat/triggers/external-service/test')).set(credentials).send({}).expect(400);
});
it('should fail if invalid data is sent', async () => {
await request.post(api('livechat/triggers/external-service/test')).set(credentials).send({ webhookUrl: 'test' }).expect(400);
});
it('should fail if webhookUrl is not an string', async () => {
await request
.post(api('livechat/triggers/external-service/test'))
.set(credentials)
.send({ webhookUrl: 1, timeout: 1000, fallbackMessage: 'test', extraData: [] })
.expect(400);
});
it('should fail if timeout is not an number', async () => {
await request
.post(api('livechat/triggers/external-service/test'))
.set(credentials)
.send({ webhookUrl: 'test', timeout: '1000', fallbackMessage: 'test', extraData: [] })
.expect(400);
});
it('should fail if fallbackMessage is not an string', async () => {
await request
.post(api('livechat/triggers/external-service/test'))
.set(credentials)
.send({ webhookUrl: 'test', timeout: 1000, fallbackMessage: 1, extraData: [] })
.expect(400);
});
it('should fail if params is not an array', async () => {
await request
.post(api('livechat/triggers/external-service/test'))
.set(credentials)
.send({ webhookUrl: 'test', timeout: 1000, fallbackMessage: 'test', extraData: 1 })
.expect(400);
});
it('should fail if user doesnt have view-livechat-webhooks permission', async () => {
await removePermissionFromAllRoles('view-livechat-manager');
await request
.post(api('livechat/triggers/external-service/test'))
.set(credentials)
.send({ webhookUrl: 'test', timeout: 1000, fallbackMessage: 'test', extraData: [] })
.expect(403);
});
it('should fail if Livechat_secret_token setting is empty', async () => {
await restorePermissionToRoles('view-livechat-manager');
await updateSetting('Livechat_secret_token', '');
await request
.post(api('livechat/triggers/external-service/test'))
.set(credentials)
.send({ webhookUrl: 'test', timeout: 1000, fallbackMessage: 'test', extraData: [] })
.expect(400);
});
it('should return error when webhook returns error', async () => {
await updateSetting('Livechat_secret_token', 'test');
await request
.post(api('livechat/triggers/external-service/test'))
.set(credentials)
.send({ webhookUrl: `${webhookUrl}/status/500`, timeout: 5000, fallbackMessage: 'test', extraData: [] })
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'error-invalid-external-service-response');
expect(res.body).to.have.property('response').to.be.a('string');
});
});
it('should return error when webhook times out', async () => {
await request
.post(api('livechat/triggers/external-service/test'))
.set(credentials)
.send({ webhookUrl: `${webhookUrl}/delay/2`, timeout: 1000, fallbackMessage: 'test', extraData: [] })
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'error-timeout');
expect(res.body).to.have.property('response').to.be.a('string');
});
});
it('should fail when webhook returns an answer that doesnt match the format', async () => {
await request
.post(api('livechat/triggers/external-service/test'))
.set(credentials)
.send({ webhookUrl: `${webhookUrl}/anything`, timeout: 5000, fallbackMessage: 'test', extraData: [] })
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'error-invalid-external-service-response');
});
});
});
describe('livechat/triggers/:id', () => {
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await updatePermission('view-livechat-manager', []);
await request.get(api('livechat/triggers/invalid-id')).set(credentials).expect('Content-Type', 'application/json').expect(403);
await updatePermission('view-livechat-manager', ['admin']);
});
it('should return null when trigger does not exist', async () => {
await updatePermission('view-livechat-manager', ['admin']);
const response = await request
.get(api('livechat/triggers/invalid-id'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(response.body).to.have.property('success', true);
expect(response.body.trigger).to.be.null;
});
it('should return the trigger', async () => {
const triggerName = `test${Date.now()}`;
await createTrigger(triggerName);
const trigger = (await fetchTriggers()).find((t) => t.name === triggerName);
const response = await request
.get(api(`livechat/triggers/${trigger?._id}`))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(response.body).to.have.property('success', true);
expect(response.body.trigger).to.be.an('object');
expect(response.body.trigger).to.have.property('_id', trigger?._id);
});
});
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,215 @@
import type { IEmailInbox } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import type { Response } from 'supertest';
import { getCredentials, api, request, credentials } from '../../../data/api-data';
import { createEmailInbox } from '../../../data/livechat/inboxes';
import { updatePermission } from '../../../data/permissions.helper';
describe('Email inbox', () => {
before((done) => getCredentials(done));
let testInbox = '';
before(async () => {
await request
.post(api('email-inbox'))
.set(credentials)
.send({
active: true,
name: 'test-email-inbox##',
email: 'test-email@example.com',
description: 'test email inbox',
senderInfo: 'test email inbox',
smtp: {
server: 'smtp.example.com',
port: 587,
username: 'example@example.com',
password: 'not-a-real-password',
secure: true,
},
imap: {
server: 'imap.example.com',
port: 993,
username: 'example@example.com',
password: 'not-a-real-password',
secure: true,
maxRetries: 10,
},
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success');
if (res.body.success === true) {
testInbox = res.body._id;
} else {
expect(res.body).to.have.property('error');
expect(res.body.error.includes('E11000')).to.be.eq(true);
}
});
});
after(async () => {
if (testInbox) {
await request
.delete(api(`email-inbox/${testInbox}`))
.set(credentials)
.send()
.expect(200);
}
});
describe('GET email-inbox.list', () => {
it('should fail if user doesnt have manage-email-inbox permission', async () => {
await updatePermission('manage-email-inbox', []);
await request.get(api('email-inbox.list')).set(credentials).expect(403);
});
it('should return a list of email inboxes', async () => {
await updatePermission('manage-email-inbox', ['admin']);
const res = await request.get(api('email-inbox.list')).set(credentials).send().expect('Content-Type', 'application/json').expect(200);
expect(res.body).to.have.property('emailInboxes');
expect(res.body.emailInboxes).to.be.an('array');
expect(res.body.emailInboxes).to.have.length.of.at.least(1);
expect(res.body.emailInboxes.filter((ibx: IEmailInbox) => ibx.email === 'test-email@example.com')).to.have.length.gte(1);
// make sure we delete the test inbox, even if creation failed on this test run
testInbox = res.body.emailInboxes.filter((ibx: IEmailInbox) => ibx.email === 'test-email@example.com')[0]._id;
expect(res.body).to.have.property('total');
expect(res.body.total).to.be.a('number');
expect(res.body).to.have.property('count');
expect(res.body.count).to.be.a('number');
expect(res.body).to.have.property('offset');
expect(res.body.offset).to.be.a('number');
});
});
describe('POST email-inbox', () => {
let inboxId: string;
const mockedPayload = {
name: 'test',
active: false,
email: `test${new Date().getTime()}@test.com`,
description: 'Updated test description',
senderInfo: 'test',
smtp: {
server: 'smtp.example.com',
port: 587,
username: 'xxxx',
password: 'xxxx',
secure: true,
},
imap: {
server: 'imap.example.com',
port: 993,
username: 'xxxx',
password: 'xxxx',
secure: true,
maxRetries: 10,
},
};
it('should fail if user doesnt have manage-email-inbox permission', async () => {
await updatePermission('manage-email-inbox', []);
await request.post(api('email-inbox')).set(credentials).send(mockedPayload).expect(403);
});
it('should fail if smtp config is not on body params', async () => {
await updatePermission('manage-email-inbox', ['admin']);
await request.post(api('email-inbox')).set(credentials).send({}).expect(400);
});
it('should fail if imap config is not on body params', async () => {
const { imap, ...payload } = mockedPayload;
await request.post(api('email-inbox')).set(credentials).send(payload).expect(400);
});
it('should fail if name is not on body params', async () => {
await updatePermission('manage-email-inbox', ['admin']);
const { name, ...payload } = mockedPayload;
await request.post(api('email-inbox')).set(credentials).send(payload).expect(400);
});
it('should fail if active is not on body params', async () => {
await updatePermission('manage-email-inbox', ['admin']);
const { active, ...payload } = mockedPayload;
await request.post(api('email-inbox')).set(credentials).send(payload).expect(400);
});
it('should fail if email is not on body params', async () => {
await updatePermission('manage-email-inbox', ['admin']);
const { email, ...payload } = mockedPayload;
await request.post(api('email-inbox')).set(credentials).send(payload).expect(400);
});
it('should save an email inbox', async () => {
await updatePermission('manage-email-inbox', ['admin']);
const { email, ...payload } = mockedPayload;
const { body } = await request
.post(api('email-inbox'))
.set(credentials)
.send({
...payload,
email: `test${new Date().getTime()}@test.com`,
})
.expect(200);
expect(body).to.have.property('_id');
inboxId = body._id;
});
it('should update an email inbox when _id is passed in the object', async () => {
await updatePermission('manage-email-inbox', ['admin']);
const { body } = await request
.post(api('email-inbox'))
.set(credentials)
.send({
...mockedPayload,
_id: inboxId,
description: 'Updated test description',
})
.expect(200);
expect(body).to.have.property('_id');
});
});
describe('GET email-inbox/:_id', () => {
it('should fail if user doesnt have manage-email-inbox permission', async () => {
await updatePermission('manage-email-inbox', []);
await request.get(api('email-inbox/123')).set(credentials).expect(403);
});
it('should fail when email inbox does not exist', async () => {
await updatePermission('manage-email-inbox', ['admin']);
await request.get(api('email-inbox/123')).set(credentials).expect(404);
});
it('should return an email inbox', async () => {
const inbox = await createEmailInbox();
const { body } = await request
.get(api(`email-inbox/${inbox._id}`))
.set(credentials)
.expect(200);
expect(body).to.have.property('_id');
expect(body).to.have.property('name', 'test');
});
});
describe('DELETE email-inbox/:_id', () => {
it('should fail if user doesnt have manage-email-inbox permission', async () => {
await updatePermission('manage-email-inbox', []);
await request.delete(api('email-inbox/123')).set(credentials).expect(403);
});
it('should return nothing when email inbox does not exist', async () => {
await updatePermission('manage-email-inbox', ['admin']);
await request.delete(api('email-inbox/123')).set(credentials).expect(404);
});
it('should delete an email inbox', async () => {
const inbox = await createEmailInbox();
const { body } = await request
.delete(api(`email-inbox/${inbox._id}`))
.set(credentials)
.expect(200);
expect(body).to.have.property('success', true);
});
});
describe('GET email-inbox.search', () => {
it('should fail if user doesnt have manage-email-inbox permission', async () => {
await updatePermission('manage-email-inbox', []);
await request.get(api('email-inbox.search')).set(credentials).expect(403);
});
it('should return an email inbox matching email', async () => {
await createEmailInbox();
await updatePermission('manage-email-inbox', ['admin']);
await request.get(api('email-inbox.search')).query({ email: 'test' }).set(credentials).expect(200);
});
});
});

View File

@@ -0,0 +1,844 @@
import type { Credentials } from '@rocket.chat/api-client';
import type { ILivechatDepartment, IUser } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import { sleep } from '../../../../lib/utils/sleep';
import { getCredentials, api, request, credentials } from '../../../data/api-data';
import { createCustomField, deleteCustomField } from '../../../data/livechat/custom-fields';
import {
addOrRemoveAgentFromDepartment,
createDepartmentWithAnOfflineAgent,
createDepartmentWithAnOnlineAgent,
deleteDepartment,
} from '../../../data/livechat/department';
import {
createVisitor,
createLivechatRoom,
makeAgentUnavailable,
closeOmnichannelRoom,
sendMessage,
deleteVisitor,
createDepartment,
} from '../../../data/livechat/rooms';
import { createBotAgent, getRandomVisitorToken } from '../../../data/livechat/users';
import type { WithRequiredProperty } from '../../../data/livechat/utils';
import { removePermissionFromAllRoles, restorePermissionToRoles, updatePermission, updateSetting } from '../../../data/permissions.helper';
import { deleteUser } from '../../../data/users.helper';
import { IS_EE } from '../../../e2e/config/constants';
describe('LIVECHAT - Utils', () => {
before((done) => getCredentials(done));
before(async () => {
await updateSetting('Omnichannel_enable_department_removal', true);
});
after(async () => {
await updateSetting('Livechat_enabled', true);
await updateSetting('Livechat_offline_email', '');
await updateSetting('Omnichannel_enable_department_removal', false);
});
describe('livechat/offline.message', () => {
it('should fail if name is not sent as body parameter', async () => {
const { body } = await request.post(api('livechat/offline.message')).set(credentials).send({});
expect(body).to.have.property('success', false);
});
it('should fail if email is not sent as body parameter', async () => {
const { body } = await request.post(api('livechat/offline.message')).set(credentials).send({ name: 'test' });
expect(body).to.have.property('success', false);
});
it('should fail if message is not sent as body parameter', async () => {
await request.post(api('livechat/offline.message')).set(credentials).send({ name: 'test', email: '' }).expect(400);
});
it('should fail if setting Livechat_display_offline_form is disabled', async () => {
await updateSetting('Livechat_display_offline_form', false);
await request.post(api('livechat/offline.message')).set(credentials).send({ name: 'test', email: '', message: 'test' }).expect(400);
});
it('should fail if Livechat_validate_offline_email is enabled and email passed is not resolvable', async () => {
await updateSetting('Livechat_display_offline_form', true);
await updateSetting('Livechat_validate_offline_email', true);
await request
.post(api('livechat/offline.message'))
.set(credentials)
.send({ name: 'test', email: 'test@fadsjfldasjfd.com', message: 'test' })
.expect(400);
});
it('should fail if setting Livechat_offline_email is not setup or is invalid', async () => {
await updateSetting('Livechat_validate_offline_email', false);
await updateSetting('Livechat_validate_offline_email', 'afsdxcvxc');
await request
.post(api('livechat/offline.message'))
.set(credentials)
.send({ name: 'test', email: 'test@email.com', message: 'this is a test :)' })
.expect(400);
});
it('should send an offline email', async () => {
await updateSetting('Livechat_validate_offline_email', false);
await updateSetting('Livechat_offline_email', 'test-email@rocket.chat');
await request
.post(api('livechat/offline.message'))
.set(credentials)
.send({ name: 'test', email: 'test@email.com', message: 'this is a test :)' })
.expect(200);
});
});
describe('livechat/config', () => {
let emptyDepartment: ILivechatDepartment;
let forwardDepartment: ILivechatDepartment;
let testDepartment: ILivechatDepartment;
let agent: { user: WithRequiredProperty<IUser, 'username'>; credentials: Credentials };
let agent2: { user: WithRequiredProperty<IUser, 'username'>; credentials: Credentials };
before(async () => {
if (!IS_EE) {
return;
}
emptyDepartment = await createDepartment();
({ department: forwardDepartment, agent } = await createDepartmentWithAnOnlineAgent());
({ department: testDepartment, agent: agent2 } = await createDepartmentWithAnOfflineAgent({
departmentsAllowedToForward: [forwardDepartment._id],
}));
});
after(() => {
if (!IS_EE) {
return;
}
return Promise.all([
deleteDepartment(emptyDepartment._id),
deleteDepartment(forwardDepartment._id),
deleteDepartment(testDepartment._id),
deleteUser(agent.user),
deleteUser(agent2.user),
]);
});
it('should return enabled: false if livechat is disabled', async () => {
await updateSetting('Livechat_enabled', false);
const { body } = await request.get(api('livechat/config')).set(credentials);
expect(body).to.have.property('config');
expect(body.config).to.have.property('enabled', false);
});
it('should return basic livechat config when enabled', async () => {
await updateSetting('Livechat_enabled', true);
const { body } = await request.get(api('livechat/config')).set(credentials);
expect(body).to.have.property('config');
expect(body.config).to.have.property('enabled', true);
expect(body.config).to.have.property('settings');
expect(body.config).to.have.property('departments').that.is.an('array');
});
it('should have custom fields data', async () => {
const customFieldName = `new_custom_field_${Date.now()}`;
await createCustomField({
searchable: true,
field: customFieldName,
label: customFieldName,
defaultValue: 'test_default_address',
scope: 'visitor',
visibility: 'visible',
regexp: '',
public: true,
required: false,
options: '',
});
const { body } = await request.get(api('livechat/config')).set(credentials);
expect(body).to.have.property('config');
expect(body.config).to.have.property('customFields').that.is.an('array');
expect(body.config.customFields).to.have.length.greaterThan(0);
const customField = body.config.customFields.find((field: any) => field._id === customFieldName);
expect(customField).to.be.an('object');
expect(customField).to.have.property('label', customFieldName);
await deleteCustomField(customFieldName);
});
(IS_EE ? it : it.skip)('should return online as true if there is at least one agent online', async () => {
const { department } = await createDepartmentWithAnOnlineAgent();
const { body } = await request.get(api('livechat/config')).query({ department: department._id }).set(credentials);
expect(body).to.have.property('config');
expect(body.config).to.have.property('online', true);
});
(IS_EE ? it : it.skip)('should return online as false if there is no agent online', async () => {
const { department, agent } = await createDepartmentWithAnOnlineAgent();
await makeAgentUnavailable(agent.credentials);
const { body } = await request.get(api('livechat/config')).query({ department: department._id }).set(credentials);
expect(body).to.have.property('config');
expect(body.config).to.have.property('online', false);
});
(IS_EE ? it : it.skip)('should return online as true if bot is online and there is no agent online', async () => {
await updateSetting('Livechat_assign_new_conversation_to_bot', true);
const { department, agent } = await createDepartmentWithAnOnlineAgent();
await makeAgentUnavailable(agent.credentials);
const botUser = await createBotAgent();
await addOrRemoveAgentFromDepartment(department._id, { agentId: botUser.user._id, username: botUser.user.username as string }, true);
const { body } = await request.get(api('livechat/config')).query({ department: department._id }).set(credentials);
expect(body).to.have.property('config');
await updateSetting('Livechat_assign_new_conversation_to_bot', false);
await makeAgentUnavailable(botUser.credentials);
});
it('should return a guest if there exists a guest with the same token', async () => {
const guest = await createVisitor();
const { body } = await request.get(api('livechat/config')).query({ token: guest.token }).set(credentials);
expect(body).to.have.property('config');
expect(body.config).to.have.property('guest');
expect(body.config.guest).to.have.property('name', guest.name);
});
it('should not return a guest if there exists a guest with the same token but the guest is not online', async () => {
const token = getRandomVisitorToken();
const { body } = await request.get(api('livechat/config')).query({ token }).set(credentials);
expect(body).to.have.property('config');
expect(body.config).to.not.have.property('guest');
});
it('should return no online room if visitor is not chatting with an agent', async () => {
const visitor = await createVisitor();
const { body } = await request.get(api('livechat/config')).query({ token: visitor.token }).set(credentials);
expect(body).to.have.property('config');
expect(body.config).to.not.have.property('room');
});
it('should return online room if visitor is already chatting with an agent', async () => {
const newVisitor = await createVisitor();
const newRoom = await createLivechatRoom(newVisitor.token);
const { body } = await request.get(api('livechat/config')).query({ token: newVisitor.token }).set(credentials);
expect(body).to.have.property('config');
expect(body.config).to.have.property('room');
expect(body.config.room).to.have.property('_id', newRoom._id);
});
(IS_EE ? it : it.skip)('should return list of departments with at least one agent', async () => {
const { body } = await request.get(api('livechat/config')).set(credentials);
expect(body).to.have.property('success', true);
expect(body).to.have.property('config');
expect(body.config).to.have.property('departments');
expect(body.config.departments).to.be.an('array').with.lengthOf.at.least(2);
expect(body.config.departments).to.not.deep.include({
_id: emptyDepartment._id,
name: emptyDepartment.name,
showOnRegistration: emptyDepartment.showOnRegistration,
showOnOfflineForm: emptyDepartment.showOnOfflineForm,
});
expect(body.config.departments).to.deep.include({
_id: forwardDepartment._id,
name: forwardDepartment.name,
showOnRegistration: forwardDepartment.showOnRegistration,
showOnOfflineForm: forwardDepartment.showOnOfflineForm,
});
expect(body.config.departments).to.deep.include({
_id: testDepartment._id,
name: testDepartment.name,
showOnRegistration: testDepartment.showOnRegistration,
showOnOfflineForm: testDepartment.showOnOfflineForm,
departmentsAllowedToForward: [forwardDepartment._id],
});
});
});
describe('livechat/page.visited', () => {
it('should fail if token is not sent as body param', async () => {
const { body } = await request.post(api('livechat/page.visited')).set(credentials).send({});
expect(body).to.have.property('success', false);
});
it('should fail if rid is not an string', async () => {
const { body } = await request.post(api('livechat/page.visited')).set(credentials).send({ token: 'test', rid: {} });
expect(body).to.have.property('success', false);
});
it('should fail if pageInfo is not sent as body param', async () => {
const { body } = await request.post(api('livechat/page.visited')).set(credentials).send({ token: 'test', rid: 'test' });
expect(body).to.have.property('success', false);
});
it('should fail if pageInfo is not of the right format', async () => {
const { body } = await request.post(api('livechat/page.visited')).set(credentials).send({ token: 'test', rid: 'test', pageInfo: {} });
expect(body).to.have.property('success', false);
});
it('should return empty if pageInfo.change is not equal to Livechat_history_monitor_type', async () => {
const { body } = await request
.post(api('livechat/page.visited'))
.set(credentials)
.send({ token: 'test', rid: 'test', pageInfo: { change: 'test', title: 'test', location: { href: 'test' } } });
expect(body).to.have.property('success', true);
});
it('should store values for 1 month if visitor has no room yet', async () => {
const { body } = await request
.post(api('livechat/page.visited'))
.set(credentials)
.send({ token: 'test', rid: 'test', pageInfo: { change: 'url', title: 'Rocket.Chat', location: { href: 'https://rocket.chat' } } });
expect(body).to.have.property('success', true);
expect(body).to.have.property('page');
expect(body.page).to.have.property('navigation');
expect(body.page).to.have.property('msg');
});
it('should store values correctly on visitor room when room is valid', async () => {
const visitor = await createVisitor();
const room = await createLivechatRoom(visitor.token);
const { body } = await request
.post(api('livechat/page.visited'))
.set(credentials)
.send({
token: visitor.token,
rid: room._id,
pageInfo: { change: 'url', title: 'Rocket.Chat', location: { href: 'https://rocket.chat' } },
});
expect(body).to.have.property('success', true);
expect(body).to.have.property('page');
expect(body.page).to.have.property('navigation');
expect(body.page).to.have.property('msg');
});
});
describe('livechat/transcript', () => {
it('should fail if token is not in body params', async () => {
const { body } = await request.post(api('livechat/transcript')).set(credentials).send({});
expect(body).to.have.property('success', false);
});
it('should fail if rid is not in body params', async () => {
const { body } = await request.post(api('livechat/transcript')).set(credentials).send({ token: 'test' });
expect(body).to.have.property('success', false);
});
it('should fail if email is not in body params', async () => {
const { body } = await request.post(api('livechat/transcript')).set(credentials).send({ token: 'test', rid: 'test' });
expect(body).to.have.property('success', false);
});
it('should fail if token is not a valid guest token', async () => {
const { body } = await request.post(api('livechat/transcript')).set(credentials).send({ token: 'test', rid: 'test', email: '' });
expect(body).to.have.property('success', false);
});
it('should fail if rid is not a valid room id', async () => {
const visitor = await createVisitor();
const { body } = await request
.post(api('livechat/transcript'))
.set(credentials)
.send({ token: visitor.token, rid: 'test', email: '' });
expect(body).to.have.property('success', false);
});
it('should fail if requesting a transcript of another visitors room', async () => {
const visitor = await createVisitor();
const room = await createLivechatRoom(visitor.token);
const visitor2 = await createVisitor();
const { body } = await request
.post(api('livechat/transcript'))
.set(credentials)
.send({ token: visitor2.token, rid: room._id, email: '' });
expect(body).to.have.property('success', false);
});
it('should fail if email is not a valid email', async () => {
const visitor = await createVisitor();
const room = await createLivechatRoom(visitor.token);
const { body } = await request
.post(api('livechat/transcript'))
.set(credentials)
.send({ token: visitor.token, rid: room._id, email: 'test' });
expect(body).to.have.property('success', false);
});
it('should send a transcript if all is good', async () => {
const visitor = await createVisitor();
const room = await createLivechatRoom(visitor.token);
const { body } = await request
.post(api('livechat/transcript'))
.set(credentials)
.send({ token: visitor.token, rid: room._id, email: 'visitor@notadomain.com' });
expect(body).to.have.property('success', true);
});
it('should allow a visitor to get a transcript even if token changed by using an old token that matches room.v', async () => {
const visitor = await createVisitor();
const room = await createLivechatRoom(visitor.token);
await closeOmnichannelRoom(room._id);
const visitor2 = await createVisitor(undefined, undefined, visitor.visitorEmails?.[0].address);
const room2 = await createLivechatRoom(visitor2.token);
await closeOmnichannelRoom(room2._id);
expect(visitor.token !== visitor2.token).to.be.true;
const { body } = await request
.post(api('livechat/transcript'))
.set(credentials)
.send({ token: visitor.token, rid: room._id, email: 'visitor@notadomain.com' });
expect(body).to.have.property('success', true);
const { body: body2 } = await request
.post(api('livechat/transcript'))
.set(credentials)
.send({ token: visitor2.token, rid: room2._id, email: 'visitor@notadomain.com' });
expect(body2).to.have.property('success', true);
});
});
describe('livechat/transcript/:rid', () => {
it('should fail if user is not authenticated', async () => {
await request.delete(api('livechat/transcript/rid')).send({}).expect(401);
});
it('should fail if user doesnt have "send-omnichannel-chat-transcript" permission', async () => {
const user = await createVisitor();
const room = await createLivechatRoom(user.token);
await removePermissionFromAllRoles('send-omnichannel-chat-transcript');
await request
.delete(api(`livechat/transcript/${room._id}`))
.set(credentials)
.send({})
.expect(403);
});
it('should fail if rid is not a valid room id', async () => {
await restorePermissionToRoles('send-omnichannel-chat-transcript');
await request.delete(api('livechat/transcript/rid')).set(credentials).send({}).expect(400);
});
it('should fail if room is not open', async () => {
const user = await createVisitor();
const room = await createLivechatRoom(user.token);
await closeOmnichannelRoom(room._id);
await request
.delete(api(`livechat/transcript/${room._id}`))
.set(credentials)
.send({})
.expect(400);
});
it('should fail if room doesnt have transcript requested', async () => {
const user = await createVisitor();
const room = await createLivechatRoom(user.token);
await request
.delete(api(`livechat/transcript/${room._id}`))
.set(credentials)
.send({})
.expect(400);
});
it('should remove transcript if all good', async () => {
const user = await createVisitor();
const room = await createLivechatRoom(user.token);
await request
.post(api(`livechat/transcript/${room._id}`))
.set(credentials)
.send({ email: 'abc@abc.com', subject: 'test' })
.expect(200);
await request
.delete(api(`livechat/transcript/${room._id}`))
.set(credentials)
.send({})
.expect(200);
});
});
describe('POST livechat/transcript/:rid', () => {
it('should fail if user is not authenticated', async () => {
await request.post(api('livechat/transcript/rid')).send({}).expect(401);
});
it('should fail if "email" param is not sent', async () => {
const user = await createVisitor();
const room = await createLivechatRoom(user.token);
await request
.post(api(`livechat/transcript/${room._id}`))
.set(credentials)
.send({})
.expect(400);
});
it('should fail if "subject" param is not sent', async () => {
const user = await createVisitor();
const room = await createLivechatRoom(user.token);
await request
.post(api(`livechat/transcript/${room._id}`))
.set(credentials)
.send({ email: 'abc@abc.xmz' })
.expect(400);
});
it('should fail if user doesnt have "send-omnichannel-chat-transcript" permission', async () => {
const user = await createVisitor();
const room = await createLivechatRoom(user.token);
await updatePermission('send-omnichannel-chat-transcript', []);
await request
.post(api(`livechat/transcript/${room._id}`))
.set(credentials)
.send({ email: 'abc@abc.com', subject: 'test' })
.expect(403);
});
it('should fail if rid is not a valid room id', async () => {
await updatePermission('send-omnichannel-chat-transcript', ['livechat-manager', 'admin']);
await request.post(api('livechat/transcript/rid')).set(credentials).send({ email: 'abc@abc.com', subject: 'test' }).expect(400);
});
it('should fail if room is not open', async () => {
const user = await createVisitor();
const room = await createLivechatRoom(user.token);
await closeOmnichannelRoom(room._id);
await request
.post(api(`livechat/transcript/${room._id}`))
.set(credentials)
.send({ email: 'abc@abc.com', subject: 'test' })
.expect(400);
});
it('should fail if room already has transcript requested', async () => {
const user = await createVisitor();
const room = await createLivechatRoom(user.token);
await request
.post(api(`livechat/transcript/${room._id}`))
.set(credentials)
.send({ email: 'abc@abc.com', subject: 'test' })
.expect(200);
await request
.post(api(`livechat/transcript/${room._id}`))
.set(credentials)
.send({ email: 'abc@abc.com', subject: 'test' })
.expect(400);
});
it('should request transcript if all good', async () => {
const user = await createVisitor();
const room = await createLivechatRoom(user.token);
await request
.post(api(`livechat/transcript/${room._id}`))
.set(credentials)
.send({ email: 'abc@abc.com', subject: 'test' })
.expect(200);
});
});
describe('livechat/visitors.search', () => {
it('should bring sorted data by last chat time', async () => {
const visitor1 = await createVisitor(undefined, 'VisitorInPast');
const room1 = await createLivechatRoom(visitor1.token);
const visitor2 = await createVisitor(undefined, 'VisitorInPresent');
const room2 = await createLivechatRoom(visitor2.token);
const { body: result1 } = await request
.get(api('livechat/visitors.search'))
.query({ term: 'VisitorIn', sort: '{"lastChat.ts":1}' })
.set(credentials)
.send();
expect(result1).to.have.property('visitors').that.is.an('array');
expect(result1.visitors[0]).to.have.property('name');
expect(result1.visitors[0].name).to.be.eq('VisitorInPast');
const { body: result2 } = await request
.get(api('livechat/visitors.search'))
.query({ term: 'VisitorIn', sort: '{"lastChat.ts":-1}' })
.set(credentials)
.send();
expect(result2).to.have.property('visitors').that.is.an('array');
expect(result2.visitors[0]).to.have.property('name');
expect(result2.visitors[0].name).to.be.eq('VisitorInPresent');
await closeOmnichannelRoom(room1._id);
await closeOmnichannelRoom(room2._id);
await deleteVisitor(visitor1.token);
await deleteVisitor(visitor2.token);
});
});
describe('livechat/message', () => {
const visitorTokens: string[] = [];
after(() => Promise.all(visitorTokens.map((token) => deleteVisitor(token))));
it('should fail if no token', async () => {
await request.post(api('livechat/message')).set(credentials).send({}).expect(400);
});
it('should fail if no rid', async () => {
await request.post(api('livechat/message')).set(credentials).send({ token: 'test' }).expect(400);
});
it('should fail if no msg', async () => {
await request.post(api('livechat/message')).set(credentials).send({ token: 'test', rid: 'test' }).expect(400);
});
it('should fail if token is invalid', async () => {
await request.post(api('livechat/message')).set(credentials).send({ token: 'test', rid: 'test', msg: 'test' }).expect(400);
});
it('should fail if rid is invalid', async () => {
const visitor = await createVisitor();
visitorTokens.push(visitor.token);
await request.post(api('livechat/message')).set(credentials).send({ token: visitor.token, rid: 'test', msg: 'test' }).expect(400);
});
it('should fail if rid belongs to another visitor', async () => {
const visitor = await createVisitor();
const visitor2 = await createVisitor();
visitorTokens.push(visitor.token, visitor2.token);
const room = await createLivechatRoom(visitor2.token);
await request.post(api('livechat/message')).set(credentials).send({ token: visitor.token, rid: room._id, msg: 'test' }).expect(400);
});
it('should fail if room is closed', async () => {
const visitor = await createVisitor();
visitorTokens.push(visitor.token);
const room = await createLivechatRoom(visitor.token);
await closeOmnichannelRoom(room._id);
await request.post(api('livechat/message')).set(credentials).send({ token: visitor.token, rid: room._id, msg: 'test' }).expect(400);
});
it('should fail if message is greater than Livechat_enable_message_character_limit setting', async () => {
const visitor = await createVisitor();
visitorTokens.push(visitor.token);
const room = await createLivechatRoom(visitor.token);
await updateSetting('Livechat_enable_message_character_limit', true);
await updateSetting('Livechat_message_character_limit', 1);
await request.post(api('livechat/message')).set(credentials).send({ token: visitor.token, rid: room._id, msg: 'test' }).expect(400);
await updateSetting('Livechat_enable_message_character_limit', false);
});
it('should send a message', async () => {
const visitor = await createVisitor();
visitorTokens.push(visitor.token);
const room = await createLivechatRoom(visitor.token);
await request.post(api('livechat/message')).set(credentials).send({ token: visitor.token, rid: room._id, msg: 'test' }).expect(200);
});
});
describe('[GET] livechat/message/:_id', () => {
it('should fail if no token is provided', async () => {
await request.get(api('livechat/message/rid')).set(credentials).expect(400);
});
it('should fail if no rid is provided', async () => {
await request.get(api('livechat/message/mid')).query({ token: 'test' }).expect(400);
});
it('should fail if token points to an invalid visitor', async () => {
await request.get(api('livechat/message/mid')).query({ token: 'test', rid: 'test' }).expect(400);
});
it('should fail if room points to an invalid room', async () => {
const visitor = await createVisitor();
await request.get(api('livechat/message/mid')).query({ token: visitor.token, rid: 'test' }).expect(400);
});
it('should fail if _id points to an invalid message', async () => {
const visitor = await createVisitor();
const room = await createLivechatRoom(visitor.token);
await request.get(api('livechat/message/mid')).query({ token: visitor.token, rid: room._id }).expect(400);
});
it('should return a message', async () => {
const visitor = await createVisitor();
const room = await createLivechatRoom(visitor.token);
const message = await sendMessage(room._id, 'test', visitor.token);
await request
.get(api(`livechat/message/${message._id}`))
.query({ token: visitor.token, rid: room._id })
.expect(200);
});
});
describe('[PUT] livechat/message/:_id', () => {
it('should fail if no token is provided', async () => {
await request.put(api('livechat/message/rid')).set(credentials).expect(400);
});
it('should fail if no rid is provided', async () => {
await request.put(api('livechat/message/mid')).query({ token: 'test' }).expect(400);
});
it('should fail if token points to an invalid visitor', async () => {
await request.put(api('livechat/message/mid')).query({ token: 'test', rid: 'test' }).expect(400);
});
it('should fail if room points to an invalid room', async () => {
const visitor = await createVisitor();
await request.put(api('livechat/message/mid')).query({ token: visitor.token, rid: 'test' }).expect(400);
});
it('should fail if _id points to an invalid message', async () => {
const visitor = await createVisitor();
const room = await createLivechatRoom(visitor.token);
await request.put(api('livechat/message/mid')).query({ token: visitor.token, rid: room._id }).expect(400);
});
it('should update a message', async () => {
const visitor = await createVisitor();
const room = await createLivechatRoom(visitor.token);
const message = await sendMessage(room._id, 'test', visitor.token);
const { body } = await request
.put(api(`livechat/message/${message._id}`))
.set(credentials)
.send({ msg: 'test2', token: visitor.token, rid: room._id })
.expect(200);
expect(body).to.have.property('message');
expect(body.message).to.have.property('_id', message._id);
expect(body.message).to.have.property('msg', 'test2');
});
});
describe('[DELETE] livechat/message/:_id', () => {
it('should fail if no token is provided', async () => {
await request.delete(api('livechat/message/rid')).set(credentials).expect(400);
});
it('should fail if no rid is provided', async () => {
await request.delete(api('livechat/message/mid')).query({ token: 'test' }).expect(400);
});
it('should fail if token points to an invalid visitor', async () => {
await request.delete(api('livechat/message/mid')).query({ token: 'test', rid: 'test' }).expect(400);
});
it('should fail if room points to an invalid room', async () => {
const visitor = await createVisitor();
await request.delete(api('livechat/message/mid')).query({ token: visitor.token, rid: 'test' }).expect(400);
});
it('should fail if _id points to an invalid message', async () => {
const visitor = await createVisitor();
const room = await createLivechatRoom(visitor.token);
await request.delete(api('livechat/message/mid')).query({ token: visitor.token, rid: room._id }).expect(400);
});
it('should delete a message', async () => {
const visitor = await createVisitor();
const room = await createLivechatRoom(visitor.token);
const message = await sendMessage(room._id, 'test', visitor.token);
const { body } = await request
.delete(api(`livechat/message/${message._id}`))
.send({ token: visitor.token, rid: room._id })
.expect(200);
expect(body).to.have.property('message');
expect(body.message).to.have.property('_id', message._id);
});
});
describe('livechat/messages.history/:rid', () => {
it('should fail if no token is provided', async () => {
await request.get(api('livechat/messages.history/rid')).set(credentials).expect(400);
});
it('should fail if token points to an invalid visitor', async () => {
await request.get(api('livechat/messages.history/rid')).query({ token: 'test' }).expect(400);
});
it('should fail if room points to an invalid room', async () => {
const visitor = await createVisitor();
await request.get(api('livechat/messages.history/rid')).query({ token: visitor.token }).expect(400);
});
it('should fail if room points to a room of another visitor', async () => {
const visitor = await createVisitor();
const visitor2 = await createVisitor();
const room = await createLivechatRoom(visitor2.token);
await request
.get(api(`livechat/messages.history/${room._id}`))
.query({ token: visitor.token })
.expect(400);
});
it('should return a list of messages', async () => {
const visitor = await createVisitor();
const room = await createLivechatRoom(visitor.token);
await sendMessage(room._id, 'test', visitor.token);
const { body } = await request
.get(api(`livechat/messages.history/${room._id}`))
.query({ token: visitor.token })
.expect(200);
expect(body).to.have.property('messages').that.is.an('array');
expect(body.messages).to.have.lengthOf(2);
expect(body.messages[0]).to.have.property('msg', 'test');
});
it('should return a list of messages with offset and count', async () => {
const visitor = await createVisitor();
const room = await createLivechatRoom(visitor.token);
await sendMessage(room._id, 'test', visitor.token);
await sendMessage(room._id, 'test2', visitor.token);
const { body } = await request
.get(api(`livechat/messages.history/${room._id}`))
.query({ token: visitor.token, offset: 1, limit: 1 })
.expect(200);
expect(body).to.have.property('messages').that.is.an('array');
expect(body.messages).to.have.lengthOf(1);
expect(body.messages[0]).to.have.property('msg', 'test');
});
it('should return a list of unseen messages', async () => {
const visitor = await createVisitor();
const room = await createLivechatRoom(visitor.token);
await sendMessage(room._id, 'test', visitor.token);
await sendMessage(room._id, 'test2', visitor.token);
const { body } = await request
.get(api(`livechat/messages.history/${room._id}`))
.query({ token: visitor.token, ls: new Date() })
.expect(200);
expect(body).to.have.property('messages').that.is.an('array');
expect(body.messages).to.have.lengthOf(3);
expect(body.messages[0]).to.have.property('msg', 'test2');
});
it('should return a list of messages up to a specific date', async () => {
const visitor = await createVisitor();
const room = await createLivechatRoom(visitor.token);
const msg = await sendMessage(room._id, 'test', visitor.token);
const tsPlusSomeMillis = new Date(new Date(msg.ts).getTime() + 500);
await sleep(1000);
await sendMessage(room._id, 'test2', visitor.token);
const { body } = await request
.get(api(`livechat/messages.history/${room._id}`))
.query({ token: visitor.token, end: tsPlusSomeMillis })
.expect(200);
expect(body).to.have.property('messages').that.is.an('array');
expect(body.messages).to.have.lengthOf(2);
expect(body.messages[0]).to.have.property('msg', 'test');
});
it('should return message history for a valid room with pagination', async () => {
const visitor = await createVisitor();
const room = await createLivechatRoom(visitor.token);
await sendMessage(room._id, 'Hello', visitor.token);
const { body } = await request
.get(api(`livechat/messages.history/${room._id}`))
.set(credentials)
.query({ token: visitor.token, limit: 1 })
.expect('Content-Type', 'application/json')
.expect(200);
expect(body).to.have.property('success', true);
expect(body).to.have.property('messages').of.length(1);
expect(body.messages[0]).to.have.property('msg', 'Hello');
});
it('should return message history for a valid room with pagination and offset', async () => {
const visitor = await createVisitor();
const room = await createLivechatRoom(visitor.token);
await sendMessage(room._id, 'Hello', visitor.token);
const { body } = await request
.get(api(`livechat/messages.history/${room._id}`))
.set(credentials)
.query({ token: visitor.token, limit: 1, offset: 1 })
.expect('Content-Type', 'application/json')
.expect(200);
expect(body).to.have.property('success', true);
expect(body).to.have.property('messages').of.length(1);
expect(body.messages[0]).to.have.property('t');
});
});
(IS_EE ? describe : describe.skip)('[EE] livechat widget', () => {
it('should include additional css when provided via Livechat_WidgetLayoutClasses setting', async () => {
await updateSetting('Livechat_WidgetLayoutClasses', 'http://my.css.com/my.css');
const x = await request.get('/livechat').expect(200);
expect(x.text.includes('http://my.css.com/my.css')).to.be.true;
});
it('should remove additional css when setting Livechat_WidgetLayoutClasses is empty', async () => {
await updateSetting('Livechat_WidgetLayoutClasses', '');
const x = await request.get('/livechat').expect(200);
expect(x.text.includes('http://my.css.com/my.css')).to.be.false;
});
it('should include additional js when provided via Livechat_AdditionalWidgetScripts setting', async () => {
await updateSetting('Livechat_AdditionalWidgetScripts', 'http://my.js.com/my.js');
const x = await request.get('/livechat').expect(200);
expect(x.text.includes('http://my.js.com/my.js')).to.be.true;
});
it('should remove additional js when setting Livechat_AdditionalWidgetScripts is empty', async () => {
await updateSetting('Livechat_AdditionalWidgetScripts', '');
const x = await request.get('/livechat').expect(200);
expect(x.text.includes('http://my.js.com/my.js')).to.be.false;
});
});
});

View File

@@ -0,0 +1,135 @@
import { expect } from 'chai';
import { before, after, describe, it } from 'mocha';
import type { Response } from 'supertest';
import { api, request, credentials, getCredentials } from '../../../data/api-data';
import { updatePermission } from '../../../data/permissions.helper';
describe('Mailer', () => {
before((done) => getCredentials(done));
describe('POST mailer', async () => {
before(async () => {
return updatePermission('send-mail', ['admin']);
});
after(async () => {
return updatePermission('send-mail', ['admin']);
});
it('should send an email if the payload is correct', async () => {
await request
.post(api('mailer'))
.set(credentials)
.send({
from: 'rocketchat.internal.admin.test@rocket.chat',
subject: 'Test email subject',
body: 'Test email body [unsubscribe]',
dryrun: true,
query: '',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
});
});
it('should throw an error if the request is incorrect', async () => {
await request
.post(api('mailer'))
.set(credentials)
.send({
from: 12345,
subject: {},
body: 'Test email body',
dryrun: true,
query: '',
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
});
});
it('should throw an error if the "from" param is missing', async () => {
await request
.post(api('mailer'))
.set(credentials)
.send({
subject: 'Test email subject',
body: 'Test email body',
dryrun: true,
query: '',
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
});
});
it('should throw an error if user does NOT have the send-mail permission', async () => {
await updatePermission('send-mail', []);
await request
.post(api('mailer'))
.set(credentials)
.send({
from: 'test-mail@test.com',
subject: 'Test email subject',
body: 'Test email body',
dryrun: true,
query: '',
})
.expect('Content-Type', 'application/json')
.expect(403)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'User does not have the permissions required for this action [error-unauthorized]');
});
});
});
});
describe('Mailer Unsubscribe', () => {
describe('POST mailer unsubscribe', () => {
it('should unsubscribe to mailer if the request is correct', async () => {
await request
.post(api('mailer.unsubscribe'))
.set(credentials)
.send({
_id: credentials['X-User-Id'],
createdAt: new Date().getTime().toString(),
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
});
});
it('should throw an error if the "_id" param is missing', async () => {
await request
.post(api('mailer.unsubscribe'))
.set(credentials)
.send({
createdAt: new Date().getTime().toString(),
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
});
});
it('should throw an error if the "createdAt" param is missing', async () => {
await request
.post(api('mailer.unsubscribe'))
.set(credentials)
.send({
_id: credentials['X-User-Id'],
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
});
});
});
});

View File

@@ -0,0 +1,936 @@
import { faker } from '@faker-js/faker';
import type {
ILivechatInquiryRecord,
ILivechatPriority,
IOmnichannelRoom,
IOmnichannelServiceLevelAgreements,
} from '@rocket.chat/core-typings';
import { OmnichannelSortingMechanismSettingType } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import { getCredentials, api, request, credentials } from '../../../data/api-data';
import { createDepartmentWithAnOnlineAgent } from '../../../data/livechat/department';
import { fetchAllInquiries } from '../../../data/livechat/inquiries';
import { createSLA, deleteSLA, bulkCreateSLA, deleteAllSLA } from '../../../data/livechat/priorities';
import {
createAgent,
createVisitor,
createLivechatRoom,
bulkCreateLivechatRooms,
startANewLivechatRoomAndTakeIt,
makeAgentAvailable,
deleteAgent,
} from '../../../data/livechat/rooms';
import {
addPermissions,
removePermissionFromAllRoles,
removePermissions,
restorePermissionToRoles,
updateEESetting,
updatePermission,
updateSetting,
} from '../../../data/permissions.helper';
import { IS_EE } from '../../../e2e/config/constants';
import { generateRandomSLAData } from '../../../e2e/utils/omnichannel/sla';
(IS_EE ? describe : describe.skip)('[EE] LIVECHAT - Priorities & SLAs', () => {
before((done) => getCredentials(done));
before(async () => {
await updateSetting('Livechat_enabled', true);
await updateSetting('Livechat_Routing_Method', 'Manual_Selection');
await updateEESetting('Livechat_Require_Contact_Verification', 'never');
});
after(async () => {
await addPermissions({
'manage-livechat-priorities': ['admin', 'livechat-manager'],
'manage-livechat-sla': ['admin', 'livechat-manager'],
'view-l-room': ['admin', 'livechat-manager', 'livechat-agent'],
});
});
describe('livechat/sla', () => {
// GET
it('should return an "unauthorized error" when the user does not have the necessary permission for [GET] livechat/sla endpoint', async () => {
await removePermissions(['manage-livechat-sla', 'view-l-room']);
const response = await request.get(api('livechat/sla')).set(credentials).expect('Content-Type', 'application/json').expect(403);
expect(response.body).to.have.property('success', false);
});
it('should return an array of slas', async () => {
await addPermissions({
'manage-livechat-sla': ['admin', 'livechat-manager'],
'view-l-room': ['livechat-agent', 'admin', 'livechat-manager'],
});
const sla = await createSLA();
expect(sla).to.not.be.undefined;
expect(sla).to.have.property('_id');
const response = await request.get(api('livechat/sla')).set(credentials).expect('Content-Type', 'application/json').expect(200);
expect(response.body).to.have.property('success', true);
expect(response.body.sla).to.be.an('array').with.lengthOf.greaterThan(0);
const current = response.body.sla.find((p: IOmnichannelServiceLevelAgreements) => p && p?._id === sla._id);
expect(current).to.be.an('object');
expect(current).to.have.property('name', sla.name);
expect(current).to.have.property('description', sla.description);
expect(current).to.have.property('dueTimeInMinutes', sla.dueTimeInMinutes);
await deleteSLA(sla._id);
});
// POST
it('should return an "unauthorized error" when the user does not have the necessary permission for [POST] livechat/sla endpoint', async () => {
await updatePermission('manage-livechat-sla', []);
const response = await request
.post(api('livechat/sla'))
.set(credentials)
.send(generateRandomSLAData())
.expect('Content-Type', 'application/json')
.expect(403);
expect(response.body).to.have.property('success', false);
});
it('should create a new sla', async () => {
await restorePermissionToRoles('manage-livechat-sla');
const sla = generateRandomSLAData();
const response = await request
.post(api('livechat/sla'))
.set(credentials)
.send(sla)
.expect('Content-Type', 'application/json')
.expect(200);
expect(response.body).to.have.property('success', true);
expect(response.body.sla).to.be.an('object');
expect(response.body.sla).to.have.property('_id');
expect(response.body.sla).to.have.property('name', sla.name);
expect(response.body.sla).to.have.property('description', sla.description);
expect(response.body.sla).to.have.property('dueTimeInMinutes', sla.dueTimeInMinutes);
await deleteSLA(response.body.sla._id);
});
it('should throw an error when trying to create a duplicate sla with same dueTimeInMinutes', async () => {
const firstSla = generateRandomSLAData();
const response = await request
.post(api('livechat/sla'))
.set(credentials)
.send(firstSla)
.expect('Content-Type', 'application/json')
.expect(200);
expect(response.body).to.have.property('success', true);
const secondSla = generateRandomSLAData();
secondSla.dueTimeInMinutes = firstSla.dueTimeInMinutes;
const secondResponse = await request
.post(api('livechat/sla'))
.set(credentials)
.send(secondSla)
.expect('Content-Type', 'application/json')
.expect(400);
expect(secondResponse.body).to.have.property('success', false);
expect(secondResponse.body).to.have.property('error');
expect(secondResponse.body?.error).to.contain('error-duplicated-sla');
await deleteSLA(response.body.sla._id);
});
it('should throw an error when trying to create a duplicate sla with same name', async () => {
const firstSla = generateRandomSLAData();
const response = await request
.post(api('livechat/sla'))
.set(credentials)
.send(firstSla)
.expect('Content-Type', 'application/json')
.expect(200);
expect(response.body).to.have.property('success', true);
const secondSla = generateRandomSLAData();
secondSla.name = firstSla.name;
const secondResponse = await request
.post(api('livechat/sla'))
.set(credentials)
.send(secondSla)
.expect('Content-Type', 'application/json')
.expect(400);
expect(secondResponse.body).to.have.property('success', false);
expect(secondResponse.body).to.have.property('error');
expect(secondResponse.body?.error).to.contain('error-duplicated-sla');
await deleteSLA(response.body.sla._id);
});
});
describe('livechat/sla/:slaId', () => {
// GET
it('should return an "unauthorized error" when the user does not have the necessary permission for [GET] livechat/sla/:slaId endpoint', async () => {
await Promise.all([removePermissionFromAllRoles('manage-livechat-sla'), removePermissionFromAllRoles('view-l-room')]);
const response = await request.get(api('livechat/sla/123')).set(credentials).expect('Content-Type', 'application/json').expect(403);
expect(response.body).to.have.property('success', false);
});
it('should create, find and delete an sla', async () => {
await Promise.all([restorePermissionToRoles('manage-livechat-sla'), restorePermissionToRoles('view-l-room')]);
const sla = await createSLA();
const response = await request
.get(api(`livechat/sla/${sla._id}`))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(response.body).to.have.property('success', true);
expect(response.body).to.be.an('object');
expect(response.body._id).to.be.equal(sla._id);
await deleteSLA(sla._id);
});
// PUT
it('should return an "unauthorized error" when the user does not have the necessary permission for [PUT] livechat/sla/:slaId endpoint', async () => {
await removePermissionFromAllRoles('manage-livechat-sla');
const response = await request
.put(api('livechat/sla/123'))
.set(credentials)
.send(generateRandomSLAData())
.expect('Content-Type', 'application/json')
.expect(403);
expect(response.body).to.have.property('success', false);
});
it('should update an sla', async () => {
await restorePermissionToRoles('manage-livechat-sla');
const sla = await createSLA();
const newSlaData = generateRandomSLAData();
const response = await request
.put(api(`livechat/sla/${sla._id}`))
.set(credentials)
.send(newSlaData)
.expect('Content-Type', 'application/json')
.expect(200);
expect(response.body).to.have.property('success', true);
expect(response.body.sla).to.be.an('object');
expect(response.body.sla).to.have.property('_id');
expect(response.body.sla).to.have.property('name', newSlaData.name);
expect(response.body.sla).to.have.property('description', newSlaData.description);
expect(response.body.sla).to.have.property('dueTimeInMinutes', newSlaData.dueTimeInMinutes);
await deleteSLA(response.body.sla._id);
});
it('should throw an error when trying to update a sla with a duplicate name', async () => {
const firstSla = await createSLA();
const secondSla = await createSLA();
secondSla.name = firstSla.name;
const response = await request
.put(api(`livechat/sla/${secondSla._id}`))
.set(credentials)
.send({
name: secondSla.name,
description: secondSla.description,
dueTimeInMinutes: secondSla.dueTimeInMinutes,
})
.expect('Content-Type', 'application/json')
.expect(400);
expect(response.body).to.have.property('success', false);
expect(response.body).to.have.property('error');
expect(response.body?.error).to.contain('error-duplicated-sla');
await deleteSLA(firstSla._id);
await deleteSLA(secondSla._id);
});
it('should throw an error when trying to update a sla with a duplicate dueTimeInMinutes', async () => {
const firstSla = await createSLA();
const secondSla = await createSLA();
secondSla.dueTimeInMinutes = firstSla.dueTimeInMinutes;
const response = await request
.put(api(`livechat/sla/${secondSla._id}`))
.set(credentials)
.send({
name: secondSla.name,
description: secondSla.description,
dueTimeInMinutes: secondSla.dueTimeInMinutes,
})
.expect('Content-Type', 'application/json')
.expect(400);
expect(response.body).to.have.property('success', false);
expect(response.body).to.have.property('error');
expect(response.body?.error).to.contain('error-duplicated-sla');
await deleteSLA(firstSla._id);
await deleteSLA(secondSla._id);
});
// DELETE
it('should return an "unauthorized error" when the user does not have the necessary permission for [DELETE] livechat/sla/:slaId endpoint', async () => {
await updatePermission('manage-livechat-sla', []);
const response = await request
.delete(api('livechat/sla/123'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(403);
expect(response.body).to.have.property('success', false);
});
it('should delete an sla', async () => {
await updatePermission('manage-livechat-sla', ['admin', 'livechat-manager']);
const sla = await createSLA();
const response = await request
.delete(api(`livechat/sla/${sla._id}`))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(response.body).to.have.property('success', true);
});
});
describe('livechat/inquiry.setSLA', () => {
before(async () => {
await createAgent();
await makeAgentAvailable();
});
after(async () => {
await deleteAgent();
});
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await removePermissions(['manage-livechat-sla', 'view-l-room', 'manage-livechat-priorities']);
const response = await request
.put(api('livechat/inquiry.setSLA'))
.set(credentials)
.send({
roomId: '123',
sla: '123',
})
.expect('Content-Type', 'application/json')
.expect(403);
expect(response.body).to.have.property('success', false);
});
it('should fail if roomId is not in request body', async () => {
await addPermissions({
'manage-livechat-sla': ['admin', 'livechat-manager'],
'manage-livechat-priorities': ['admin', 'livechat-manager'],
'view-l-room': ['livechat-agent', 'admin', 'livechat-manager'],
});
const response = await request
.put(api('livechat/inquiry.setSLA'))
.set(credentials)
.send({
sla: '123',
})
.expect('Content-Type', 'application/json')
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should fail if roomId is invalid', async () => {
const response = await request
.put(api('livechat/inquiry.setSLA'))
.set(credentials)
.send({
roomId: '123',
sla: '123',
})
.expect('Content-Type', 'application/json')
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should fail if sla is not in request body', async () => {
const response = await request
.put(api('livechat/inquiry.setSLA'))
.set(credentials)
.send({
roomId: '123',
})
.expect('Content-Type', 'application/json')
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should fail if sla is not valid', async () => {
const visitor = await createVisitor();
const room = await createLivechatRoom(visitor.token);
const response = await request
.put(api('livechat/inquiry.setSLA'))
.set(credentials)
.send({
roomId: room._id,
sla: '123',
})
.expect('Content-Type', 'application/json')
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should fail if inquiry is not queued', async () => {
const {
room: { _id: roomId },
} = await startANewLivechatRoomAndTakeIt();
const response = await request
.put(api('livechat/inquiry.setSLA'))
.set(credentials)
.send({
roomId,
sla: '123',
})
.expect('Content-Type', 'application/json')
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should prioritize an inquiry', async () => {
const visitor = await createVisitor();
const room = await createLivechatRoom(visitor.token);
const sla = await createSLA();
const response = await request
.put(api('livechat/inquiry.setSLA'))
.set(credentials)
.send({
roomId: room._id,
sla: sla._id,
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(response.body).to.have.property('success', true);
});
});
describe('livechat/priorities', () => {
let priority: ILivechatPriority;
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await removePermissions(['manage-livechat-priorities', 'view-l-room']);
const response = await request
.get(api('livechat/priorities'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(403);
expect(response.body).to.have.property('success', false);
});
it('should return an array of priorities', async () => {
await addPermissions({
'manage-livechat-priorities': ['admin', 'livechat-manager'],
'view-l-room': ['livechat-agent', 'admin', 'livechat-manager'],
});
const response = await request
.get(api('livechat/priorities'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(response.body).to.have.property('success', true);
expect(response.body.priorities).to.be.an('array');
expect(response.body.priorities).to.have.lengthOf(5);
expect(response.body.priorities[0]).to.have.property('_id');
expect(response.body.priorities[0]).to.have.property('i18n');
expect(response.body.priorities[0]).to.have.property('dirty');
priority = response.body.priorities[0];
});
it('should return an array of priorities matching text param', async () => {
const response = await request
.get(api('livechat/priorities'))
.set(credentials)
.query({
text: priority.name,
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(response.body).to.have.property('success', true);
expect(response.body.priorities).to.be.an('array');
expect(response.body.priorities).to.have.length.greaterThan(0);
expect(response.body.priorities[0]).to.have.property('_id');
});
});
describe('livechat/priorities/:priorityId', () => {
let priority: ILivechatPriority;
const name = `${faker.lorem.word()}${faker.git.commitSha({ length: 6 })}`;
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await removePermissions(['manage-livechat-priorities', 'view-l-room']);
const response = await request.get(api('livechat/priorities/123')).set(credentials).expect(403);
expect(response.body).to.have.property('success', false);
expect(response.body).to.have.property('error');
expect(response.body?.error).to.contain('error-unauthorized');
});
it('should return a priority by id', async () => {
await addPermissions({
'manage-livechat-priorities': ['admin', 'livechat-manager'],
'view-l-room': ['livechat-agent', 'admin', 'livechat-manager'],
});
const {
body: { priorities },
} = await request.get(api('livechat/priorities')).set(credentials).expect('Content-Type', 'application/json').expect(200);
priority = priorities[0];
const response = await request
.get(api(`livechat/priorities/${priority._id}`))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(response.body).to.have.property('success', true);
expect(response.body).to.be.an('object');
expect(response.body._id).to.be.equal(priority._id);
});
it('should throw 404 when priority does not exist', async () => {
const response = await request.get(api('livechat/priorities/123')).set(credentials).expect(404);
expect(response.body).to.have.property('success', false);
});
it('should update a priority when using PUT', async () => {
const response = await request
.put(api(`livechat/priorities/${priority._id}`))
.set(credentials)
.send({
name,
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(response.body).to.have.property('success', true);
});
it('should return dirty: true after a priority has been updated', async () => {
const response = await request
.get(api(`livechat/priorities/${priority._id}`))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(response.body).to.have.property('success', true);
expect(response.body).to.have.property('dirty', true);
});
it('should return an array of priorities matching text param', async () => {
const response = await request
.get(api('livechat/priorities'))
.set(credentials)
.query({
text: name,
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(response.body).to.have.property('success', true);
expect(response.body.priorities).to.be.an('array');
expect(response.body.priorities).to.have.length.greaterThan(0);
const pos = response.body.priorities.findIndex((p: ILivechatPriority) => p._id === priority._id);
expect(pos).to.be.greaterThan(-1);
expect(response.body.priorities[pos]).to.have.property('_id');
expect(response.body.priorities[pos]).to.have.property('i18n', priority.i18n);
});
it('should edit a priority with a PUT', async () => {
const newName = `${faker.lorem.word()}${faker.git.commitSha({ length: 6 })}`;
const response = await request
.put(api(`livechat/priorities/${priority._id}`))
.set(credentials)
.send({
name: newName,
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(response.body).to.have.property('success', true);
const newPriorityResponse = await request
.get(api(`livechat/priorities/${priority._id}`))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(newPriorityResponse.body).to.have.property('success', true);
expect(newPriorityResponse.body).to.have.property('dirty', true);
expect(newPriorityResponse.body).to.have.property('name', newName);
});
it('should fail to edit a priority with a PUT if using too many parameters', async () => {
const newName = faker.lorem.word();
const response = await request
.put(api(`livechat/priorities/${priority._id}`))
.set(credentials)
.send({
name: newName,
reset: true,
})
.expect('Content-Type', 'application/json')
.expect(400);
expect(response.body).to.have.property('success', false);
expect(response.body).to.have.property('error');
expect(response.body?.error).to.contain('invalid-params');
});
it('should fail to edit a priority with a PUT if using an object as name', async () => {
const newName = faker.lorem.word();
const response = await request
.put(api(`livechat/priorities/${priority._id}`))
.set(credentials)
.send({
name: { name: newName },
})
.expect('Content-Type', 'application/json')
.expect(400);
expect(response.body).to.have.property('success', false);
expect(response.body).to.have.property('error');
expect(response.body?.error).to.contain('invalid-params');
});
it('should not fail to edit a priority with a PUT if using a boolean as name (it becomes a string)', async () => {
const response = await request
.put(api(`livechat/priorities/${priority._id}`))
.set(credentials)
.send({
name: false,
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(response.body).to.have.property('success', true);
});
it('should fail to update a non-existing priority', async () => {
const response = await request
.put(api('livechat/priorities/123'))
.set(credentials)
.send({
name: faker.lorem.word(),
})
.expect('Content-Type', 'application/json')
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should reset a single priority with a reset:true PUT parameter', async () => {
const response = await request
.put(api(`livechat/priorities/${priority._id}`))
.set(credentials)
.send({
reset: true,
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(response.body).to.have.property('success', true);
const newPriorityResponse = await request
.get(api(`livechat/priorities/${priority._id}`))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(newPriorityResponse.body).to.have.property('success', true);
expect(newPriorityResponse.body).to.have.property('dirty', false);
expect(newPriorityResponse.body).to.not.have.property('name');
});
it('should throw a duplicate error incase there is already a priority with same name', async () => {
const {
body: { priorities },
} = await request.get(api('livechat/priorities')).set(credentials).expect('Content-Type', 'application/json').expect(200);
// change name of first priority to a random name
const newName = `${faker.lorem.word()}${faker.git.commitSha({ length: 6 })}`;
await request
.put(api(`livechat/priorities/${priorities[0]._id}`))
.set(credentials)
.send({ name: newName })
.expect('Content-Type', 'application/json')
.expect(200);
// change name of second priority to the name of first priority and expect error
const response = await request
.put(api(`livechat/priorities/${priorities[1]._id}`))
.set(credentials)
.send({ name: newName })
.expect('Content-Type', 'application/json')
.expect(400);
expect(response.body).to.have.property('success', false);
expect(response.body).to.have.property('error', 'error-duplicate-priority-name');
});
it('should throw a duplicate error incase there is already a priority with same name (case insensitive)', async () => {
const {
body: { priorities },
} = await request.get(api('livechat/priorities')).set(credentials).expect('Content-Type', 'application/json').expect(200);
// change name of first priority to a random name
const newNameLowercase = `${faker.lorem.word()}${faker.git.commitSha({ length: 6 })}`.toLowerCase();
await request
.put(api(`livechat/priorities/${priorities[0]._id}`))
.set(credentials)
.send({ name: newNameLowercase })
.expect('Content-Type', 'application/json')
.expect(200);
// change name of second priority to the name of first priority in different case and expect error
const response = await request
.put(api(`livechat/priorities/${priorities[1]._id}`))
.set(credentials)
.send({ name: newNameLowercase.toUpperCase() })
.expect('Content-Type', 'application/json')
.expect(400);
expect(response.body).to.have.property('success', false);
expect(response.body).to.have.property('error', 'error-duplicate-priority-name');
});
});
describe('livechat/priorities.reset', () => {
let priority: ILivechatPriority;
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await updatePermission('manage-livechat-priorities', []);
const response = await request.post(api('livechat/priorities.reset')).set(credentials).expect(403);
expect(response.body).to.have.property('success', false);
expect(response.body).to.have.property('error');
expect(response.body?.error).to.contain('error-unauthorized');
});
it('should respond reset:true when a priority has been changed', async () => {
await updatePermission('manage-livechat-priorities', ['admin', 'livechat-manager']);
const {
body: { priorities },
} = await request.get(api('livechat/priorities')).set(credentials).expect('Content-Type', 'application/json').expect(200);
priority = priorities[0];
priority.name = `${faker.lorem.word()}${faker.git.commitSha({ length: 6 })}`;
const responseChange = await request
.put(api(`livechat/priorities/${priority._id}`))
.set(credentials)
.send({
name: priority.name,
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(responseChange.body).to.have.property('success', true);
const response = await request
.get(api('livechat/priorities.reset'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(response.body).to.have.property('success', true);
expect(response.body).to.have.property('reset', true);
});
it('should reset all priorities', async () => {
const resetRespose = await request
.post(api('livechat/priorities.reset'))
.set(credentials)
.send()
.expect('Content-Type', 'application/json')
.expect(200);
expect(resetRespose.body).to.have.property('success', true);
});
it('should return reset: false after all priorities have been reset', async () => {
const response = await request
.get(api('livechat/priorities.reset'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(response.body).to.have.property('success', true);
expect(response.body).to.have.property('reset', false);
});
it('should change the priority name and dirty status when reset', async () => {
const response = await request
.get(api(`livechat/priorities/${priority._id}`))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(response.body).to.have.property('success', true);
expect(response.body).to.have.property('dirty', false);
expect(response.body).to.not.have.property('name');
});
it('should change all priorities to their default', async () => {
const response = await request
.get(api('livechat/priorities'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(response.body).to.have.property('success', true);
expect(response.body.priorities).to.be.an('array');
response.body.priorities.forEach((priority: ILivechatPriority) => {
expect(priority).to.not.have.property('name');
expect(priority).to.have.property('dirty', false);
});
});
it('should fail to reset when lacking permissions', async () => {
await updatePermission('manage-livechat-priorities', []);
const response = await request.post(api('livechat/priorities.reset')).set(credentials).expect(403);
expect(response.body).to.have.property('success', false);
expect(response.body).to.have.property('error');
expect(response.body?.error).to.contain('error-unauthorized');
});
});
describe('Inquiry queue sorting mechanism', () => {
let omniRooms: IOmnichannelRoom[];
let departmentWithAgent: Awaited<ReturnType<typeof createDepartmentWithAnOnlineAgent>>;
let priorities: ILivechatPriority[];
let slas: Awaited<ReturnType<typeof bulkCreateSLA>>;
const hasSlaProps = (inquiry: ILivechatInquiryRecord): inquiry is ILivechatInquiryRecord & { slaId: string } =>
!!inquiry.estimatedWaitingTimeQueue && !!inquiry.slaId;
const hasPriorityProps = (inquiry: ILivechatInquiryRecord): inquiry is ILivechatInquiryRecord & { priorityId: string } =>
inquiry.priorityId !== undefined;
const getPriorityOrderById = (id: string) => {
const priority = priorities.find((priority) => priority._id === id);
return priority?.sortItem || 0;
};
const sortBySLAProps = (inquiry1: ILivechatInquiryRecord, inquiry2: ILivechatInquiryRecord): number => {
if (hasSlaProps(inquiry1) || hasSlaProps(inquiry2)) {
if (hasSlaProps(inquiry1) && hasSlaProps(inquiry2)) {
// if both inquiries have sla props, then sort by estimatedWaitingTimeQueue: 1
const estimatedWaitingTimeQueue1 = inquiry1.estimatedWaitingTimeQueue;
const estimatedWaitingTimeQueue2 = inquiry2.estimatedWaitingTimeQueue;
if (estimatedWaitingTimeQueue1 !== estimatedWaitingTimeQueue2) {
return estimatedWaitingTimeQueue1 - estimatedWaitingTimeQueue2;
}
}
if (hasSlaProps(inquiry1)) {
return -1;
}
if (hasSlaProps(inquiry2)) {
return 1;
}
}
return 0;
};
const sortByPriorityProps = (inquiry1: ILivechatInquiryRecord, inquiry2: ILivechatInquiryRecord): number => {
if (hasPriorityProps(inquiry1) || hasPriorityProps(inquiry2)) {
if (hasPriorityProps(inquiry1) && hasPriorityProps(inquiry2)) {
// if both inquiries have priority props, then sort by priorityId: 1
return getPriorityOrderById(inquiry1.priorityId) - getPriorityOrderById(inquiry2.priorityId);
}
if (hasPriorityProps(inquiry1)) {
return -1;
}
if (hasPriorityProps(inquiry2)) {
return 1;
}
}
return 0;
};
// this should sort using logic - { ts: 1 }
const sortByTimestamp = (inquiry1: ILivechatInquiryRecord, inquiry2: ILivechatInquiryRecord) => {
return new Date(inquiry1.ts).getTime() - new Date(inquiry2.ts).getTime();
};
// this should sort using logic - { priorityWeight: 1, ts: 1 }
const sortByPriority = (inquiry1: ILivechatInquiryRecord, inquiry2: ILivechatInquiryRecord) => {
const priorityPropsSort = sortByPriorityProps(inquiry1, inquiry2);
if (priorityPropsSort !== 0) {
return priorityPropsSort;
}
return sortByTimestamp(inquiry1, inquiry2);
};
// this should sort using logic - { estimatedWaitingTimeQueue: 1, ts: 1 }
const sortBySLA = (inquiry1: ILivechatInquiryRecord, inquiry2: ILivechatInquiryRecord) => {
const slaPropsSort = sortBySLAProps(inquiry1, inquiry2);
if (slaPropsSort !== 0) {
return slaPropsSort;
}
return sortByTimestamp(inquiry1, inquiry2);
};
const filterUnnecessaryProps = (inquiries: ILivechatInquiryRecord[]) =>
inquiries.map((inquiry) => {
return {
_id: inquiry._id,
rid: inquiry.rid,
...(hasPriorityProps(inquiry) && {
priority: {
priorityWeight: getPriorityOrderById(inquiry.priorityId),
id: inquiry.priorityWeight,
},
}),
...(hasSlaProps(inquiry) && {
sla: {
estimatedWaitingTimeQueue: inquiry.estimatedWaitingTimeQueue,
},
}),
ts: inquiry.ts,
};
});
const compareInquiries = (inquiries1: ILivechatInquiryRecord[], inquiries2: ILivechatInquiryRecord[]) => {
const filteredInquiries1 = filterUnnecessaryProps(inquiries1);
const filteredInquiries2 = filterUnnecessaryProps(inquiries2);
expect(filteredInquiries1).to.deep.equal(filteredInquiries2);
};
it('it should create all the data required for further testing', async () => {
departmentWithAgent = await createDepartmentWithAnOnlineAgent();
const {
body: { priorities: prioritiesResponse },
} = (await request.get(api('livechat/priorities')).set(credentials).expect('Content-Type', 'application/json').expect(200)) as {
body: { priorities: ILivechatPriority[] };
};
priorities = prioritiesResponse;
await deleteAllSLA();
slas = await bulkCreateSLA(5);
type RoomParamsReturnType = { priority: string } | { sla: string } | { priority: string; sla: string } | undefined;
// create 20 rooms, 5 with only priority, 5 with only SLA, 5 with both, 5 without priority or SLA
omniRooms = await bulkCreateLivechatRooms(20, departmentWithAgent.department._id, (index): RoomParamsReturnType => {
if (index < 5) {
return {
priority: priorities[index]._id,
};
}
if (index < 10) {
return {
sla: slas[index - 5]._id,
};
}
if (index < 15) {
// random number between 0 and 4
const randomPriorityIndex = Math.floor(Math.random() * 5);
const randomSlaIndex = Math.floor(Math.random() * 5);
return {
priority: priorities[randomPriorityIndex]._id,
sla: slas[randomSlaIndex]._id,
};
}
});
});
it('it should sort the queue based on priority', async () => {
await updateEESetting('Omnichannel_sorting_mechanism', OmnichannelSortingMechanismSettingType.Priority);
const origInquiries = await fetchAllInquiries(departmentWithAgent.agent.credentials, departmentWithAgent.department._id);
expect(origInquiries.length).to.be.greaterThanOrEqual(omniRooms.length);
const expectedSortedInquiries: ILivechatInquiryRecord[] = JSON.parse(JSON.stringify(origInquiries));
expectedSortedInquiries.sort(sortByPriority);
compareInquiries(origInquiries, expectedSortedInquiries);
});
it('it should sort the queue based on slas', async () => {
await updateEESetting('Omnichannel_sorting_mechanism', OmnichannelSortingMechanismSettingType.SLAs);
const origInquiries = await fetchAllInquiries(departmentWithAgent.agent.credentials, departmentWithAgent.department._id);
expect(origInquiries.length).to.be.greaterThanOrEqual(omniRooms.length);
const expectedSortedInquiries = origInquiries.sort(sortBySLA);
compareInquiries(origInquiries, expectedSortedInquiries);
});
it('it should sort the queue based on timestamp', async () => {
await updateEESetting('Omnichannel_sorting_mechanism', OmnichannelSortingMechanismSettingType.Timestamp);
const origInquiries = await fetchAllInquiries(departmentWithAgent.agent.credentials, departmentWithAgent.department._id);
expect(origInquiries.length).to.be.greaterThanOrEqual(omniRooms.length);
const expectedSortedInquiries = origInquiries.sort(sortByTimestamp);
compareInquiries(origInquiries, expectedSortedInquiries);
});
});
});

View File

@@ -0,0 +1,242 @@
import type { ILivechatDepartment, ILivechatTag } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import { getCredentials, api, request, credentials } from '../../../data/api-data';
import { createDepartmentWithAnOnlineAgent } from '../../../data/livechat/department';
import { saveTags, removeAllTags } from '../../../data/livechat/tags';
import { createMonitor, createUnit } from '../../../data/livechat/units';
import { removePermissionFromAllRoles, restorePermissionToRoles, updatePermission, updateSetting } from '../../../data/permissions.helper';
import type { IUserWithCredentials } from '../../../data/user';
import { password } from '../../../data/user';
import { createUser, deleteUser, login } from '../../../data/users.helper';
import { IS_EE } from '../../../e2e/config/constants';
(IS_EE ? describe : describe.skip)('[EE] Livechat - Tags', () => {
before((done) => getCredentials(done));
before(async () => {
await updateSetting('Livechat_enabled', true);
});
describe('livechat/tags', () => {
let tagsData: {
caseA: { department: ILivechatDepartment; tag: ILivechatTag; agent: IUserWithCredentials };
caseB: { department: ILivechatDepartment; tag: ILivechatTag; agent: IUserWithCredentials };
casePublic: { tag: ILivechatTag };
};
let monitor: IUserWithCredentials;
before(async () => {
// create monitor
const monitorUser = await createUser();
const monitorCredentials = await login(monitorUser.username, password);
await createMonitor(monitorUser.username);
monitor = {
user: monitorUser,
credentials: monitorCredentials,
};
// remove all existing tags
await removeAllTags();
// should add 3 tags
const { department: dA, agent: agentA } = await createDepartmentWithAnOnlineAgent();
const tagA = await saveTags([dA._id]);
const { department: dB, agent: agentB } = await createDepartmentWithAnOnlineAgent();
const tagB = await saveTags([dB._id]);
const publicTag = await saveTags();
tagsData = {
caseA: { department: dA, tag: tagA, agent: agentA },
caseB: { department: dB, tag: tagB, agent: agentB },
casePublic: { tag: publicTag },
};
});
after(async () => {
await deleteUser(monitor.user);
await removeAllTags();
});
it('should throw unauthorized error when the user does not have the necessary permission', async () => {
await removePermissionFromAllRoles('manage-livechat-tags');
await removePermissionFromAllRoles('view-l-room');
const response = await request.get(api('livechat/tags')).set(credentials).expect('Content-Type', 'application/json').expect(403);
expect(response.body).to.have.property('success', false);
await restorePermissionToRoles('manage-livechat-tags');
await restorePermissionToRoles('view-l-room');
});
it('should return an array of tags', async () => {
const { tag } = tagsData.caseA;
const response = await request
.get(api('livechat/tags'))
.set(credentials)
.query({ text: tag.name, viewAll: 'true' })
.expect('Content-Type', 'application/json')
.expect(200);
expect(response.body).to.have.property('success', true);
expect(response.body.tags).to.be.an('array').with.lengthOf(1);
expect(response.body.tags[0]).to.have.property('_id', tag._id);
});
it('[Manager role] show return all tags when "viewAll" param is true and user has access to all tags', async () => {
const response = await request
.get(api('livechat/tags'))
.set(credentials)
.query({ viewAll: 'true' })
.expect('Content-Type', 'application/json')
.expect(200);
expect(response.body).to.have.property('success', true);
expect(response.body.tags).to.be.an('array').with.lengthOf(3);
const expectedTags = [tagsData.caseA.tag, tagsData.caseB.tag, tagsData.casePublic.tag].map((tag) => tag._id).sort();
const actualTags = response.body.tags.map((tag: ILivechatTag) => tag._id).sort();
expect(actualTags).to.deep.equal(expectedTags);
});
it('[Monitor role] show return only public tags when "viewAll" param is true and user does not have access to all tags', async () => {
const response = await request
.get(api('livechat/tags'))
.set(monitor.credentials)
.query({ viewAll: 'true' })
.expect('Content-Type', 'application/json')
.expect(200);
expect(response.body).to.have.property('success', true);
expect(response.body.tags).to.be.an('array').with.lengthOf(1);
const expectedTags = [tagsData.casePublic.tag].map((tag) => tag._id).sort();
const actualTags = response.body.tags.map((tag: ILivechatTag) => tag._id).sort();
expect(actualTags).to.deep.equal(expectedTags);
});
it('[Manager Role] should return department tags and public tags when "departmentId" param is provided', async () => {
const { department } = tagsData.caseA;
const response = await request
.get(api('livechat/tags'))
.set(credentials)
.query({ department: department._id })
.expect('Content-Type', 'application/json')
.expect(200);
expect(response.body).to.have.property('success', true);
expect(response.body.tags).to.be.an('array').with.lengthOf(2);
const expectedTags = [tagsData.caseA.tag, tagsData.casePublic.tag].map((tag) => tag._id).sort();
const actualTags = response.body.tags.map((tag: ILivechatTag) => tag._id).sort();
expect(actualTags).to.deep.equal(expectedTags);
});
it('[Manager role] should return public tags when "departmentId" param is not provided', async () => {
const response = await request.get(api('livechat/tags')).set(credentials).expect('Content-Type', 'application/json').expect(200);
expect(response.body).to.have.property('success', true);
expect(response.body.tags).to.be.an('array').with.lengthOf(1);
expect(response.body.tags[0]).to.have.property('_id', tagsData.casePublic.tag._id);
});
it('[Monitor role] should return only public tags when user does not have access to department', async () => {
const { department } = tagsData.caseA;
const response = await request
.get(api('livechat/tags'))
.set(monitor.credentials)
.query({ department: department._id })
.expect('Content-Type', 'application/json')
.expect(200);
expect(response.body).to.have.property('success', true);
expect(response.body.tags).to.be.an('array').with.lengthOf(1);
const expectedTags = [tagsData.casePublic.tag].map((tag) => tag._id).sort();
const actualTags = response.body.tags.map((tag: ILivechatTag) => tag._id).sort();
expect(actualTags).to.deep.equal(expectedTags);
});
it('[Monitor Role] should return department tags and public tags when "departmentId" param is provided', async () => {
const { department } = tagsData.caseA;
await createUnit(monitor.user._id, monitor.user.username || '', [department._id]);
const response = await request
.get(api('livechat/tags'))
.set(monitor.credentials)
.query({ department: department._id })
.expect('Content-Type', 'application/json')
.expect(200);
expect(response.body).to.have.property('success', true);
expect(response.body.tags).to.be.an('array').with.lengthOf(2);
const expectedTags = [tagsData.caseA.tag, tagsData.casePublic.tag].map((tag) => tag._id).sort();
const actualTags = response.body.tags.map((tag: ILivechatTag) => tag._id).sort();
expect(actualTags).to.deep.equal(expectedTags);
});
it('[Agent Role] should return department tags and public tags when "departmentId" param is provided', async () => {
const { department, agent } = tagsData.caseA;
const response = await request
.get(api('livechat/tags'))
.set(agent.credentials)
.query({ department: department._id })
.expect('Content-Type', 'application/json')
.expect(200);
expect(response.body).to.have.property('success', true);
expect(response.body.tags).to.be.an('array').with.lengthOf(2);
const expectedTags = [tagsData.caseA.tag, tagsData.casePublic.tag].map((tag) => tag._id).sort();
const actualTags = response.body.tags.map((tag: ILivechatTag) => tag._id).sort();
expect(actualTags).to.deep.equal(expectedTags);
});
it('[Monitor role] should return public tags and department tags which they are part of, when "departmentId" param is not provided', async () => {
const response = await request
.get(api('livechat/tags'))
.set(monitor.credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(response.body).to.have.property('success', true);
expect(response.body.tags).to.be.an('array').with.lengthOf(2);
const expectedTags = [tagsData.caseA.tag, tagsData.casePublic.tag].map((tag) => tag._id).sort();
const actualTags = response.body.tags.map((tag: ILivechatTag) => tag._id).sort();
expect(actualTags).to.deep.equal(expectedTags);
});
it('[Agent role] should return public tags when "departmentId" param is not provided', async () => {
const response = await request
.get(api('livechat/tags'))
.set(monitor.credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(response.body).to.have.property('success', true);
expect(response.body.tags).to.be.an('array').with.lengthOf(2);
const expectedTags = [tagsData.caseA.tag, tagsData.casePublic.tag].map((tag) => tag._id).sort();
const actualTags = response.body.tags.map((tag: ILivechatTag) => tag._id).sort();
expect(actualTags).to.deep.equal(expectedTags);
});
});
describe('livechat/tags/:tagId', () => {
it('should throw unauthorized error when the user does not have the necessary permission', async () => {
await updatePermission('manage-livechat-tags', []);
await updatePermission('view-l-room', []);
const response = await request.get(api('livechat/tags/123')).set(credentials).expect('Content-Type', 'application/json').expect(403);
expect(response.body).to.have.property('success', false);
});
it('should return null when the tag does not exist', async () => {
await updatePermission('manage-livechat-tags', ['admin']);
await updatePermission('view-l-room', ['livechat-agent']);
await request.get(api('livechat/tags/123')).set(credentials).expect('Content-Type', 'application/json').expect(404);
});
it('should return a tag', async () => {
const tag = await saveTags();
const response = await request
.get(api(`livechat/tags/${tag._id}`))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(response.body).to.have.property('success', true);
expect(response.body).to.have.property('_id', tag._id);
expect(response.body).to.have.property('name', tag.name);
expect(response.body).to.have.property('numDepartments', 0);
});
});
});

View File

@@ -0,0 +1,739 @@
import type { ILivechatDepartment, IOmnichannelBusinessUnit } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { before, after, describe, it } from 'mocha';
import { getCredentials, api, request, credentials } from '../../../data/api-data';
import { deleteDepartment, getDepartmentById } from '../../../data/livechat/department';
import { createDepartment, updateDepartment } from '../../../data/livechat/rooms';
import { createMonitor, createUnit, deleteUnit, getUnit } from '../../../data/livechat/units';
import { updatePermission, updateSetting } from '../../../data/permissions.helper';
import { password } from '../../../data/user';
import { createUser, deleteUser, login } from '../../../data/users.helper';
import { IS_EE } from '../../../e2e/config/constants';
(IS_EE ? describe : describe.skip)('[EE] LIVECHAT - Units', () => {
before((done) => getCredentials(done));
before(async () => {
await updateSetting('Livechat_enabled', true);
await updatePermission('manage-livechat-departments', ['livechat-manager', 'livechat-monitor', 'admin']);
await updateSetting('Omnichannel_enable_department_removal', true);
});
after(async () => {
await updateSetting('Omnichannel_enable_department_removal', false);
});
describe('[GET] livechat/units', () => {
it('should return empty if manage-livechat-units permission is missing', async () => {
await updatePermission('manage-livechat-units', []);
return request
.get(api('livechat/units'))
.set(credentials)
.send({
unitData: {
name: 'test',
enabled: true,
},
unitMonitors: [],
unitDepartments: [],
})
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('units').that.is.an('array').with.lengthOf(0);
});
});
it('should return a list of units', async () => {
await updatePermission('manage-livechat-units', ['admin']);
const user = await createUser();
await createMonitor(user.username);
const department = await createDepartment();
const unit = await createUnit(user._id, user.username, [department._id]);
const { body } = await request.get(api('livechat/units')).set(credentials).expect(200);
expect(body.units).to.be.an('array').with.lengthOf.greaterThan(0);
const unitFound = body.units.find((u: IOmnichannelBusinessUnit) => u._id === unit._id);
expect(unitFound).to.have.property('_id', unit._id);
expect(unitFound).to.have.property('name', unit.name);
expect(unitFound).to.have.property('numMonitors', 1);
expect(unitFound).to.have.property('numDepartments', 1);
expect(unitFound).to.have.property('type', 'u');
// cleanup
await deleteUser(user);
});
it('should return a list of units matching the provided filter', async () => {
const user = await createUser();
await createMonitor(user.username);
const department = await createDepartment();
const unit = await createUnit(user._id, user.username, [department._id]);
const { body } = await request.get(api('livechat/units')).query({ text: unit.name }).set(credentials).expect(200);
expect(body.units).to.be.an('array').with.lengthOf(1);
const unitFound = body.units.find((u: IOmnichannelBusinessUnit) => u._id === unit._id);
expect(unitFound).to.have.property('_id', unit._id);
expect(unitFound).to.have.property('name', unit.name);
expect(unitFound).to.have.property('numMonitors', 1);
expect(unitFound).to.have.property('numDepartments', 1);
expect(unitFound).to.have.property('type', 'u');
// cleanup
await deleteUser(user);
});
it('should properly paginate the result set', async () => {
const { body } = await request.get(api('livechat/units')).query({ count: 1 }).set(credentials).expect(200);
expect(body).to.have.property('units').and.to.be.an('array').with.lengthOf(1);
expect(body).to.have.property('count').and.to.be.equal(1);
const unit = body.units[0];
const { body: body2 } = await request.get(api('livechat/units')).query({ count: 1, offset: 1 }).set(credentials).expect(200);
expect(body2).to.have.property('units').and.to.be.an('array').with.lengthOf(1);
const unit2 = body2.units[0];
expect(unit._id).to.not.be.equal(unit2._id);
});
it('should sort the result set based on provided fields', async () => {
const user = await createUser();
await createMonitor(user.username);
const department = await createDepartment();
const unit = await createUnit(user._id, user.username, [department._id], 'A test 1234');
const unit2 = await createUnit(user._id, user.username, [department._id], 'a test 1234');
const { body } = await request
.get(api('livechat/units'))
.query({ sort: JSON.stringify({ name: 1 }), text: 'test', count: 2 })
.set(credentials)
.expect(200);
expect(body).to.have.property('units').and.to.be.an('array').with.lengthOf(2);
expect(body.units[0]._id).to.be.equal(unit._id);
expect(body.units[1]._id).to.be.equal(unit2._id);
await deleteUser(user);
});
});
describe('[POST] livechat/units', () => {
it('should fail if manage-livechat-units permission is missing', async () => {
await updatePermission('manage-livechat-units', []);
return request
.post(api('livechat/units'))
.set(credentials)
.send({
unitData: {
name: 'test',
enabled: true,
},
unitMonitors: [],
unitDepartments: [],
})
.expect(403);
});
it('should return a created unit', async () => {
await updatePermission('manage-livechat-units', ['admin']);
const user = await createUser();
await createMonitor(user.username);
const department = await createDepartment();
const { body } = await request
.post(api('livechat/units'))
.set(credentials)
.send({
unitData: { name: 'test', visibility: 'public', enabled: true, description: 'test' },
unitMonitors: [{ monitorId: user._id, username: user.username }],
unitDepartments: [{ departmentId: department._id }],
})
.expect(200);
expect(body).to.have.property('_id');
expect(body).to.have.property('name', 'test');
expect(body).to.have.property('visibility', 'public');
expect(body).to.have.property('type', 'u');
expect(body).to.have.property('numMonitors', 1);
expect(body).to.have.property('numDepartments', 1);
// cleanup
await deleteUser(user);
});
it('should return a unit with no monitors if a user who is not a monitor is passed', async () => {
await updatePermission('manage-livechat-units', ['admin']);
const user = await createUser();
const department = await createDepartment();
const { body } = await request
.post(api('livechat/units'))
.set(credentials)
.send({
unitData: { name: 'test', visibility: 'public', enabled: true, description: 'test' },
unitMonitors: [{ monitorId: user._id, username: user.username }],
unitDepartments: [{ departmentId: department._id }],
})
.expect(200);
expect(body).to.have.property('numMonitors', 0);
expect(body).to.have.property('name', 'test');
// cleanup
await deleteUser(user);
});
});
describe('[GET] livechat/units/:id', () => {
it('should fail if manage-livechat-units permission is missing', async () => {
await updatePermission('manage-livechat-units', []);
return request.get(api('livechat/units/123')).set(credentials).send().expect(403);
});
it('should return a unit', async () => {
await updatePermission('manage-livechat-units', ['admin']);
const user = await createUser();
await createMonitor(user.username);
const department = await createDepartment();
const unit = await createUnit(user._id, user.username, [department._id]);
const { body } = await request
.get(api(`livechat/units/${unit._id}`))
.set(credentials)
.expect(200);
expect(body).to.have.property('_id', unit._id);
expect(body).to.have.property('name', unit.name);
expect(body).to.have.property('numMonitors', 1);
expect(body).to.have.property('numDepartments', 1);
expect(body).to.have.property('type', 'u');
// cleanup
await deleteUser(user);
});
});
describe('[POST] livechat/units/:id', () => {
it('should fail if manage-livechat-units permission is missing', async () => {
await updatePermission('manage-livechat-units', []);
return request.post(api('livechat/units/123')).set(credentials).expect(403);
});
it('should fail if unit does not exist', async () => {
await updatePermission('manage-livechat-units', ['admin']);
const user = await createUser();
await createMonitor(user.username);
const department = await createDepartment();
const { body } = await request
.post(api('livechat/units/123'))
.set(credentials)
.send({
unitData: { name: 'test', visibility: 'public', enabled: true, description: 'test' },
unitMonitors: [{ monitorId: user._id, username: user.username }],
unitDepartments: [{ departmentId: department._id }],
})
.expect(400);
expect(body).to.have.property('success', false);
// cleanup
await deleteUser(user);
});
it('should return a updated unit', async () => {
const user = await createUser();
await createMonitor(user.username);
const department = await createDepartment();
const unit = await createUnit(user._id, user.username, [department._id]);
const { body } = await request
.post(api(`livechat/units/${unit._id}`))
.set(credentials)
.send({
unitData: { name: 'test', visibility: 'private', enabled: true, description: 'test' },
unitMonitors: [{ monitorId: user._id, username: user.username }],
unitDepartments: [{ departmentId: department._id }],
})
.expect(200);
expect(body).to.have.property('_id');
expect(body).to.have.property('name', 'test');
expect(body).to.have.property('visibility', 'private');
expect(body).to.have.property('type', 'u');
expect(body).to.have.property('numMonitors', 1);
expect(body).to.have.property('numDepartments', 1);
// cleanup
await deleteUser(user);
});
it('should move the department to the latest unit that attempted to assign it', async () => {
const user = await createUser();
await createMonitor(user.username);
const department = await createDepartment();
const unit1 = await createUnit(user._id, user.username, [department._id]);
const unit2 = await createUnit(user._id, user.username, [department._id]);
const { body } = await request
.get(api(`livechat/units/${unit1._id}/departments`))
.set(credentials)
.expect(200);
expect(body).to.have.property('departments');
expect(body.departments).to.have.lengthOf(0);
expect(unit2.numDepartments).to.be.equal(1);
});
it('should remove the department from the unit if it is not passed in the request', async () => {
const user = await createUser();
await createMonitor(user.username);
const department = await createDepartment();
const unit1 = await createUnit(user._id, user.username, [department._id]);
const { body } = await request
.post(api(`livechat/units/${unit1._id}`))
.set(credentials)
.send({
unitData: { name: unit1.name, visibility: unit1.visibility },
unitMonitors: [{ monitorId: user._id, username: user.username }],
unitDepartments: [],
})
.expect(200);
expect(body).to.have.property('_id', unit1._id);
expect(body).to.have.property('numDepartments', 0);
});
it('should remove the monitor from the unit if it is not passed in the request', async () => {
const user = await createUser();
await createMonitor(user.username);
const department = await createDepartment();
const unit1 = await createUnit(user._id, user.username, [department._id]);
const { body } = await request
.post(api(`livechat/units/${unit1._id}`))
.set(credentials)
.send({
unitData: { name: unit1.name, visibility: unit1.visibility },
unitMonitors: [],
unitDepartments: [{ departmentId: department._id }],
})
.expect(200);
expect(body).to.have.property('_id', unit1._id);
expect(body).to.have.property('numMonitors', 0);
});
});
describe('[DELETE] livechat/units/:id', () => {
it('should fail if manage-livechat-units permission is missing', async () => {
await updatePermission('manage-livechat-units', []);
return request.delete(api('livechat/units/123')).set(credentials).expect(403);
});
it('should return a deleted unit', async () => {
await updatePermission('manage-livechat-units', ['admin']);
const user = await createUser();
await createMonitor(user.username);
const department = await createDepartment();
const unit = await createUnit(user._id, user.username, [department._id]);
const { body } = await request
.delete(api(`livechat/units/${unit._id}`))
.set(credentials)
.expect(200);
expect(body).to.have.be.a('number').equal(1);
// cleanup
await deleteUser(user);
});
});
describe('livechat/units/:unitId/departments', () => {
it('should fail if manage-livechat-units permission is missing', async () => {
await updatePermission('manage-livechat-units', []);
return request.get(api('livechat/units/123/departments')).set(credentials).expect(403);
});
it('should return departments associated with a unit', async () => {
await updatePermission('manage-livechat-units', ['admin']);
const user = await createUser();
await createMonitor(user.username);
const department = await createDepartment();
const unit = await createUnit(user._id, user.username, [department._id]);
const { body } = await request
.get(api(`livechat/units/${unit._id}/departments`))
.set(credentials)
.expect(200);
expect(body).to.have.property('departments');
expect(body.departments).to.have.lengthOf(1);
expect(body.departments[0]).to.have.property('_id', department._id);
expect(body.departments[0]).to.have.property('name', department.name);
// cleanup
await deleteUser(user);
});
});
describe('livechat/units/:unitId/departments/available', () => {
it('should fail if manage-livechat-units permission is missing', async () => {
await updatePermission('manage-livechat-units', []);
return request.get(api('livechat/units/123/departments/available')).set(credentials).expect(403);
});
it('should return departments not associated with a unit', async () => {
await updatePermission('manage-livechat-units', ['admin']);
const user = await createUser();
await createMonitor(user.username);
const department = await createDepartment();
const unit = await createUnit(user._id, user.username, [department._id]);
const { body } = await request
.get(api(`livechat/units/${unit._id}/departments/available`))
.set(credentials)
.expect(200);
expect(body).to.have.property('departments');
expect(body.departments).to.have.lengthOf.greaterThan(0);
const myUnit = body.departments.find((d: ILivechatDepartment) => d.parentId === unit._id);
expect(myUnit).to.not.be.undefined.and.not.be.null;
// cleanup
await deleteUser(user);
});
});
describe('livechat/units/:unitId/monitors', () => {
it('should fail if manage-livechat-units permission is missing', async () => {
await updatePermission('manage-livechat-monitors', []);
return request.get(api('livechat/units/123/monitors')).set(credentials).expect(403);
});
it('should return monitors associated with a unit', async () => {
await updatePermission('manage-livechat-monitors', ['admin']);
const user = await createUser();
await createMonitor(user.username);
const department = await createDepartment();
const unit = await createUnit(user._id, user.username, [department._id]);
const { body } = await request
.get(api(`livechat/units/${unit._id}/monitors`))
.set(credentials)
.expect(200);
expect(body).to.have.property('monitors');
expect(body.monitors).to.have.lengthOf(1);
expect(body.monitors[0]).to.have.property('monitorId', user._id);
expect(body.monitors[0]).to.have.property('username', user.username);
// cleanup
await deleteUser(user);
});
});
describe('[POST] livechat/department', () => {
let monitor1: Awaited<ReturnType<typeof createUser>>;
let monitor1Credentials: Awaited<ReturnType<typeof login>>;
let monitor2: Awaited<ReturnType<typeof createUser>>;
let monitor2Credentials: Awaited<ReturnType<typeof login>>;
let unit: IOmnichannelBusinessUnit;
before(async () => {
monitor1 = await createUser();
monitor2 = await createUser();
await createMonitor(monitor1.username);
monitor1Credentials = await login(monitor1.username, password);
await createMonitor(monitor2.username);
monitor2Credentials = await login(monitor2.username, password);
unit = await createUnit(monitor1._id, monitor1.username, []);
});
after(async () => Promise.all([deleteUser(monitor1), deleteUser(monitor2), deleteUnit(unit)]));
it('should fail creating department when providing an invalid property in the department unit object', () => {
return request
.post(api('livechat/department'))
.set(credentials)
.send({
department: { name: 'Fail-Test', enabled: true, showOnOfflineForm: true, showOnRegistration: true, email: 'bla@bla' },
departmentUnit: { invalidProperty: true },
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'invalid-params');
});
});
it('should fail creating a department into an existing unit that a monitor does not supervise', async () => {
const department = await createDepartment(undefined, undefined, undefined, undefined, { _id: unit._id }, monitor2Credentials);
const updatedUnit = await getUnit(unit._id);
expect(updatedUnit).to.have.property('name', unit.name);
expect(updatedUnit).to.have.property('numMonitors', 1);
expect(updatedUnit).to.have.property('numDepartments', 0);
const fullDepartment = await getDepartmentById(department._id);
expect(fullDepartment).to.not.have.property('parentId');
expect(fullDepartment).to.not.have.property('ancestors');
await deleteDepartment(department._id);
});
it('should succesfully create a department into an existing unit as an admin', async () => {
const department = await createDepartment(undefined, undefined, undefined, undefined, { _id: unit._id });
const updatedUnit = await getUnit(unit._id);
expect(updatedUnit).to.have.property('name', unit.name);
expect(updatedUnit).to.have.property('numMonitors', 1);
expect(updatedUnit).to.have.property('numDepartments', 1);
const fullDepartment = await getDepartmentById(department._id);
expect(fullDepartment).to.have.property('parentId', unit._id);
expect(fullDepartment).to.have.property('ancestors').that.is.an('array').with.lengthOf(1);
expect(fullDepartment.ancestors?.[0]).to.equal(unit._id);
await deleteDepartment(department._id);
});
it('should succesfully create a department into an existing unit that a monitor supervises', async () => {
const department = await createDepartment(undefined, undefined, undefined, undefined, { _id: unit._id }, monitor1Credentials);
const updatedUnit = await getUnit(unit._id);
expect(updatedUnit).to.have.property('name', unit.name);
expect(updatedUnit).to.have.property('numMonitors', 1);
expect(updatedUnit).to.have.property('numDepartments', 1);
const fullDepartment = await getDepartmentById(department._id);
expect(fullDepartment).to.have.property('parentId', unit._id);
expect(fullDepartment).to.have.property('ancestors').that.is.an('array').with.lengthOf(1);
expect(fullDepartment.ancestors?.[0]).to.equal(unit._id);
await deleteDepartment(department._id);
});
it('unit should end up with 0 departments after removing all of them', async () => {
const updatedUnit = await getUnit(unit._id);
expect(updatedUnit).to.have.property('name', unit.name);
expect(updatedUnit).to.have.property('numMonitors', 1);
expect(updatedUnit).to.have.property('numDepartments', 0);
});
});
describe('[PUT] livechat/department', () => {
let monitor1: Awaited<ReturnType<typeof createUser>>;
let monitor1Credentials: Awaited<ReturnType<typeof login>>;
let monitor2: Awaited<ReturnType<typeof createUser>>;
let monitor2Credentials: Awaited<ReturnType<typeof login>>;
let unit: IOmnichannelBusinessUnit;
let department: ILivechatDepartment;
let baseDepartment: ILivechatDepartment;
before(async () => {
monitor1 = await createUser();
monitor2 = await createUser();
await createMonitor(monitor1.username);
monitor1Credentials = await login(monitor1.username, password);
await createMonitor(monitor2.username);
monitor2Credentials = await login(monitor2.username, password);
department = await createDepartment();
baseDepartment = await createDepartment();
unit = await createUnit(monitor1._id, monitor1.username, [baseDepartment._id]);
});
after(async () =>
Promise.all([
deleteUser(monitor1),
deleteUser(monitor2),
deleteUnit(unit),
deleteDepartment(department._id),
deleteDepartment(baseDepartment._id),
]),
);
it("should fail updating a department's unit when providing an invalid property in the department unit object", () => {
const updatedName = 'updated-department-name';
return request
.put(api(`livechat/department/${department._id}`))
.set(credentials)
.send({
department: { name: updatedName, enabled: true, showOnOfflineForm: true, showOnRegistration: true, email: 'bla@bla' },
departmentUnit: { invalidProperty: true },
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'Match error: Unknown key in field departmentUnit.invalidProperty');
});
});
it("should fail updating a department's unit when providing an invalid _id type in the department unit object", () => {
const updatedName = 'updated-department-name';
return request
.put(api(`livechat/department/${department._id}`))
.set(credentials)
.send({
department: { name: updatedName, enabled: true, showOnOfflineForm: true, showOnRegistration: true, email: 'bla@bla' },
departmentUnit: { _id: true },
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'Match error: Expected string, got boolean in field departmentUnit._id');
});
});
it('should fail removing the last department from a unit', () => {
const updatedName = 'updated-department-name';
return request
.put(api(`livechat/department/${baseDepartment._id}`))
.set(credentials)
.send({
department: { name: updatedName, enabled: true, showOnOfflineForm: true, showOnRegistration: true, email: 'bla@bla' },
departmentUnit: {},
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'error-unit-cant-be-empty');
});
});
it('should succesfully add an existing department to a unit as an admin', async () => {
const updatedName = 'updated-department-name';
const updatedDepartment = await updateDepartment({
userCredentials: credentials,
departmentId: department._id,
name: updatedName,
departmentUnit: { _id: unit._id },
});
expect(updatedDepartment).to.have.property('name', updatedName);
expect(updatedDepartment).to.have.property('type', 'd');
expect(updatedDepartment).to.have.property('_id', department._id);
const updatedUnit = await getUnit(unit._id);
expect(updatedUnit).to.have.property('name', unit.name);
expect(updatedUnit).to.have.property('numMonitors', 1);
expect(updatedUnit).to.have.property('numDepartments', 2);
const fullDepartment = await getDepartmentById(department._id);
expect(fullDepartment).to.have.property('parentId', unit._id);
expect(fullDepartment).to.have.property('ancestors').that.is.an('array').with.lengthOf(1);
expect(fullDepartment.ancestors?.[0]).to.equal(unit._id);
});
it('should succesfully remove an existing department from a unit as an admin', async () => {
const updatedName = 'updated-department-name';
const updatedDepartment = await updateDepartment({
userCredentials: credentials,
departmentId: department._id,
name: updatedName,
departmentUnit: {},
});
expect(updatedDepartment).to.have.property('name', updatedName);
expect(updatedDepartment).to.have.property('type', 'd');
expect(updatedDepartment).to.have.property('_id', department._id);
const updatedUnit = await getUnit(unit._id);
expect(updatedUnit).to.have.property('name', unit.name);
expect(updatedUnit).to.have.property('numMonitors', 1);
expect(updatedUnit).to.have.property('numDepartments', 1);
const fullDepartment = await getDepartmentById(department._id);
expect(fullDepartment).to.not.have.property('parentId');
expect(fullDepartment).to.not.have.property('ancestors');
});
it('should fail adding a department into an existing unit that a monitor does not supervise', async () => {
const updatedName = 'updated-department-name2';
const updatedDepartment = await updateDepartment({
userCredentials: monitor2Credentials,
departmentId: department._id,
name: updatedName,
departmentUnit: { _id: unit._id },
});
expect(updatedDepartment).to.have.property('name', updatedName);
expect(updatedDepartment).to.have.property('type', 'd');
expect(updatedDepartment).to.have.property('_id', department._id);
const updatedUnit = await getUnit(unit._id);
expect(updatedUnit).to.have.property('name', unit.name);
expect(updatedUnit).to.have.property('numMonitors', 1);
expect(updatedUnit).to.have.property('numDepartments', 1);
const fullDepartment = await getDepartmentById(department._id);
expect(fullDepartment).to.not.have.property('parentId');
expect(fullDepartment).to.not.have.property('ancestors');
});
it('should succesfully add a department into an existing unit that a monitor supervises', async () => {
const updatedName = 'updated-department-name3';
const updatedDepartment = await updateDepartment({
userCredentials: monitor1Credentials,
departmentId: department._id,
name: updatedName,
departmentUnit: { _id: unit._id },
});
expect(updatedDepartment).to.have.property('name', updatedName);
expect(updatedDepartment).to.have.property('type', 'd');
expect(updatedDepartment).to.have.property('_id', department._id);
const updatedUnit = await getUnit(unit._id);
expect(updatedUnit).to.have.property('name', unit.name);
expect(updatedUnit).to.have.property('numMonitors', 1);
expect(updatedUnit).to.have.property('numDepartments', 2);
const fullDepartment = await getDepartmentById(department._id);
expect(fullDepartment).to.have.property('name', updatedName);
expect(fullDepartment).to.have.property('parentId', unit._id);
expect(fullDepartment).to.have.property('ancestors').that.is.an('array').with.lengthOf(1);
expect(fullDepartment.ancestors?.[0]).to.equal(unit._id);
});
it('should fail removing a department from a unit that a monitor does not supervise', async () => {
const updatedName = 'updated-department-name4';
const updatedDepartment = await updateDepartment({
userCredentials: monitor2Credentials,
departmentId: department._id,
name: updatedName,
departmentUnit: {},
});
expect(updatedDepartment).to.have.property('name', updatedName);
expect(updatedDepartment).to.have.property('type', 'd');
expect(updatedDepartment).to.have.property('_id', department._id);
const updatedUnit = await getUnit(unit._id);
expect(updatedUnit).to.have.property('name', unit.name);
expect(updatedUnit).to.have.property('numMonitors', 1);
expect(updatedUnit).to.have.property('numDepartments', 2);
const fullDepartment = await getDepartmentById(department._id);
expect(fullDepartment).to.have.property('name', updatedName);
expect(fullDepartment).to.have.property('parentId', unit._id);
expect(fullDepartment).to.have.property('ancestors').that.is.an('array').with.lengthOf(1);
expect(fullDepartment.ancestors?.[0]).to.equal(unit._id);
});
it('should succesfully remove a department from a unit that a monitor supervises', async () => {
const updatedName = 'updated-department-name5';
const updatedDepartment = await updateDepartment({
userCredentials: monitor1Credentials,
departmentId: department._id,
name: updatedName,
departmentUnit: {},
});
expect(updatedDepartment).to.have.property('name', updatedName);
expect(updatedDepartment).to.have.property('type', 'd');
expect(updatedDepartment).to.have.property('_id', department._id);
const updatedUnit = await getUnit(unit._id);
expect(updatedUnit).to.have.property('name', unit.name);
expect(updatedUnit).to.have.property('numMonitors', 1);
expect(updatedUnit).to.have.property('numDepartments', 1);
const fullDepartment = await getDepartmentById(department._id);
expect(fullDepartment).to.have.property('name', updatedName);
expect(fullDepartment).to.not.have.property('parentId');
expect(fullDepartment).to.not.have.property('ancestors');
});
});
});

View File

@@ -0,0 +1,296 @@
import { faker } from '@faker-js/faker';
import type { IOmnichannelCannedResponse } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { before, describe, it } from 'mocha';
import { getCredentials, api, request, credentials } from '../../../data/api-data';
import { createCannedResponse } from '../../../data/livechat/canned-responses';
import { createAgent, createDepartment } from '../../../data/livechat/rooms';
import { removeTag, saveTags } from '../../../data/livechat/tags';
import { createMonitor, createUnit } from '../../../data/livechat/units';
import { updatePermission, updateSetting } from '../../../data/permissions.helper';
import { password, adminUsername } from '../../../data/user';
import { createUser, login } from '../../../data/users.helper';
import { IS_EE } from '../../../e2e/config/constants';
(IS_EE ? describe : describe.skip)('[EE] LIVECHAT - Canned responses', () => {
before((done) => getCredentials(done));
before(async () => {
await updateSetting('Livechat_enabled', true);
});
describe('canned-responses.get', () => {
it('should throw unauthorized when view-canned-responses permission is not set', async () => {
await updatePermission('view-canned-responses', []);
return request.get(api('canned-responses.get')).set(credentials).expect(403);
});
it('should return an array of canned responses when available', async () => {
await updatePermission('view-canned-responses', ['livechat-agent', 'livechat-monitor', 'livechat-manager', 'admin']);
await createCannedResponse();
const { body } = await request.get(api('canned-responses.get')).set(credentials).expect(200);
expect(body).to.have.property('success', true);
expect(body.responses).to.be.an('array').with.lengthOf.greaterThan(0);
expect(body.responses[0]).to.have.property('_id');
expect(body.responses[0]).to.have.property('shortcut');
expect(body.responses[0]).to.have.property('scope');
expect(body.responses[0]).to.have.property('tags');
expect(body.responses[0]).to.have.property('text');
expect(body.responses[0]).to.have.property('userId');
});
it('should return canned responses for monitors even if department is disabled', async () => {
const user = await createUser();
const monitor = await createMonitor(user.username);
const creds = await login(user.username, password);
const department = await createDepartment(undefined, undefined, false);
await createUnit(monitor._id, user.username, [department._id]);
// create canned response
const shortcut = faker.string.uuid();
await request
.post(api('canned-responses'))
.set(credentials)
.send({ shortcut, scope: 'department', tags: ['tag'], text: 'text', departmentId: department._id })
.expect(200);
const { body } = await request.get(api('canned-responses.get')).set(creds).expect(200);
expect(body).to.have.property('success', true);
expect(body.responses).to.be.an('array').with.lengthOf.greaterThan(0);
expect(body.responses.find((response: IOmnichannelCannedResponse) => response.shortcut === shortcut)).to.be.an('object');
});
it('should return canned respones for scope user when agent is not on any department', async () => {
const user = await createUser();
await createAgent(user.username);
const creds = await login(user.username, password);
// create canned response
const shortcut = faker.string.uuid();
await request
.post(api('canned-responses'))
.set(creds)
.send({ shortcut, scope: 'user', tags: ['tag'], text: 'text' })
.expect(200);
const { body } = await request.get(api('canned-responses.get')).set(creds).expect(200);
expect(body).to.have.property('success', true);
expect(body.responses).to.be.an('array').with.lengthOf.greaterThan(0);
expect(body.responses.find((response: IOmnichannelCannedResponse) => response.shortcut === shortcut)).to.be.an('object');
});
it('should return canned responses on the global scope', async () => {
const user = await createUser();
const creds = await login(user.username, password);
await createAgent(user.username);
// create canned response
const shortcut = faker.string.uuid();
await request.post(api('canned-responses')).set(credentials).send({ shortcut, scope: 'global', text: 'text' }).expect(200);
const { body } = await request.get(api('canned-responses.get')).set(creds).expect(200);
expect(body).to.have.property('success', true);
expect(body.responses).to.be.an('array').with.lengthOf.greaterThan(0);
expect(body.responses.find((response: IOmnichannelCannedResponse) => response.shortcut === shortcut)).to.be.an('object');
});
it('should return canned responses from the departments user is in', async () => {
const user = await createUser();
const creds = await login(user.username, password);
await createAgent(user.username);
const department = await createDepartment([{ agentId: user._id }], undefined, true);
// create canned response
const shortcut = faker.string.uuid();
await request
.post(api('canned-responses'))
.set(credentials)
.send({ shortcut, scope: 'department', text: 'text', departmentId: department._id })
.expect(200);
const { body } = await request.get(api('canned-responses.get')).set(creds).expect(200);
expect(body).to.have.property('success', true);
expect(body.responses).to.be.an('array').with.lengthOf.greaterThan(0);
expect(body.responses.find((response: IOmnichannelCannedResponse) => response.shortcut === shortcut)).to.be.an('object');
});
});
describe('[GET] canned-responses', () => {
it('should fail if user dont have view-canned-responses permission', async () => {
await updatePermission('view-canned-responses', []);
return request.get(api('canned-responses')).set(credentials).expect(403);
});
it('should return an array of canned responses when available', async () => {
await updatePermission('view-canned-responses', ['livechat-agent', 'livechat-monitor', 'livechat-manager', 'admin']);
await createCannedResponse();
const { body } = await request.get(api('canned-responses')).query({ sort: '{ "_createdAt": -1 }' }).set(credentials).expect(200);
expect(body).to.have.property('success', true);
expect(body.cannedResponses).to.be.an('array').with.lengthOf.greaterThan(0);
expect(body.cannedResponses[0]).to.have.property('_id');
expect(body.cannedResponses[0]).to.have.property('shortcut');
expect(body.cannedResponses[0]).to.have.property('scope');
expect(body.cannedResponses[0]).to.have.property('tags');
expect(body.cannedResponses[0]).to.have.property('text');
expect(body.cannedResponses[0]).to.have.property('userId');
});
it('should return a canned response matching the params provided (shortcut)', async () => {
const response = await createCannedResponse();
const { body } = await request.get(api('canned-responses')).set(credentials).query({ shortcut: response.shortcut }).expect(200);
expect(body).to.have.property('success', true);
expect(body.cannedResponses).to.be.an('array').with.lengthOf(1);
expect(body.cannedResponses[0]).to.have.property('_id');
expect(body.cannedResponses[0]).to.have.property('shortcut', response.shortcut);
});
it('should return a canned response matching the params provided (scope)', async () => {
const response = await createCannedResponse();
const { body } = await request.get(api('canned-responses')).set(credentials).query({ scope: response.scope }).expect(200);
expect(body).to.have.property('success', true);
expect(body.cannedResponses).to.be.an('array').with.lengthOf.greaterThan(0);
expect(body.cannedResponses[0]).to.have.property('_id');
expect(body.cannedResponses[0]).to.have.property('scope', response.scope);
});
it('should return a canned response matching the params provided (tags)', async () => {
const response = await createCannedResponse();
const { body } = await request.get(api('canned-responses')).set(credentials).query({ 'tags[]': response.tags?.[0] }).expect(200);
expect(body).to.have.property('success', true);
expect(body.cannedResponses).to.be.an('array').with.lengthOf(1);
expect(body.cannedResponses[0]).to.have.property('_id');
expect(body.cannedResponses[0]).to.have.property('tags').that.is.an('array').which.includes(response.tags?.[0]);
});
it('should return a canned response matching the params provided (text)', async () => {
const response = await createCannedResponse();
const { body } = await request.get(api('canned-responses')).set(credentials).query({ text: response.text }).expect(200);
expect(body).to.have.property('success', true);
expect(body.cannedResponses).to.be.an('array').with.lengthOf(1);
expect(body.cannedResponses[0]).to.have.property('_id');
expect(body.cannedResponses[0]).to.have.property('text', response.text);
});
});
describe('[POST] canned-responses', () => {
const dupshortcut = `shortcut-${faker.string.nanoid(6)}`;
it('should fail if user dont have save-canned-responses permission', async () => {
await updatePermission('save-canned-responses', []);
return request
.post(api('canned-responses'))
.set(credentials)
.send({ shortcut: 'shortcut', scope: 'user', tags: ['tag'], text: 'text' })
.expect(403);
});
it('should fail if shortcut is not on the request', async () => {
await updatePermission('save-canned-responses', ['livechat-agent', 'livechat-monitor', 'livechat-manager', 'admin']);
return request.post(api('canned-responses')).set(credentials).expect(400);
});
it('should fail if text is not on the request', async () => {
return request.post(api('canned-responses')).set(credentials).send({ shortcut: 'shortcut' }).expect(400);
});
it('should fail if scope is not on the request', async () => {
return request.post(api('canned-responses')).set(credentials).send({ shortcut: 'shortcut', text: 'text' }).expect(400);
});
it('should fail if tags is not an array of strings', async () => {
return request
.post(api('canned-responses'))
.set(credentials)
.send({ shortcut: 'shortcut', text: 'text', scope: 'department', tags: 'tag' })
.expect(400);
});
it('should create a new canned response', async () => {
const { body } = await request
.post(api('canned-responses'))
.set(credentials)
.send({ shortcut: dupshortcut, scope: 'user', tags: ['tag'], text: 'text' })
.expect(200);
expect(body).to.have.property('success', true);
});
it('should fail if shortcut is already in use', async () => {
return request
.post(api('canned-responses'))
.set(credentials)
.send({ shortcut: dupshortcut, scope: 'user', tags: ['tag'], text: 'text' })
.expect(400);
});
it('should save a canned response related to an EE tag', async () => {
const tag = await saveTags();
const { body } = await request
.post(api('canned-responses'))
.set(credentials)
.send({ shortcut: `eetag-${faker.string.nanoid(6)}`, scope: 'user', tags: [tag.name], text: 'text' })
.expect(200);
expect(body).to.have.property('success', true);
const { body: getResult } = await request.get(api('canned-responses')).set(credentials).query({ 'tags[]': tag.name }).expect(200);
expect(getResult).to.have.property('success', true);
expect(getResult.cannedResponses).to.be.an('array').with.lengthOf(1);
expect(getResult.cannedResponses[0]).to.have.property('_id');
expect(getResult.cannedResponses[0]).to.have.property('tags').that.is.an('array').which.includes(tag.name);
});
it('should not include removed tags in the response', async () => {
const tag = await saveTags();
const { body } = await request
.post(api('canned-responses'))
.set(credentials)
.send({ shortcut: `remove-${faker.string.nanoid(6)}`, scope: 'user', tags: [tag.name], text: 'text' })
.expect(200);
expect(body).to.have.property('success', true);
await removeTag(tag._id);
const { body: getResult } = await request.get(api('canned-responses')).set(credentials).query({ 'tags[]': tag.name }).expect(200);
expect(getResult).to.have.property('success', true);
expect(getResult.cannedResponses).to.be.an('array').with.lengthOf(0);
});
it('should persist userId when updating a canned response', async () => {
const response = await createCannedResponse();
const {
body: { cannedResponses },
} = await request.get(api('canned-responses')).set(credentials).query({}).expect(200);
const targetCannedResponse = cannedResponses.find(
(cannedResponse: IOmnichannelCannedResponse) => cannedResponse.shortcut === response.shortcut,
);
const { body } = await request
.post(api('canned-responses'))
.set(credentials)
.send({ _id: targetCannedResponse._id, shortcut: `${targetCannedResponse.shortcut}-edited`, text: 'edited text', scope: 'user' })
.expect(200);
expect(body).to.have.property('success', true);
const { body: getResult } = await request
.get(api('canned-responses'))
.set(credentials)
.query({ shortcut: `${targetCannedResponse.shortcut}-edited` })
.expect(200);
expect(getResult).to.have.property('success', true);
expect(getResult.cannedResponses[0]).to.have.property('userId', adminUsername);
});
});
describe('[DELETE] canned-responses', () => {
it('should fail if user dont have remove-canned-responses permission', async () => {
await updatePermission('remove-canned-responses', []);
return request.delete(api('canned-responses/sfdads')).set(credentials).expect(403);
});
it('should fail if _id is not on the request', async () => {
await updatePermission('remove-canned-responses', ['livechat-agent', 'livechat-monitor', 'livechat-manager', 'admin']);
return request.delete(api('canned-responses')).set(credentials).expect(400);
});
it('should delete a canned response', async () => {
const response = await createCannedResponse();
const { body: cr } = await request.get(api('canned-responses')).set(credentials).query({ shortcut: response.shortcut }).expect(200);
const { body } = await request
.delete(api(`canned-responses/${cr.cannedResponses[0]._id}`))
.set(credentials)
.expect(200);
expect(body).to.have.property('success', true);
});
});
});

View File

@@ -0,0 +1,821 @@
import type { ILivechatDepartment, ILivechatVisitor } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { before, describe, it, after } from 'mocha';
import { getCredentials, api, request, credentials } from '../../../data/api-data';
import { createDepartment, deleteDepartment } from '../../../data/livechat/department';
import {
createVisitor,
createLivechatRoom,
takeInquiry,
closeOmnichannelRoom,
makeAgentUnavailable,
makeAgentAvailable,
createAgent,
sendAgentMessage,
sendMessage,
fetchInquiry,
deleteVisitor,
} from '../../../data/livechat/rooms';
import { updateEESetting, updatePermission, updateSetting } from '../../../data/permissions.helper';
import { pagination } from '../../../data/utils';
import { IS_EE } from '../../../e2e/config/constants';
(IS_EE ? describe : describe.skip)('[EE] LIVECHAT - dashboards', () => {
before((done) => getCredentials(done));
before(async () => {
await updateSetting('Livechat_enabled', true);
await updateEESetting('Livechat_Require_Contact_Verification', 'never');
await updateSetting('Omnichannel_enable_department_removal', true);
await createAgent();
await makeAgentAvailable();
});
after(async () => {
await updateSetting('Omnichannel_enable_department_removal', false);
});
describe('livechat/analytics/agents/average-service-time', () => {
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await updatePermission('view-livechat-manager', []);
await request
.get(api('livechat/analytics/agents/average-service-time'))
.set(credentials)
.query({ start: '2020-01-01', end: '2020-01-02' })
.expect('Content-Type', 'application/json')
.expect(403);
});
it('should fail if start is not present as query param', async () => {
await updatePermission('view-livechat-manager', ['admin']);
const response = await request
.get(api('livechat/analytics/agents/average-service-time'))
.set(credentials)
.query({ end: '2020-01-02' })
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should fail if end is not present as query param', async () => {
const response = await request
.get(api('livechat/analytics/agents/average-service-time'))
.set(credentials)
.query({ start: '2020-01-01' })
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should fail if start is not a valid date', async () => {
const date = new Date(new Date().setDate(new Date().getDate() - 2)).toISOString();
const response = await request
.get(api('livechat/analytics/agents/average-service-time'))
.set(credentials)
.query({ start: '2020-01-01x', end: date })
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should fail if end is not a valid date', async () => {
const date = new Date(new Date().setDate(new Date().getDate() - 2)).toISOString();
const response = await request
.get(api('livechat/analytics/agents/average-service-time'))
.set(credentials)
.query({ start: date, end: '2020-x01-02' })
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should return an array of agents', async () => {
const date = new Date(new Date().setDate(new Date().getDate() - 2)).toISOString();
const visitor = await createVisitor();
const room = await createLivechatRoom(visitor.token);
const inq = await fetchInquiry(room._id);
await takeInquiry(inq._id);
await closeOmnichannelRoom(room._id);
const { body } = await request
.get(api('livechat/analytics/agents/average-service-time'))
.set(credentials)
.query({ start: date, end: new Date().toISOString() })
.expect('Content-Type', 'application/json')
.expect(200);
expect(body).to.have.property('success', true);
expect(body.agents).to.be.an('array').with.lengthOf.greaterThan(0);
expect(body.agents[0]).to.have.a.property('_id');
expect(body.agents[0]).to.have.a.property('username');
expect(body.agents[0]).to.have.a.property('averageServiceTimeInSeconds').that.is.a('number');
});
it('should accept pagination', async () => {
await pagination('livechat/analytics/agents/average-service-time', credentials, {
start: new Date().toISOString(),
end: new Date().toISOString(),
});
});
});
describe('livechat/analytics/agents/total-service-time', () => {
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await updatePermission('view-livechat-manager', []);
await request
.get(api('livechat/analytics/agents/total-service-time'))
.set(credentials)
.query({ start: '2020-01-01', end: '2020-01-02' })
.expect('Content-Type', 'application/json')
.expect(403);
});
it('should fail if start is not present as query param', async () => {
await updatePermission('view-livechat-manager', ['admin']);
const response = await request
.get(api('livechat/analytics/agents/total-service-time'))
.set(credentials)
.query({ end: '2020-01-02' })
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should fail if end is not present as query param', async () => {
const response = await request
.get(api('livechat/analytics/agents/total-service-time'))
.set(credentials)
.query({ start: '2020-01-01' })
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should fail if start is not a valid date', async () => {
const date = new Date(new Date().setDate(new Date().getDate() - 2)).toISOString();
const response = await request
.get(api('livechat/analytics/agents/total-service-time'))
.set(credentials)
.query({ start: '2020-01-01x', end: date })
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should fail if end is not a valid date', async () => {
const date = new Date(new Date().setDate(new Date().getDate() - 2)).toISOString();
const response = await request
.get(api('livechat/analytics/agents/total-service-time'))
.set(credentials)
.query({ start: date, end: '2020-x01-02' })
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should return an array of agents', async () => {
const date = new Date(new Date().setDate(new Date().getDate() - 2)).toISOString();
const { body } = await request
.get(api('livechat/analytics/agents/total-service-time'))
.set(credentials)
.query({ start: date, end: new Date().toISOString() })
.expect('Content-Type', 'application/json')
.expect(200);
expect(body).to.have.property('success', true);
expect(body.agents).to.be.an('array').with.lengthOf.greaterThan(0);
expect(body.agents[0]).to.have.a.property('_id');
expect(body.agents[0]).to.have.a.property('username');
expect(body.agents[0]).to.have.a.property('serviceTimeDuration').that.is.a('number');
});
it('should accept pagination', async () => {
await pagination('livechat/analytics/agents/total-service-time', credentials, {
start: new Date().toISOString(),
end: new Date().toISOString(),
});
});
});
describe('livechat/analytics/agents/available-for-service-history', () => {
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await updatePermission('view-livechat-manager', []);
await request
.get(api('livechat/analytics/agents/available-for-service-history'))
.set(credentials)
.query({ start: '2020-01-01', end: '2020-01-02' })
.expect('Content-Type', 'application/json')
.expect(403);
});
it('should fail if start is not present as query param', async () => {
await updatePermission('view-livechat-manager', ['admin']);
const response = await request
.get(api('livechat/analytics/agents/available-for-service-history'))
.set(credentials)
.query({ end: '2020-01-02' })
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should fail if end is not present as query param', async () => {
const response = await request
.get(api('livechat/analytics/agents/available-for-service-history'))
.set(credentials)
.query({ start: '2020-01-01' })
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should fail if start is not a valid date', async () => {
const date = new Date(new Date().setDate(new Date().getDate() - 2)).toISOString();
const response = await request
.get(api('livechat/analytics/agents/available-for-service-history'))
.set(credentials)
.query({ start: '2020-01-01x', end: date })
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should fail if end is not a valid date', async () => {
const date = new Date(new Date().setDate(new Date().getDate() - 2)).toISOString();
const response = await request
.get(api('livechat/analytics/agents/available-for-service-history'))
.set(credentials)
.query({ start: date, end: '2020-x01-02' })
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should return an array of agents', async () => {
// Toggling to populate agent activity collection
await createAgent();
await makeAgentUnavailable();
await makeAgentAvailable();
const date = new Date(new Date().setDate(new Date().getDate() - 2)).toISOString();
const { body } = await request
.get(api('livechat/analytics/agents/available-for-service-history'))
.set(credentials)
.query({ start: date, end: new Date().toISOString() })
.expect('Content-Type', 'application/json')
.expect(200);
expect(body).to.have.property('success', true);
expect(body.agents).to.be.an('array').with.lengthOf.greaterThan(0);
expect(body.agents[0]).to.have.a.property('username');
expect(body.agents[0]).to.have.a.property('availableTimeInSeconds').that.is.a('number');
});
it('should accept pagination', async () => {
await pagination('livechat/analytics/agents/available-for-service-history', credentials, {
start: new Date().toISOString(),
end: new Date().toISOString(),
});
});
});
describe('livechat/analytics/departments/amount-of-chats', () => {
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await updatePermission('view-livechat-manager', []);
await request
.get(api('livechat/analytics/departments/amount-of-chats'))
.set(credentials)
.query({ start: '2020-01-01', end: '2020-01-02' })
.expect('Content-Type', 'application/json')
.expect(403);
});
it('should fail if start is not present as query param', async () => {
await updatePermission('view-livechat-manager', ['admin']);
const response = await request
.get(api('livechat/analytics/departments/amount-of-chats'))
.set(credentials)
.query({ end: '2020-01-02' })
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should fail if end is not present as query param', async () => {
const response = await request
.get(api('livechat/analytics/departments/amount-of-chats'))
.set(credentials)
.query({ start: '2020-01-01' })
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should fail if start is not a valid date', async () => {
const date = new Date(new Date().setDate(new Date().getDate() - 2)).toISOString();
const response = await request
.get(api('livechat/analytics/departments/amount-of-chats'))
.set(credentials)
.query({ start: '2020-01-01x', end: date })
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should fail if end is not a valid date', async () => {
const date = new Date(new Date().setDate(new Date().getDate() - 2)).toISOString();
const response = await request
.get(api('livechat/analytics/departments/amount-of-chats'))
.set(credentials)
.query({ start: date, end: '2020-x01-02' })
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should return an array of departments', async () => {
const date = new Date(new Date().setDate(new Date().getDate() - 2)).toISOString();
const { body } = await request
.get(api('livechat/analytics/departments/amount-of-chats'))
.set(credentials)
.query({ start: date, end: new Date().toISOString() })
.expect('Content-Type', 'application/json')
.expect(200);
expect(body).to.have.property('success', true);
expect(body.departments).to.be.an('array').with.lengthOf.greaterThan(0);
expect(body.departments[0]).to.have.a.property('rooms').that.is.a('number');
expect(body.departments[0]).to.have.a.property('_id');
});
it('should accept pagination', async () => {
await pagination('livechat/analytics/departments/amount-of-chats', credentials, {
start: new Date().toISOString(),
end: new Date().toISOString(),
});
});
});
describe('livechat/analytics/departments/average-service-time', () => {
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await updatePermission('view-livechat-manager', []);
await request
.get(api('livechat/analytics/departments/average-service-time'))
.set(credentials)
.query({ start: '2020-01-01', end: '2020-01-02' })
.expect('Content-Type', 'application/json')
.expect(403);
});
it('should fail if start is not present as query param', async () => {
await updatePermission('view-livechat-manager', ['admin']);
const response = await request
.get(api('livechat/analytics/departments/average-service-time'))
.set(credentials)
.query({ end: '2020-01-02' })
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should fail if end is not present as query param', async () => {
const response = await request
.get(api('livechat/analytics/departments/average-service-time'))
.set(credentials)
.query({ start: '2020-01-01' })
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should fail if start is not a valid date', async () => {
const date = new Date(new Date().setDate(new Date().getDate() - 2)).toISOString();
const response = await request
.get(api('livechat/analytics/departments/average-service-time'))
.set(credentials)
.query({ start: '2020-01-01x', end: date })
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should fail if end is not a valid date', async () => {
const date = new Date(new Date().setDate(new Date().getDate() - 2)).toISOString();
const response = await request
.get(api('livechat/analytics/departments/average-service-time'))
.set(credentials)
.query({ start: date, end: '2020-x01-02' })
.expect(400);
expect(response.body).to.have.property('success', false);
});
describe('with data', () => {
let department: ILivechatDepartment;
let visitor: ILivechatVisitor;
before(async () => {
department = await createDepartment({}, [{ agentId: credentials['X-User-Id'] }]);
visitor = await createVisitor(department._id);
const room = await createLivechatRoom(visitor.token);
const inq = await fetchInquiry(room._id);
await takeInquiry(inq._id);
await sendAgentMessage(room._id);
await closeOmnichannelRoom(room._id);
});
after(async () => {
await deleteVisitor(visitor.token);
await deleteDepartment(department._id);
});
it('should return an array of departments', async () => {
const date = new Date(new Date().setDate(new Date().getDate() - 2)).toISOString();
const { body } = await request
.get(api('livechat/analytics/departments/average-service-time'))
.set(credentials)
.query({ start: date, end: new Date().toISOString() })
.expect('Content-Type', 'application/json')
.expect(200);
expect(body).to.have.property('success', true);
expect(body.departments).to.be.an('array').with.lengthOf.greaterThan(0);
expect(body.departments[0]).to.have.a.property('averageServiceTimeInSeconds').that.is.a('number');
expect(body.departments[0]).to.have.a.property('_id');
});
});
it('should accept pagination', async () => {
await pagination('livechat/analytics/departments/average-service-time', credentials, {
start: new Date().toISOString(),
end: new Date().toISOString(),
});
});
});
describe('livechat/analytics/departments/average-chat-duration-time', () => {
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await updatePermission('view-livechat-manager', []);
await request
.get(api('livechat/analytics/departments/average-chat-duration-time'))
.set(credentials)
.query({ start: '2020-01-01', end: '2020-01-02' })
.expect('Content-Type', 'application/json')
.expect(403);
});
it('should fail if start is not present as query param', async () => {
await updatePermission('view-livechat-manager', ['admin']);
const response = await request
.get(api('livechat/analytics/departments/average-chat-duration-time'))
.set(credentials)
.query({ end: '2020-01-02' })
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should fail if end is not present as query param', async () => {
const response = await request
.get(api('livechat/analytics/departments/average-chat-duration-time'))
.set(credentials)
.query({ start: '2020-01-01' })
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should fail if start is not a valid date', async () => {
const date = new Date(new Date().setDate(new Date().getDate() - 2)).toISOString();
const response = await request
.get(api('livechat/analytics/departments/average-chat-duration-time'))
.set(credentials)
.query({ start: '2020-01-01x', end: date })
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should fail if end is not a valid date', async () => {
const date = new Date(new Date().setDate(new Date().getDate() - 2)).toISOString();
const response = await request
.get(api('livechat/analytics/departments/average-chat-duration-time'))
.set(credentials)
.query({ start: date, end: '2020-x01-02' })
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should return an array of departments', async () => {
const date = new Date(new Date().setDate(new Date().getDate() - 2)).toISOString();
const { body } = await request
.get(api('livechat/analytics/departments/average-chat-duration-time'))
.set(credentials)
.query({ start: date, end: new Date().toISOString() })
.expect('Content-Type', 'application/json')
.expect(200);
expect(body).to.have.property('success', true);
expect(body.departments).to.be.an('array').with.lengthOf.greaterThan(0);
expect(body.departments[0]).to.have.a.property('averageChatDurationTimeInSeconds').that.is.a('number');
expect(body.departments[0]).to.have.a.property('_id');
});
it('should accept pagination', async () => {
await pagination('livechat/analytics/departments/average-chat-duration-time', credentials, {
start: new Date().toISOString(),
end: new Date().toISOString(),
});
});
});
describe('livechat/analytics/departments/total-service-time', () => {
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await updatePermission('view-livechat-manager', []);
await request
.get(api('livechat/analytics/departments/total-service-time'))
.set(credentials)
.query({ start: '2020-01-01', end: '2020-01-02' })
.expect('Content-Type', 'application/json')
.expect(403);
});
it('should fail if start is not present as query param', async () => {
await updatePermission('view-livechat-manager', ['admin']);
const response = await request
.get(api('livechat/analytics/departments/total-service-time'))
.set(credentials)
.query({ end: '2020-01-02' })
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should fail if end is not present as query param', async () => {
const response = await request
.get(api('livechat/analytics/departments/total-service-time'))
.set(credentials)
.query({ start: '2020-01-01' })
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should fail if start is not a valid date', async () => {
const date = new Date(new Date().setDate(new Date().getDate() - 2)).toISOString();
const response = await request
.get(api('livechat/analytics/departments/total-service-time'))
.set(credentials)
.query({ start: '2020-01-01x', end: date })
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should fail if end is not a valid date', async () => {
const date = new Date(new Date().setDate(new Date().getDate() - 2)).toISOString();
const response = await request
.get(api('livechat/analytics/departments/total-service-time'))
.set(credentials)
.query({ start: date, end: '2020-x01-02' })
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should return an array of departments', async () => {
const date = new Date(new Date().setDate(new Date().getDate() - 2)).toISOString();
const { body } = await request
.get(api('livechat/analytics/departments/total-service-time'))
.set(credentials)
.query({ start: date, end: new Date().toISOString() })
.expect('Content-Type', 'application/json')
.expect(200);
expect(body).to.have.property('success', true);
expect(body.departments).to.be.an('array').with.lengthOf.greaterThan(0);
expect(body.departments[0]).to.have.a.property('chats').that.is.a('number');
expect(body.departments[0]).to.have.a.property('serviceTimeDuration').that.is.a('number');
expect(body.departments[0]).to.have.a.property('_id');
});
it('should accept pagination', async () => {
await pagination('livechat/analytics/departments/total-service-time', credentials, {
start: new Date().toISOString(),
end: new Date().toISOString(),
});
});
});
describe('livechat/analytics/departments/average-waiting-time', () => {
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await updatePermission('view-livechat-manager', []);
await request
.get(api('livechat/analytics/departments/average-waiting-time'))
.set(credentials)
.query({ start: '2020-01-01', end: '2020-01-02' })
.expect('Content-Type', 'application/json')
.expect(403);
});
it('should fail if start is not present as query param', async () => {
await updatePermission('view-livechat-manager', ['admin']);
const response = await request
.get(api('livechat/analytics/departments/average-waiting-time'))
.set(credentials)
.query({ end: '2020-01-02' })
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should fail if end is not present as query param', async () => {
const response = await request
.get(api('livechat/analytics/departments/average-waiting-time'))
.set(credentials)
.query({ start: '2020-01-01' })
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should fail if start is not a valid date', async () => {
const date = new Date(new Date().setDate(new Date().getDate() - 2)).toISOString();
const response = await request
.get(api('livechat/analytics/departments/average-waiting-time'))
.set(credentials)
.query({ start: '2020-01-01x', end: date })
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should fail if end is not a valid date', async () => {
const date = new Date(new Date().setDate(new Date().getDate() - 2)).toISOString();
const response = await request
.get(api('livechat/analytics/departments/average-waiting-time'))
.set(credentials)
.query({ start: date, end: '2020-x01-02' })
.expect(400);
expect(response.body).to.have.property('success', false);
});
describe('with data', () => {
let department: ILivechatDepartment;
let visitor: ILivechatVisitor;
before(async () => {
department = await createDepartment({}, [{ agentId: credentials['X-User-Id'] }]);
visitor = await createVisitor(department._id);
const room = await createLivechatRoom(visitor.token);
const inq = await fetchInquiry(room._id);
await takeInquiry(inq._id);
await sendAgentMessage(room._id);
await closeOmnichannelRoom(room._id);
});
after(async () => {
await deleteVisitor(visitor.token);
await deleteDepartment(department._id);
});
it('should return an array of departments', async () => {
const date = new Date(new Date().setDate(new Date().getDate() - 2)).toISOString();
const { body } = await request
.get(api('livechat/analytics/departments/average-waiting-time'))
.set(credentials)
.query({ start: date, end: new Date().toISOString() })
.expect('Content-Type', 'application/json')
.expect(200);
expect(body).to.have.property('success', true);
expect(body.departments).to.be.an('array').with.lengthOf.greaterThan(0);
expect(body.departments[0]).to.have.a.property('averageWaitingTimeInSeconds').that.is.a('number');
expect(body.departments[0]).to.have.a.property('_id');
});
});
it('should accept pagination', async () => {
await pagination('livechat/analytics/departments/average-waiting-time', credentials, {
start: new Date().toISOString(),
end: new Date().toISOString(),
});
});
});
describe('livechat/analytics/departments/total-transferred-chats', () => {
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await updatePermission('view-livechat-manager', []);
await request
.get(api('livechat/analytics/departments/total-transferred-chats'))
.set(credentials)
.query({ start: '2020-01-01', end: '2020-01-02' })
.expect('Content-Type', 'application/json')
.expect(403);
});
it('should fail if start is not present as query param', async () => {
await updatePermission('view-livechat-manager', ['admin']);
const response = await request
.get(api('livechat/analytics/departments/total-transferred-chats'))
.set(credentials)
.query({ end: '2020-01-02' })
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should fail if end is not present as query param', async () => {
const response = await request
.get(api('livechat/analytics/departments/total-transferred-chats'))
.set(credentials)
.query({ start: '2020-01-01' })
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should fail if start is not a valid date', async () => {
const date = new Date(new Date().setDate(new Date().getDate() - 2)).toISOString();
const response = await request
.get(api('livechat/analytics/departments/total-transferred-chats'))
.set(credentials)
.query({ start: '2020-01-01x', end: date })
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should fail if end is not a valid date', async () => {
const date = new Date(new Date().setDate(new Date().getDate() - 2)).toISOString();
const response = await request
.get(api('livechat/analytics/departments/total-transferred-chats'))
.set(credentials)
.query({ start: date, end: '2020-x01-02' })
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should return an array of departments', async () => {
const date = new Date(new Date().setDate(new Date().getDate() - 2)).toISOString();
const { body } = await request
.get(api('livechat/analytics/departments/total-transferred-chats'))
.set(credentials)
.query({ start: date, end: new Date().toISOString() })
.expect('Content-Type', 'application/json')
.expect(200);
expect(body).to.have.property('success', true);
expect(body.departments).to.be.an('array').with.lengthOf.greaterThan(0);
expect(body.departments[0]).to.have.a.property('numberOfTransferredRooms').that.is.a('number');
expect(body.departments[0]).to.have.a.property('_id');
});
it('should accept pagination', async () => {
await pagination('livechat/analytics/departments/total-transferred-chats', credentials, {
start: new Date().toISOString(),
end: new Date().toISOString(),
});
});
});
describe('livechat/analytics/departments/total-abandoned-chats', () => {
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await updatePermission('view-livechat-manager', []);
await request
.get(api('livechat/analytics/departments/total-abandoned-chats'))
.set(credentials)
.query({ start: '2020-01-01', end: '2020-01-02' })
.expect('Content-Type', 'application/json')
.expect(403);
});
it('should fail if start is not present as query param', async () => {
await updatePermission('view-livechat-manager', ['admin']);
const response = await request
.get(api('livechat/analytics/departments/total-abandoned-chats'))
.set(credentials)
.query({ end: '2020-01-02' })
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should fail if end is not present as query param', async () => {
const response = await request
.get(api('livechat/analytics/departments/total-abandoned-chats'))
.set(credentials)
.query({ start: '2020-01-01' })
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should fail if start is not a valid date', async () => {
const date = new Date(new Date().setDate(new Date().getDate() - 2)).toISOString();
const response = await request
.get(api('livechat/analytics/departments/total-abandoned-chats'))
.set(credentials)
.query({ start: '2020-01-01x', end: date })
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should fail if end is not a valid date', async () => {
const date = new Date(new Date().setDate(new Date().getDate() - 2)).toISOString();
const response = await request
.get(api('livechat/analytics/departments/total-abandoned-chats'))
.set(credentials)
.query({ start: date, end: '2020-x01-02' })
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should return an array of departments', async () => {
await updateSetting('Livechat_visitor_inactivity_timeout', 0);
const date = new Date(new Date().setDate(new Date().getDate() - 2)).toISOString();
const visitor = await createVisitor();
const room = await createLivechatRoom(visitor.token);
const inq = await fetchInquiry(room._id);
await takeInquiry(inq._id);
await sendMessage(room._id, 'first message', visitor.token);
await sendAgentMessage(room._id);
await closeOmnichannelRoom(room._id);
const { body } = await request
.get(api('livechat/analytics/departments/total-abandoned-chats'))
.set(credentials)
.query({ start: date, end: new Date().toISOString() })
.expect('Content-Type', 'application/json')
.expect(200);
expect(body).to.have.property('success', true);
expect(body.departments).to.be.an('array').with.lengthOf.greaterThan(0);
expect(body.departments[0]).to.have.a.property('abandonedRooms').that.is.a('number');
expect(body.departments[0]).to.have.a.property('_id');
});
it('should accept pagination', async () => {
await pagination('livechat/analytics/departments/total-abandoned-chats', credentials, {
start: new Date().toISOString(),
end: new Date().toISOString(),
});
});
});
describe('livechat/analytics/departments/percentage-abandoned-chats', () => {
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await updatePermission('view-livechat-manager', []);
await request
.get(api('livechat/analytics/departments/percentage-abandoned-chats'))
.set(credentials)
.query({ start: '2020-01-01', end: '2020-01-02' })
.expect('Content-Type', 'application/json')
.expect(403);
});
it('should fail if start is not present as query param', async () => {
await updatePermission('view-livechat-manager', ['admin']);
const response = await request
.get(api('livechat/analytics/departments/percentage-abandoned-chats'))
.set(credentials)
.query({ end: '2020-01-02' })
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should fail if end is not present as query param', async () => {
const response = await request
.get(api('livechat/analytics/departments/percentage-abandoned-chats'))
.set(credentials)
.query({ start: '2020-01-01' })
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should fail if start is not a valid date', async () => {
const date = new Date(new Date().setDate(new Date().getDate() - 2)).toISOString();
const response = await request
.get(api('livechat/analytics/departments/percentage-abandoned-chats'))
.set(credentials)
.query({ start: '2020-01-01x', end: date })
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should fail if end is not a valid date', async () => {
const date = new Date(new Date().setDate(new Date().getDate() - 2)).toISOString();
const response = await request
.get(api('livechat/analytics/departments/percentage-abandoned-chats'))
.set(credentials)
.query({ start: date, end: '2020-x01-02' })
.expect(400);
expect(response.body).to.have.property('success', false);
});
it('should return an array of departments', async () => {
await updateSetting('Livechat_visitor_inactivity_timeout', 0);
const date = new Date(new Date().setDate(new Date().getDate() - 2)).toISOString();
const { body } = await request
.get(api('livechat/analytics/departments/percentage-abandoned-chats'))
.set(credentials)
.query({ start: date, end: new Date().toISOString() })
.expect('Content-Type', 'application/json')
.expect(200);
expect(body).to.have.property('success', true);
expect(body.departments).to.be.an('array').with.lengthOf.greaterThan(0);
expect(body.departments[0]).to.have.a.property('percentageOfAbandonedChats').that.is.a('number');
expect(body.departments[0]).to.have.a.property('_id');
});
it('should accept pagination', async () => {
await pagination('livechat/analytics/departments/percentage-abandoned-chats', credentials, {
start: new Date().toISOString(),
end: new Date().toISOString(),
});
});
});
});

View File

@@ -0,0 +1,374 @@
import type { Credentials } from '@rocket.chat/api-client';
import type { IOmnichannelRoom, IUser } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import { getCredentials, api, request, credentials } from '../../../data/api-data';
import {
createVisitor,
createLivechatRoom,
sendMessage,
sendAgentMessage,
placeRoomOnHold,
getLivechatRoomInfo,
startANewLivechatRoomAndTakeIt,
makeAgentAvailable,
createAgent,
closeOmnichannelRoom,
fetchMessages,
} from '../../../data/livechat/rooms';
import { sleep } from '../../../data/livechat/utils';
import { updateEESetting, updatePermission, updateSetting } from '../../../data/permissions.helper';
import { password } from '../../../data/user';
import { createUser, deleteUser, login } from '../../../data/users.helper';
import { IS_EE } from '../../../e2e/config/constants';
(IS_EE ? describe : describe.skip)('[EE] LIVECHAT - rooms', () => {
before((done) => getCredentials(done));
let agent2: { user: IUser; credentials: Credentials };
before(async () => {
await updateSetting('Livechat_enabled', true);
await updateSetting('Livechat_Routing_Method', 'Manual_Selection');
await updateEESetting('Livechat_Require_Contact_Verification', 'never');
await createAgent();
await makeAgentAvailable();
});
before(async () => {
const user = await createUser();
const userCredentials = await login(user.username, password);
await createAgent(user.username);
await updateSetting('Livechat_allow_manual_on_hold', true);
agent2 = {
user,
credentials: userCredentials,
};
});
after(async () => {
await deleteUser(agent2.user);
await updateSetting('Livechat_allow_manual_on_hold', false);
await updateSetting('Livechat_allow_manual_on_hold_upon_agent_engagement_only', true);
});
describe('livechat/room.onHold', () => {
it('should fail if user doesnt have on-hold-livechat-room permission', async () => {
await updatePermission('on-hold-livechat-room', []);
const response = await request
.post(api('livechat/room.onHold'))
.set(credentials)
.send({
roomId: 'invalid-room-id',
})
.expect(403);
expect(response.body.success).to.be.false;
await updatePermission('on-hold-livechat-room', ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin']);
});
it('should fail if roomId is invalid', async () => {
const response = await request
.post(api('livechat/room.onHold'))
.set(credentials)
.send({
roomId: 'invalid-room-id',
})
.expect(400);
expect(response.body.success).to.be.false;
expect(response.body.error).to.be.equal('error-invalid-room');
});
it('should fail if room is an empty string', async () => {
const response = await request
.post(api('livechat/room.onHold'))
.set(credentials)
.send({
roomId: ' ',
})
.expect(400);
expect(response.body.success).to.be.false;
expect(response.body.error).to.be.equal('error-invalid-room');
});
it('should fail if room is not a livechat room', async () => {
const response = await request
.post(api('livechat/room.onHold'))
.set(credentials)
.send({
roomId: 'GENERAL',
})
.expect(400);
expect(response.body.success).to.be.false;
expect(response.body.error).to.be.equal('error-invalid-room');
});
it('should fail if visitor is awaiting response (visitor sent last message)', async () => {
const visitor = await createVisitor();
const room = await createLivechatRoom(visitor.token);
await sendMessage(room._id, 'test', visitor.token);
const response = await request
.post(api('livechat/room.onHold'))
.set(credentials)
.send({
roomId: room._id,
})
.expect(400);
expect(response.body.success).to.be.false;
expect(response.body.error).to.be.equal('error-cannot-place-chat-on-hold');
});
it('should fail if room is closed', async () => {
const visitor = await createVisitor();
const room = await createLivechatRoom(visitor.token);
await closeOmnichannelRoom(room._id);
const response = await request
.post(api('livechat/room.onHold'))
.set(credentials)
.send({
roomId: room._id,
})
.expect(400);
expect(response.body.success).to.be.false;
expect(response.body.error).to.be.equal('error-room-already-closed');
});
it('should fail if user is not serving the chat and doesnt have on-hold-others-livechat-room permission', async () => {
const { room } = await startANewLivechatRoomAndTakeIt();
await sendAgentMessage(room._id);
const response = await request
.post(api('livechat/room.onHold'))
.set(agent2.credentials)
.send({
roomId: room._id,
})
.expect(400);
expect(response.body.success).to.be.false;
expect(response.body.error).to.be.equal('Not_authorized');
});
it('should put room on hold', async () => {
const { room } = await startANewLivechatRoomAndTakeIt();
await sendAgentMessage(room._id);
const response = await request
.post(api('livechat/room.onHold'))
.set(credentials)
.send({
roomId: room._id,
})
.expect(200);
expect(response.body.success).to.be.true;
const updatedRoom = await getLivechatRoomInfo(room._id);
expect(updatedRoom.onHold).to.be.true;
});
it('Should put room on hold, even in the visitor sent the last message', async () => {
const { room, visitor } = await startANewLivechatRoomAndTakeIt();
await updateSetting('Livechat_allow_manual_on_hold_upon_agent_engagement_only', false);
await sendMessage(room._id, '-', visitor.token);
const response = await request
.post(api('livechat/room.onHold'))
.set(credentials)
.send({
roomId: room._id,
})
.expect(200);
expect(response.body.success).to.be.true;
const updatedRoom = await getLivechatRoomInfo(room._id);
expect(updatedRoom.onHold).to.be.true;
});
it('should not put room on hold when visitor sent the last message', async () => {
const { room, visitor } = await startANewLivechatRoomAndTakeIt();
await updateSetting('Livechat_allow_manual_on_hold_upon_agent_engagement_only', true);
await sendMessage(room._id, '-', visitor.token);
const response = await request
.post(api('livechat/room.onHold'))
.set(credentials)
.send({
roomId: room._id,
})
.expect(400);
expect(response.body.success).to.be.false;
expect(response.body.error).to.be.equal('error-cannot-place-chat-on-hold');
});
});
describe('livechat/room.resumeOnHold', () => {
it('should fail if user doesnt have view-l-room permission', async () => {
await updatePermission('view-l-room', []);
const response = await request
.post(api('livechat/room.resumeOnHold'))
.set(credentials)
.send({
roomId: 'invalid-room-id',
})
.expect(403);
expect(response.body.success).to.be.false;
});
it('should fail if roomId is invalid', async () => {
await updatePermission('view-l-room', ['admin', 'livechat-manager', 'livechat-agent']);
const response = await request
.post(api('livechat/room.resumeOnHold'))
.set(credentials)
.send({
roomId: 'invalid-room-id',
})
.expect(400);
expect(response.body.success).to.be.false;
expect(response.body.error).to.be.equal('error-invalid-room');
});
it('should fail if room is not a livechat room', async () => {
const response = await request
.post(api('livechat/room.resumeOnHold'))
.set(credentials)
.send({
roomId: 'GENERAL',
})
.expect(400);
expect(response.body.success).to.be.false;
expect(response.body.error).to.be.equal('error-invalid-room');
});
it('should fail if room is not on hold', async () => {
const { room } = await startANewLivechatRoomAndTakeIt();
const response = await request
.post(api('livechat/room.resumeOnHold'))
.set(credentials)
.send({
roomId: room._id,
})
.expect(400);
expect(response.body.success).to.be.false;
expect(response.body.error).to.be.equal('error-room-not-on-hold');
});
it('should resume room on hold', async () => {
const { room } = await startANewLivechatRoomAndTakeIt();
await sendAgentMessage(room._id);
await placeRoomOnHold(room._id);
const response = await request
.post(api('livechat/room.resumeOnHold'))
.set(credentials)
.send({
roomId: room._id,
})
.expect(200);
expect(response.body.success).to.be.true;
const updatedRoom = await getLivechatRoomInfo(room._id);
expect(updatedRoom).to.not.have.property('onHold');
});
it('should resume room on hold and send proper system message', async () => {
const { room, visitor } = await startANewLivechatRoomAndTakeIt();
await sendAgentMessage(room._id);
await placeRoomOnHold(room._id);
const response = await request
.post(api('livechat/room.resumeOnHold'))
.set(credentials)
.send({
roomId: room._id,
})
.expect(200);
expect(response.body.success).to.be.true;
const messages = await fetchMessages(room._id, visitor.token);
expect(messages).to.be.an('array');
expect(messages[0]).to.not.be.undefined;
expect(messages[0]).to.have.property('t', 'omnichannel_on_hold_chat_resumed');
expect(messages[0]).to.have.property('comment', 'The chat was manually resumed from On Hold by RocketChat Internal Admin Test');
});
it('should resume chat automatically if visitor sent a message', async () => {
const { room, visitor } = await startANewLivechatRoomAndTakeIt();
await sendAgentMessage(room._id);
await placeRoomOnHold(room._id);
await sendMessage(room._id, 'test', visitor.token);
// wait for the room to be resumed since that logic is within callbacks
await sleep(500);
const updatedRoom = await getLivechatRoomInfo(room._id);
expect(updatedRoom).to.not.have.property('onHold');
});
});
describe('visitor abandonment feature', () => {
let room: IOmnichannelRoom;
before(async () => {
await updateSetting('Livechat_abandoned_rooms_action', 'Livechat_close_chat');
await updateSetting('Livechat_visitor_inactivity_timeout', 60);
});
it('should set predictedVisitorAbandonmentAt when agent sends a message', async () => {
const { room: newRoom } = await startANewLivechatRoomAndTakeIt();
room = newRoom;
await sendAgentMessage(room._id);
const updatedRoom = await getLivechatRoomInfo(room._id);
const lastMessageTs = updatedRoom.responseBy?.lastMessageTs;
const firstResponseTs = updatedRoom.responseBy?.firstResponseTs;
const predictedVisitorAbandonmentAt = updatedRoom.omnichannel?.predictedVisitorAbandonmentAt;
expect(predictedVisitorAbandonmentAt).to.not.be.undefined;
expect(lastMessageTs).to.not.be.undefined;
expect(firstResponseTs).to.not.be.undefined;
// expect predictedVisitorAbandonmentAt to be 60 seconds after lastMessageTs
const lastMessageTsDate = new Date(lastMessageTs as Date);
const predictedVisitorAbandonmentAtDate = new Date(predictedVisitorAbandonmentAt as Date);
const firstResponseTsDate = new Date(firstResponseTs as Date);
expect(predictedVisitorAbandonmentAtDate.getTime()).to.be.equal(lastMessageTsDate.getTime() + 60000);
expect(firstResponseTsDate.getTime()).to.be.equal(lastMessageTsDate.getTime());
});
it('should not update predictedVisitorAbandonmentAt when agent sends yet another message', async () => {
await sendAgentMessage(room._id);
const updatedRoom = await getLivechatRoomInfo(room._id);
const lastMessageTs = updatedRoom.responseBy?.lastMessageTs;
const firstResponseTs = updatedRoom.responseBy?.firstResponseTs;
const predictedVisitorAbandonmentAt = updatedRoom.omnichannel?.predictedVisitorAbandonmentAt;
expect(predictedVisitorAbandonmentAt).to.not.be.undefined;
expect(lastMessageTs).to.not.be.undefined;
// expect predictedVisitorAbandonmentAt to be 60 seconds after first message
const lastMessageTsDate = new Date(lastMessageTs as Date);
const predictedVisitorAbandonmentAtDate = new Date(predictedVisitorAbandonmentAt as Date);
const firstResponseTsDate = new Date(firstResponseTs as Date);
expect(predictedVisitorAbandonmentAtDate.getTime()).to.be.equal(firstResponseTsDate.getTime() + 60000);
// lastMessageTs should be updated
expect(lastMessageTsDate.getTime()).to.not.be.equal(firstResponseTsDate.getTime());
expect(lastMessageTsDate.getTime()).to.be.greaterThan(firstResponseTsDate.getTime());
});
after(async () => {
await updateSetting('Livechat_abandoned_rooms_action', 'none');
await updateSetting('Livechat_visitor_inactivity_timeout', 3600);
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,124 @@
import { faker } from '@faker-js/faker';
import type { ILivechatAgent, ILivechatVisitor, IOmnichannelRoom, IRoom } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { before, describe, it, after } from 'mocha';
import { api, getCredentials, request } from '../../../data/api-data';
import { sendSimpleMessage } from '../../../data/chat.helper';
import {
sendMessage,
startANewLivechatRoomAndTakeIt,
sendAgentMessage,
createAgent,
makeAgentAvailable,
uploadFile,
closeOmnichannelRoom,
} from '../../../data/livechat/rooms';
import { removeAgent } from '../../../data/livechat/users';
import { updateEESetting, updateSetting } from '../../../data/permissions.helper';
import { createRoom, deleteRoom } from '../../../data/rooms.helper';
describe('LIVECHAT - messages', () => {
let agent: ILivechatAgent;
before((done) => getCredentials(done));
before(async () => {
agent = await createAgent();
await makeAgentAvailable();
await updateSetting('Livechat_Routing_Method', 'Manual_Selection');
await updateEESetting('Livechat_Require_Contact_Verification', 'never');
});
after(() => Promise.all([updateSetting('Livechat_Routing_Method', 'Auto_Selection'), removeAgent(agent._id)]));
describe('Quote message feature for visitors', () => {
it('it should verify if visitor can quote message', async () => {
const {
room: { _id: roomId },
visitor: { token },
} = await startANewLivechatRoomAndTakeIt();
await sendMessage(roomId, 'Hello from visitor', token);
const agentMsgSentence = faker.lorem.sentence();
const agentMsg = await sendAgentMessage(roomId, agentMsgSentence);
const siteUrl = process.env.SITE_URL || process.env.TEST_API_URL || 'http://localhost:3000';
const msgLink = `${siteUrl}/live/${roomId}?msg=${agentMsg._id}`;
const quotedMsgSentence = faker.lorem.sentence();
const wholeQuotedMsg = `[${quotedMsgSentence}](${msgLink})`;
const quotedMessage = await sendMessage(roomId, wholeQuotedMsg, token);
expect(quotedMessage).to.have.property('_id');
expect(quotedMessage).to.have.property('msg').that.is.equal(wholeQuotedMsg);
expect(quotedMessage).to.have.property('attachments');
expect(quotedMessage.attachments).to.be.an('array').that.has.lengthOf(1);
const quotedMessageAttachments = quotedMessage.attachments?.[0];
if (quotedMessageAttachments) {
expect(quotedMessageAttachments).to.have.property('message_link').that.is.equal(msgLink);
expect(quotedMessageAttachments).to.have.property('text').that.is.equal(agentMsgSentence);
}
});
it('should verify if visitor is receiving a message with a image attachment', async () => {
const {
room: { _id: roomId },
visitor: { token },
} = await startANewLivechatRoomAndTakeIt();
const imgMessage = await uploadFile(roomId, token);
expect(imgMessage).to.have.property('files').that.is.an('array');
expect(imgMessage.files?.[0]).to.have.keys('_id', 'name', 'type');
expect(imgMessage).to.have.property('file').that.deep.equal(imgMessage?.files?.[0]);
});
});
describe('Livechat Messages', async () => {
let room: IOmnichannelRoom;
let privateRoom: IRoom;
let visitor: ILivechatVisitor;
before(async () => {
await updateSetting('Livechat_Routing_Method', 'Auto_Selection');
const data = await startANewLivechatRoomAndTakeIt();
visitor = data.visitor;
room = data.room;
const response = await createRoom({ type: 'p', name: `private-room-${Math.random()}` } as any);
privateRoom = response.body.group;
});
after(() => Promise.all([closeOmnichannelRoom(room._id), deleteRoom({ roomId: privateRoom._id, type: 'p' })]));
it('should not allow fetching arbitrary messages from another channel', async () => {
const response = await sendSimpleMessage({ roomId: privateRoom._id } as any);
const { message } = response.body;
await request
.get(api(`livechat/message/${message._id}`))
.query({ token: visitor.token, rid: room._id })
.send()
.expect(400)
.expect((res) => {
expect(res.body.error).to.be.equal('invalid-message');
});
});
it('should allow fetching messages using their _id and roomId', async () => {
const message = await sendMessage(room._id, 'Hello from visitor', visitor.token);
await request
.get(api(`livechat/message/${message._id}`))
.query({ token: visitor.token, rid: room._id })
.send()
.expect(200)
.expect((res) => {
expect(res.body.message._id).to.be.equal(message._id);
});
});
});
});

View File

@@ -0,0 +1,573 @@
import type { Credentials } from '@rocket.chat/api-client';
import type { IUser, IOmnichannelRoom } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import { api, request, credentials, getCredentials } from '../../../data/api-data';
import { createDepartment, addOrRemoveAgentFromDepartment } from '../../../data/livechat/department';
import { startANewLivechatRoomAndTakeIt, createAgent } from '../../../data/livechat/rooms';
import { createMonitor, createUnit } from '../../../data/livechat/units';
import { restorePermissionToRoles, updateEESetting, updatePermission } from '../../../data/permissions.helper';
import { password } from '../../../data/user';
import { createUser, deleteUser, login } from '../../../data/users.helper';
import { IS_EE } from '../../../e2e/config/constants';
(IS_EE ? describe : describe.skip)('LIVECHAT - reports', () => {
before((done) => getCredentials(done));
let agent2: { user: IUser; credentials: Credentials };
let agent3: { user: IUser; credentials: Credentials };
before(async () => {
await updateEESetting('Livechat_Require_Contact_Verification', 'never');
const user = await createUser();
const userCredentials = await login(user.username, password);
if (!user.username) {
throw new Error('user not created');
}
await createMonitor(user.username);
agent2 = {
user,
credentials: userCredentials,
};
});
before(async () => {
const user = await createUser();
const userCredentials = await login(user.username, password);
await createAgent();
if (!user.username) {
throw new Error('user not created');
}
await createMonitor(user.username);
const dep1 = await createDepartment();
await addOrRemoveAgentFromDepartment(
dep1._id,
{ agentId: 'rocketchat.internal.admin.test', username: 'rocketchat.internal.admin.test', count: 0, order: 0 },
true,
);
const { room, visitor } = await startANewLivechatRoomAndTakeIt({ departmentId: dep1._id });
await request
.post(api('livechat/room.saveInfo'))
.set(credentials)
.send({
roomData: {
_id: room._id,
topic: 'new topic',
tags: ['tag1', 'tag2'],
},
guestData: {
_id: visitor._id,
},
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
});
await createUnit(user._id, user.username, [dep1._id]);
agent3 = {
user,
credentials: userCredentials,
};
});
after(async () => {
await deleteUser(agent2.user);
await deleteUser(agent3.user);
});
describe('livechat/analytics/dashboards/conversations-by-source', () => {
it('should return an error when the user does not have the necessary permission', async () => {
await updatePermission('view-livechat-reports', []);
await request
.get(api('livechat/analytics/dashboards/conversations-by-source'))
.set(credentials)
.query({ start: 'test', end: 'test' })
.expect(403);
});
it('should return an error when the start and end parameters are not provided', async () => {
await restorePermissionToRoles('view-livechat-reports');
await request.get(api('livechat/analytics/dashboards/conversations-by-source')).set(credentials).expect(400);
});
it('should return an error when the start parameter is not provided', async () => {
await request.get(api('livechat/analytics/dashboards/conversations-by-source')).set(credentials).query({ end: 'test' }).expect(400);
});
it('should return an error when the end parameter is not provided', async () => {
await request.get(api('livechat/analytics/dashboards/conversations-by-source')).set(credentials).query({ start: 'test' }).expect(400);
});
it('should return an error when the start parameter is not a valid date', async () => {
await request
.get(api('livechat/analytics/dashboards/conversations-by-source'))
.set(credentials)
.query({ start: 'test', end: 'test' })
.expect(400);
});
it('should fail if dates are more than 1 year apart', async () => {
const oneYearAgo = new Date(Date.now() - 380 * 24 * 60 * 60 * 1000).toISOString();
const now = new Date().toISOString();
await request
.get(api('livechat/analytics/dashboards/conversations-by-source'))
.set(credentials)
.query({ start: oneYearAgo, end: now })
.expect(400);
});
it('should return an error when start is after end', async () => {
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString();
const now = new Date().toISOString();
await request
.get(api('livechat/analytics/dashboards/conversations-by-source'))
.set(credentials)
.query({ start: now, end: oneHourAgo })
.expect(400);
});
it('should return an error when the end parameter is not a valid date', async () => {
await request
.get(api('livechat/analytics/dashboards/conversations-by-source'))
.set(credentials)
.query({ start: '2020-01-01', end: 'test' })
.expect(400);
});
it('should return the proper data when the parameters are valid', async () => {
// Note: this way all data will come as 0
const now = new Date().toISOString();
const { body } = await request
.get(api('livechat/analytics/dashboards/conversations-by-source'))
.set(credentials)
.query({ start: now, end: now })
.expect(200);
expect(body).to.have.property('data').and.to.be.an('array');
expect(body.data).to.have.lengthOf(0);
expect(body.total).to.be.equal(0);
expect(body.success).to.be.true;
});
it('should return only public rooms for a monitor with no units', async () => {
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString();
const now = new Date().toISOString();
const { body } = await request
.get(api('livechat/analytics/dashboards/conversations-by-source'))
.set(agent2.credentials)
.query({ start: oneHourAgo, end: now })
.expect(200);
expect(body).to.have.property('data').and.to.be.an('array');
expect(body.data.every((room: IOmnichannelRoom) => !room.departmentAncestors && !room.departmentId)).to.be.true;
});
it('should return only the data from the unit the monitor belongs to', async () => {
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString();
const now = new Date().toISOString();
const { body } = await request
.get(api('livechat/analytics/dashboards/conversations-by-source'))
.set(agent3.credentials)
.query({ start: oneHourAgo, end: now })
.expect(200);
expect(body).to.have.property('data').and.to.be.an('array');
expect(body.data).to.have.lengthOf.greaterThan(0);
expect(body.data.every((item: { value: number }) => item.value >= 0)).to.be.true;
expect(body.total).to.be.greaterThan(0);
expect(body.success).to.be.true;
});
it('should return valid data when login as a manager', async () => {
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString();
const now = new Date().toISOString();
const { body } = await request
.get(api('livechat/analytics/dashboards/conversations-by-source'))
.set(credentials)
.query({ start: oneHourAgo, end: now })
.expect(200);
expect(body).to.have.property('data').and.to.be.an('array');
expect(body.data).to.have.lengthOf.greaterThan(0);
expect(body.data.every((item: { value: number }) => item.value >= 0)).to.be.true;
expect(body.total).to.be.greaterThan(0);
});
});
describe('livechat/analytics/dashboards/conversations-by-status', () => {
it('should return an error when the user does not have the necessary permission', async () => {
await updatePermission('view-livechat-reports', []);
await request
.get(api('livechat/analytics/dashboards/conversations-by-status'))
.set(credentials)
.query({ start: 'test', end: 'test' })
.expect(403);
});
it('should return an error when the start and end parameters are not provided', async () => {
await restorePermissionToRoles('view-livechat-reports');
await request.get(api('livechat/analytics/dashboards/conversations-by-status')).set(credentials).expect(400);
});
it('should return an error when the start parameter is not provided', async () => {
await request.get(api('livechat/analytics/dashboards/conversations-by-status')).set(credentials).query({ end: 'test' }).expect(400);
});
it('should return an error when the end parameter is not provided', async () => {
await request.get(api('livechat/analytics/dashboards/conversations-by-status')).set(credentials).query({ start: 'test' }).expect(400);
});
it('should return an error when the start parameter is not a valid date', async () => {
await request
.get(api('livechat/analytics/dashboards/conversations-by-status'))
.set(credentials)
.query({ start: 'test', end: 'test' })
.expect(400);
});
it('should return an error when the end parameter is not a valid date', async () => {
await request
.get(api('livechat/analytics/dashboards/conversations-by-status'))
.set(credentials)
.query({ start: '2020-01-01', end: 'test' })
.expect(400);
});
it('should return an error if dates are more than 1 year apart', async () => {
const oneYearAgo = new Date(Date.now() - 380 * 24 * 60 * 60 * 1000).toISOString();
const now = new Date().toISOString();
await request
.get(api('livechat/analytics/dashboards/conversations-by-status'))
.set(credentials)
.query({ start: oneYearAgo, end: now })
.expect(400);
});
it('should return an error when start is after end', async () => {
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString();
const now = new Date().toISOString();
await request
.get(api('livechat/analytics/dashboards/conversations-by-status'))
.set(credentials)
.query({ start: now, end: oneHourAgo })
.expect(400);
});
it('should return the proper data when the parameters are valid', async () => {
// Note: this way all data will come as 0
const now = new Date().toISOString();
const { body } = await request
.get(api('livechat/analytics/dashboards/conversations-by-status'))
.set(credentials)
.query({ start: now, end: now })
.expect(200);
expect(body).to.have.property('data').and.to.be.an('array');
expect(body.data).to.have.lengthOf(0);
expect(body.success).to.be.true;
});
it('should return public rooms for a monitor with no units', async () => {
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString();
const now = new Date().toISOString();
const { body } = await request
.get(api('livechat/analytics/dashboards/conversations-by-status'))
.set(agent2.credentials)
.query({ start: oneHourAgo, end: now })
.expect(200);
expect(body).to.have.property('data').and.to.be.an('array');
expect(body.data.every((room: IOmnichannelRoom) => !room.departmentAncestors && !room.departmentId)).to.be.true;
});
it('should return the proper data when login as a manager', async () => {
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString();
const now = new Date().toISOString();
const { body } = await request
.get(api('livechat/analytics/dashboards/conversations-by-status'))
.set(credentials)
.query({ start: oneHourAgo, end: now })
.expect(200);
expect(body).to.have.property('data').and.to.be.an('array');
expect(body.data).to.have.lengthOf.greaterThan(0);
expect(body.data.every((item: { value: number }) => item.value >= 0)).to.be.true;
});
});
describe('livechat/analytics/dashboards/conversations-by-department', () => {
it('should return an error when the user does not have the necessary permission', async () => {
await updatePermission('view-livechat-reports', []);
await request
.get(api('livechat/analytics/dashboards/conversations-by-department'))
.set(credentials)
.query({ start: 'test', end: 'test' })
.expect(403);
});
it('should return an error when the start and end parameters are not provided', async () => {
await restorePermissionToRoles('view-livechat-reports');
await request.get(api('livechat/analytics/dashboards/conversations-by-department')).set(credentials).expect(400);
});
it('should return an error when the start parameter is not provided', async () => {
await request
.get(api('livechat/analytics/dashboards/conversations-by-department'))
.set(credentials)
.query({ end: 'test' })
.expect(400);
});
it('should return an error when the end parameter is not provided', async () => {
await request
.get(api('livechat/analytics/dashboards/conversations-by-department'))
.set(credentials)
.query({ start: 'test' })
.expect(400);
});
it('should return an error when the start parameter is not a valid date', async () => {
await request
.get(api('livechat/analytics/dashboards/conversations-by-department'))
.set(credentials)
.query({ start: 'test', end: 'test' })
.expect(400);
});
it('should return an error when the end parameter is not a valid date', async () => {
await request
.get(api('livechat/analytics/dashboards/conversations-by-department'))
.set(credentials)
.query({ start: '2020-01-01', end: 'test' })
.expect(400);
});
it('should return an error if dates are more than 1 year apart', async () => {
const oneYearAgo = new Date(Date.now() - 380 * 24 * 60 * 60 * 1000).toISOString();
const now = new Date().toISOString();
await request
.get(api('livechat/analytics/dashboards/conversations-by-department'))
.set(credentials)
.query({ start: oneYearAgo, end: now })
.expect(400);
});
it('should return an error when start is after end', async () => {
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString();
const now = new Date().toISOString();
await request
.get(api('livechat/analytics/dashboards/conversations-by-department'))
.set(credentials)
.query({ start: now, end: oneHourAgo })
.expect(400);
});
it('should return the proper data when the parameters are valid', async () => {
// Note: this way all data will come as 0
const now = new Date().toISOString();
const { body } = await request
.get(api('livechat/analytics/dashboards/conversations-by-department'))
.set(credentials)
.query({ start: now, end: now })
.expect(200);
expect(body).to.have.property('data').and.to.be.an('array');
expect(body.data).to.have.lengthOf(0);
expect(body.success).to.be.true;
});
it('should return public rooms for a monitor with no units', async () => {
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString();
const now = new Date().toISOString();
const { body } = await request
.get(api('livechat/analytics/dashboards/conversations-by-department'))
.set(agent2.credentials)
.query({ start: oneHourAgo, end: now })
.expect(200);
expect(body).to.have.property('data').and.to.be.an('array');
expect(body.data.every((room: IOmnichannelRoom) => !room.departmentAncestors && !room.departmentId)).to.be.true;
});
it('should return the proper data when login as a manager', async () => {
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString();
const now = new Date().toISOString();
const { body } = await request
.get(api('livechat/analytics/dashboards/conversations-by-department'))
.set(credentials)
.query({ start: oneHourAgo, end: now })
.expect(200);
expect(body).to.have.property('data').and.to.be.an('array');
expect(body.data).to.have.lengthOf.greaterThan(0);
expect(body.data.every((item: { value: number }) => item.value >= 0)).to.be.true;
});
});
describe('livechat/analytics/dashboards/conversations-by-tags', () => {
it('should return an error when the user does not have the necessary permission', async () => {
await updatePermission('view-livechat-reports', []);
await request
.get(api('livechat/analytics/dashboards/conversations-by-tags'))
.set(credentials)
.query({ start: 'test', end: 'test' })
.expect(403);
});
it('should return an error when the start and end parameters are not provided', async () => {
await restorePermissionToRoles('view-livechat-reports');
await request.get(api('livechat/analytics/dashboards/conversations-by-tags')).set(credentials).expect(400);
});
it('should return an error when the start parameter is not provided', async () => {
await request.get(api('livechat/analytics/dashboards/conversations-by-tags')).set(credentials).query({ end: 'test' }).expect(400);
});
it('should return an error when the end parameter is not provided', async () => {
await request.get(api('livechat/analytics/dashboards/conversations-by-tags')).set(credentials).query({ start: 'test' }).expect(400);
});
it('should return an error when the start parameter is not a valid date', async () => {
await request
.get(api('livechat/analytics/dashboards/conversations-by-tags'))
.set(credentials)
.query({ start: 'test', end: 'test' })
.expect(400);
});
it('should return an error when the end parameter is not a valid date', async () => {
await request
.get(api('livechat/analytics/dashboards/conversations-by-tags'))
.set(credentials)
.query({ start: '2020-01-01', end: 'test' })
.expect(400);
});
it('should return an error if dates are more than 1 year apart', async () => {
const oneYearAgo = new Date(Date.now() - 380 * 24 * 60 * 60 * 1000).toISOString();
const now = new Date().toISOString();
await request
.get(api('livechat/analytics/dashboards/conversations-by-tags'))
.set(credentials)
.query({ start: oneYearAgo, end: now })
.expect(400);
});
it('should return an error when start is after end', async () => {
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString();
const now = new Date().toISOString();
await request
.get(api('livechat/analytics/dashboards/conversations-by-tags'))
.set(credentials)
.query({ start: now, end: oneHourAgo })
.expect(400);
});
it('should return the proper data when the parameters are valid', async () => {
// Note: this way all data will come as 0
const now = new Date().toISOString();
const { body } = await request
.get(api('livechat/analytics/dashboards/conversations-by-tags'))
.set(credentials)
.query({ start: now, end: now })
.expect(200);
expect(body).to.have.property('data').and.to.be.an('array');
expect(body.data).to.have.lengthOf(0);
expect(body.success).to.be.true;
});
it('should return public rooms for a monitor with no units', async () => {
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString();
const now = new Date().toISOString();
const { body } = await request
.get(api('livechat/analytics/dashboards/conversations-by-tags'))
.set(agent2.credentials)
.query({ start: oneHourAgo, end: now })
.expect(200);
expect(body).to.have.property('data').and.to.be.an('array');
expect(body.data.every((room: IOmnichannelRoom) => !room.departmentAncestors && !room.departmentId)).to.be.true;
});
it('should return the proper data when login as a manager', async () => {
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString();
const now = new Date().toISOString();
const { body } = await request
.get(api('livechat/analytics/dashboards/conversations-by-tags'))
.set(credentials)
.query({ start: oneHourAgo, end: now })
.expect(200);
expect(body).to.have.property('data').and.to.be.an('array');
expect(body.data).to.have.lengthOf.greaterThan(0);
expect(body.data.every((item: { value: number }) => item.value >= 0)).to.be.true;
});
});
describe('livechat/analytics/dashboards/conversations-by-agent', () => {
it('should return an error when the user does not have the necessary permission', async () => {
await updatePermission('view-livechat-reports', []);
await request
.get(api('livechat/analytics/dashboards/conversations-by-agent'))
.set(credentials)
.query({ start: 'test', end: 'test' })
.expect(403);
});
it('should return an error when the start and end parameters are not provided', async () => {
await restorePermissionToRoles('view-livechat-reports');
await request.get(api('livechat/analytics/dashboards/conversations-by-agent')).set(credentials).expect(400);
});
it('should return an error when the start parameter is not provided', async () => {
await request.get(api('livechat/analytics/dashboards/conversations-by-agent')).set(credentials).query({ end: 'test' }).expect(400);
});
it('should return an error when the end parameter is not provided', async () => {
await request.get(api('livechat/analytics/dashboards/conversations-by-agent')).set(credentials).query({ start: 'test' }).expect(400);
});
it('should return an error when the start parameter is not a valid date', async () => {
await request
.get(api('livechat/analytics/dashboards/conversations-by-agent'))
.set(credentials)
.query({ start: 'test', end: 'test' })
.expect(400);
});
it('should return an error when the end parameter is not a valid date', async () => {
await request
.get(api('livechat/analytics/dashboards/conversations-by-agent'))
.set(credentials)
.query({ start: '2020-01-01', end: 'test' })
.expect(400);
});
it('should return an error if dates are more than 1 year apart', async () => {
const oneYearAgo = new Date(Date.now() - 380 * 24 * 60 * 60 * 1000).toISOString();
const now = new Date().toISOString();
await request
.get(api('livechat/analytics/dashboards/conversations-by-agent'))
.set(credentials)
.query({ start: oneYearAgo, end: now })
.expect(400);
});
it('should return an error when start is after end', async () => {
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString();
const now = new Date().toISOString();
await request
.get(api('livechat/analytics/dashboards/conversations-by-agent'))
.set(credentials)
.query({ start: now, end: oneHourAgo })
.expect(400);
});
it('should return the proper data when the parameters are valid', async () => {
// Note: this way all data will come as 0
const now = new Date().toISOString();
const { body } = await request
.get(api('livechat/analytics/dashboards/conversations-by-agent'))
.set(credentials)
.query({ start: now, end: now })
.expect(200);
expect(body).to.have.property('data').and.to.be.an('array');
expect(body.data).to.have.lengthOf(0);
});
it('should return public rooms for a monitor with no units', async () => {
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString();
const now = new Date().toISOString();
const { body } = await request
.get(api('livechat/analytics/dashboards/conversations-by-agent'))
.set(agent2.credentials)
.query({ start: oneHourAgo, end: now })
.expect(200);
expect(body).to.have.property('data').and.to.be.an('array');
expect(body.data.every((room: IOmnichannelRoom) => !room.departmentAncestors && !room.departmentId)).to.be.true;
});
it('should return the proper data when login as a manager', async () => {
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString();
const now = new Date().toISOString();
const { body } = await request
.get(api('livechat/analytics/dashboards/conversations-by-agent'))
.set(credentials)
.query({ start: oneHourAgo, end: now })
.expect(200);
expect(body).to.have.property('data').and.to.be.an('array');
expect(body.data).to.have.lengthOf.greaterThan(0);
expect(body.data.every((item: { value: number }) => item.value >= 0)).to.be.true;
});
});
});

View File

@@ -0,0 +1,301 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import type { Credentials } from '@rocket.chat/api-client';
import type { ILivechatDepartment, IUser } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { before, it, describe, after } from 'mocha';
import { getCredentials, api, request, credentials } from '../../../data/api-data';
import { addOrRemoveAgentFromDepartment, createDepartment } from '../../../data/livechat/department';
import {
createAgent,
createLivechatRoom,
createManager,
createVisitor,
getLivechatRoomInfo,
makeAgentAvailable,
} from '../../../data/livechat/rooms';
import { createMonitor, createUnit } from '../../../data/livechat/units';
import { updateSetting, updatePermission, restorePermissionToRoles, removePermissionFromAllRoles } from '../../../data/permissions.helper';
import { password } from '../../../data/user';
import { createUser, deleteUser, login, setUserActiveStatus } from '../../../data/users.helper';
import { IS_EE } from '../../../e2e/config/constants';
type TestUser = { user: IUser; credentials: Credentials };
(IS_EE ? describe : describe.skip)('Omnichannel - Monitors', () => {
let manager: TestUser;
let monitor: TestUser;
let noUnitDepartment: ILivechatDepartment;
let unitDepartment: ILivechatDepartment;
before((done) => getCredentials(done));
before(async () => {
await updateSetting('Livechat_accept_chats_with_no_agents', true);
await setUserActiveStatus('rocketchat.internal.admin.test', true);
await createAgent();
await makeAgentAvailable();
});
before(async () => {
const user = await createUser();
const userCredentials = await login(user.username, password);
if (!user.username) {
throw new Error('user not created');
}
await createManager(user.username);
manager = {
user,
credentials: userCredentials,
};
});
before(async () => {
const user = await createUser();
const userCredentials = await login(user.username, password);
if (!user.username) {
throw new Error('user not created');
}
await createMonitor(user.username);
monitor = {
user,
credentials: userCredentials,
};
});
before(async () => {
noUnitDepartment = await createDepartment();
unitDepartment = await createDepartment();
await createUnit(monitor.user._id, monitor.user.username!, [unitDepartment._id]);
});
before(async () => {
await updatePermission('transfer-livechat-guest', ['admin', 'livechat-manager', 'livechat-agent', 'livechat-monitor']);
});
describe('Monitors', () => {
let user: IUser;
before(async () => {
user = await createUser();
});
after(async () => {
await deleteUser(user);
});
it('should properly create a new monitor', async () => {
const { body } = await request.post(api('livechat/monitors.create')).send({ username: user.username }).set(credentials).expect(200);
expect(body.success).to.be.true;
});
it('should not fail when trying to create a monitor that already exists', async () => {
const { body } = await request.post(api('livechat/monitors.create')).send({ username: user.username }).set(credentials).expect(200);
expect(body.success).to.be.true;
});
it('should fail when trying to create a monitor with an invalid username', async () => {
const { body } = await request
.post(api('livechat/monitors.create'))
.set(credentials)
.send({ username: 'invalid-username' })
.expect(400);
expect(body.success).to.be.false;
expect(body).to.have.property('error').to.be.equal('Invalid user [error-invalid-user]');
});
it('should fail when trying to create a monitor with an empty username', async () => {
const { body } = await request.post(api('livechat/monitors.create')).set(credentials).send({ username: '' }).expect(400);
expect(body.success).to.be.false;
expect(body).to.have.property('error').to.be.equal('Invalid user [error-invalid-user]');
});
it('should remove a monitor', async () => {
const { body } = await request.post(api('livechat/monitors.delete')).set(credentials).send({ username: user.username }).expect(200);
expect(body.success).to.be.true;
});
it('should fail when trying to remove a monitor that does not exist', async () => {
const { body } = await request
.post(api('livechat/monitors.delete'))
.set(credentials)
.send({ username: 'some-random-username' })
.expect(400);
expect(body.success).to.be.false;
expect(body).to.have.property('error').to.be.equal('Invalid user');
});
});
describe('[GET] livechat/monitors', () => {
it('should fail if manage-livechat-monitors permission is missing', async () => {
await removePermissionFromAllRoles('manage-livechat-monitors');
return request.get(api('livechat/monitors')).set(credentials).expect(403);
});
it('should return all monitors', async () => {
await restorePermissionToRoles('manage-livechat-monitors');
const user = await createUser();
await createMonitor(user.username);
const { body } = await request.get(api('livechat/monitors')).set(credentials).query({ text: user.username }).expect(200);
expect(body).to.have.property('monitors');
expect(body.monitors).to.have.lengthOf(1);
expect(body.monitors[0]).to.have.property('username', user.username);
// cleanup
await deleteUser(user);
});
});
describe('livechat/monitors/:username', () => {
it('should fail if manage-livechat-monitors permission is missing', async () => {
await removePermissionFromAllRoles('manage-livechat-monitors');
return request.get(api('livechat/monitors/123')).set(credentials).expect(403);
});
it('should return a monitor', async () => {
await restorePermissionToRoles('manage-livechat-monitors');
const user = await createUser();
await createMonitor(user.username);
const { body } = await request
.get(api(`livechat/monitors/${user.username}`))
.set(credentials)
.expect(200);
expect(body).to.have.property('username', user.username);
// cleanup
await deleteUser(user);
});
});
describe('Monitors & Rooms', () => {
it('should not return a room of a department that the monitor is not assigned to', async () => {
const visitor = await createVisitor(noUnitDepartment._id);
const room = await createLivechatRoom(visitor.token);
const { body } = await request
.get(api('livechat/rooms'))
.set(monitor.credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(body).to.have.property('rooms').that.is.an('array');
expect(body.rooms.find((r: any) => r._id === room._id)).to.not.exist;
});
it('should return a room of a department the monitor is assigned to', async () => {
const visitor = await createVisitor(unitDepartment._id);
const room = await createLivechatRoom(visitor.token);
const { body } = await request
.get(api('livechat/rooms'))
.set(monitor.credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(body).to.have.property('rooms').that.is.an('array');
expect(body.rooms.find((r: any) => r._id === room._id)).to.exist;
});
});
describe('Monitors & Departments', () => {
it('should not return a department that the monitor is not assigned to', async () => {
const { body } = await request
.get(api('livechat/department'))
.query({ onlyMyDepartments: true })
.set(monitor.credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(body).to.have.property('departments').that.is.an('array');
expect(body.departments.find((d: any) => d._id === noUnitDepartment._id)).to.not.exist;
});
it('should return a department that the monitor is assigned to', async () => {
const { body } = await request
.get(api('livechat/department'))
.query({ onlyMyDepartments: true })
.set(monitor.credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(body).to.have.property('departments').that.is.an('array');
expect(body.departments.length).to.be.equal(1);
expect(body.departments.find((d: any) => d._id === unitDepartment._id)).to.exist;
});
it('should return both created departments to a manager', async () => {
const { body } = await request
.get(api('livechat/department'))
.query({ onlyMyDepartments: true, sort: '{ "_updatedAt": 1 }' })
.set(manager.credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(body).to.have.property('departments').that.is.an('array');
expect(body.departments.find((d: any) => d._id === noUnitDepartment._id)).to.exist;
expect(body.departments.find((d: any) => d._id === unitDepartment._id)).to.exist;
});
// OnlyMyDepartments to return data from units + the ones you serve in
it.skip('should not return a department when monitor is only assigned as agent there', async () => {
await createAgent(monitor.user.username!);
await addOrRemoveAgentFromDepartment(
noUnitDepartment._id,
{ agentId: monitor.user._id, username: monitor.user.username!, count: 0, order: 0 },
true,
);
const { body } = await request
.get(api('livechat/department'))
.query({ onlyMyDepartments: true })
.set(monitor.credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(body).to.have.property('departments').that.is.an('array');
expect(body.departments.length).to.be.equal(1);
expect(body.departments.find((d: any) => d._id === noUnitDepartment._id)).to.not.exist;
});
});
describe('Monitors & Forward', () => {
it('should successfully forward a room to another agent', async () => {
const visitor = await createVisitor(unitDepartment._id);
const room = await createLivechatRoom(visitor.token);
const { body } = await request
.post(api('livechat/room.forward'))
.set(monitor.credentials)
.send({
roomId: room._id,
userId: 'rocketchat.internal.admin.test',
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(body).to.have.property('success', true);
const room2 = await getLivechatRoomInfo(room._id);
expect(room2).to.have.property('servedBy').that.is.an('object');
expect(room2.servedBy).to.have.property('_id', 'rocketchat.internal.admin.test');
});
it('should successfully forward a room to a department', async () => {
const visitor = await createVisitor(noUnitDepartment._id);
const room = await createLivechatRoom(visitor.token);
const { body } = await request
.post(api('livechat/room.forward'))
.set(monitor.credentials)
.send({
roomId: room._id,
departmentId: unitDepartment._id,
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(body).to.have.property('success', true);
const room2 = await getLivechatRoomInfo(room._id);
expect(room2.departmentId).to.be.equal(unitDepartment._id);
});
});
});

View File

@@ -0,0 +1,134 @@
import type { ILivechatVisitor } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { before, afterEach, after, describe, it } from 'mocha';
import moment from 'moment';
import { api, getCredentials, request, credentials } from '../../../data/api-data';
import {
createVisitor,
createLivechatRoom,
createAgent,
makeAgentAvailable,
sendAgentMessage,
getLivechatRoomInfo,
fetchInquiry,
closeOmnichannelRoom,
deleteVisitor,
} from '../../../data/livechat/rooms';
describe('MAC', () => {
before((done) => getCredentials(done));
before(async () => {
await createAgent();
await makeAgentAvailable();
});
describe('MAC rooms', () => {
let visitor: ILivechatVisitor;
let multipleContactsVisitor: ILivechatVisitor;
afterEach(() => deleteVisitor(visitor.token));
after(() => deleteVisitor(multipleContactsVisitor.token));
it('Should create an innactive room and contact by default', async () => {
visitor = await createVisitor();
const room = await createLivechatRoom(visitor.token);
expect(room).to.be.an('object');
expect(room.v.activity).to.be.undefined;
});
it('Should create an innactive contact by default', async () => {
visitor = await createVisitor();
const room = await createLivechatRoom(visitor.token);
const res = await request.get(api(`omnichannel/contacts.get`)).set(credentials).query({ contactId: room.contactId });
expect(res.body.contact.channels[0].visitor.visitorId).to.be.equal(visitor._id);
expect(res.body.contact).not.to.have.property('activity');
});
it('should mark room as active when agent sends a message', async () => {
visitor = await createVisitor();
const room = await createLivechatRoom(visitor.token);
await sendAgentMessage(room._id);
const updatedRoom = await getLivechatRoomInfo(room._id);
expect(updatedRoom).to.have.nested.property('v.activity').and.to.be.an('array');
});
it('should mark contact as active when agent sends a message', async () => {
multipleContactsVisitor = await createVisitor();
const room = await createLivechatRoom(multipleContactsVisitor.token);
await sendAgentMessage(room._id);
const res = await request.get(api(`omnichannel/contacts.get`)).set(credentials).query({ contactId: room.contactId });
expect(res.body.contact.channels[0].visitor.visitorId).to.be.equal(multipleContactsVisitor._id);
expect(res.body.contact).to.have.property('activity').that.is.an('array').with.lengthOf(1);
expect(res.body.contact.activity[0]).to.equal(moment.utc().format('YYYY-MM'));
});
it('should mark multiple rooms as active when they come from same contact after an agent sends a message', async () => {
const room = await createLivechatRoom(multipleContactsVisitor.token);
await sendAgentMessage(room._id);
const updatedRoom = await getLivechatRoomInfo(room._id);
expect(updatedRoom).to.have.nested.property('v.activity').and.to.be.an('array');
await closeOmnichannelRoom(room._id);
});
it('should keep contact active when reusing it and an agent response is received', async () => {
const room = await createLivechatRoom(multipleContactsVisitor.token);
await sendAgentMessage(room._id);
const res = await request.get(api(`omnichannel/contacts.get`)).set(credentials).query({ contactId: room.contactId });
expect(res.body.contact.channels[0].visitor.visitorId).to.be.equal(multipleContactsVisitor._id);
expect(res.body.contact).to.have.property('activity').that.is.an('array').with.lengthOf(1);
expect(res.body.contact.activity[0]).to.equal(moment.utc().format('YYYY-MM'));
await closeOmnichannelRoom(room._id);
});
it('should mark room as active when it comes from same contact on same period, even without agent interaction', async () => {
const room = await createLivechatRoom(multipleContactsVisitor.token);
expect(room).to.have.nested.property('v.activity').and.to.be.an('array');
expect(room.v.activity?.includes(moment.utc().format('YYYY-MM'))).to.be.true;
await closeOmnichannelRoom(room._id);
});
it('should mark an inquiry as active when it comes from same contact on same period, even without agent interaction', async () => {
const room = await createLivechatRoom(multipleContactsVisitor.token);
const inquiry = await fetchInquiry(room._id);
expect(inquiry).to.have.nested.property('v.activity').and.to.be.an('array');
expect(inquiry.v.activity?.includes(moment.utc().format('YYYY-MM'))).to.be.true;
expect(room.v.activity?.includes(moment.utc().format('YYYY-MM'))).to.be.true;
await closeOmnichannelRoom(room._id);
});
it('contact should be marked as active for period', async () => {
visitor = await createVisitor();
const room = await createLivechatRoom(visitor.token);
await sendAgentMessage(room._id);
const res = await request.get(api(`omnichannel/contacts.get`)).set(credentials).query({ contactId: room.contactId });
expect(res.status).to.be.equal(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.nested.property('contact').and.to.be.an('object');
expect(res.body.contact.channels).to.be.an('array').with.lengthOf(1);
expect(res.body.contact.channels[0].name).to.be.equal('api');
expect(res.body.contact.channels[0].visitor.visitorId).to.be.equal(visitor._id);
expect(res.body.contact).to.have.nested.property('activity').and.to.be.an('array').with.lengthOf(1);
expect(res.body.contact.activity[0]).to.equal(moment.utc().format('YYYY-MM'));
await closeOmnichannelRoom(room._id);
});
});
});

View File

@@ -0,0 +1,702 @@
import { faker } from '@faker-js/faker';
import type { Credentials } from '@rocket.chat/api-client';
import { UserStatus } from '@rocket.chat/core-typings';
import type { ILivechatDepartment, IUser, ILivechatAgent } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import { getCredentials, request, api, credentials } from '../../../data/api-data';
import {
createAgent,
makeAgentAvailable,
createDepartment,
createVisitor,
createLivechatRoom,
getLivechatRoomInfo,
makeAgentUnavailable,
switchLivechatStatus,
} from '../../../data/livechat/rooms';
import { getAgent } from '../../../data/livechat/users';
import { getSettingValueById, updateSetting } from '../../../data/permissions.helper';
import { password } from '../../../data/user';
import { createUser, deleteUser, login, setUserActiveStatus, setUserStatus } from '../../../data/users.helper';
import { IS_EE } from '../../../e2e/config/constants';
(IS_EE ? describe : describe.skip)('Omnichannel - Routing', () => {
before((done) => getCredentials(done));
after(async () => {
await updateSetting('Livechat_Routing_Method', 'Manual_Selection');
});
// Basically: if there's a bot in the department, it should be assigned to the conversation
// No matter what settings workspace has in, as long as the setting to assign new conversations to bots is enabled
describe('Bots - Manual selection', () => {
let botUser: { user: IUser; credentials: Credentials };
let testDepartment: ILivechatDepartment;
let testDepartment2: ILivechatDepartment;
before(async () => {
const bot = await createUser({ roles: ['bot', 'livechat-agent'] });
const credentials = await login(bot.username, password);
botUser = { user: bot, credentials };
});
before(async () => {
testDepartment = await createDepartment([{ agentId: botUser.user._id }]);
testDepartment2 = await createDepartment();
await updateSetting('Livechat_Routing_Method', 'Manual_Selection');
await updateSetting('Livechat_assign_new_conversation_to_bot', true);
await updateSetting('Livechat_accept_chats_with_no_agents', true);
});
after(async () => {
await deleteUser(botUser.user);
await updateSetting('Livechat_Routing_Method', 'Auto_Selection');
await updateSetting('Livechat_assign_new_conversation_to_bot', false);
await updateSetting('Livechat_accept_chats_with_no_agents', false);
});
it('should assign conversation to bot', async () => {
const visitor = await createVisitor(testDepartment._id);
const room = await createLivechatRoom(visitor.token);
const roomInfo = await getLivechatRoomInfo(room._id);
expect(roomInfo.servedBy?._id).to.be.equal(botUser.user._id);
});
it('should not assign conversation to bot if department has no bots', async () => {
const visitor = await createVisitor(testDepartment2._id);
const room = await createLivechatRoom(visitor.token);
expect(room.servedBy).to.be.undefined;
});
describe('with setting disabled', () => {
before(async () => {
await updateSetting('Livechat_assign_new_conversation_to_bot', false);
});
after(async () => {
await updateSetting('Livechat_assign_new_conversation_to_bot', true);
});
it('should not assign conversation to bot', async () => {
const visitor = await createVisitor(testDepartment._id);
const room = await createLivechatRoom(visitor.token);
expect(room.servedBy).to.be.undefined;
});
});
});
describe('Bots - Auto selection', () => {
let botUser: { user: IUser; credentials: Credentials };
let otherUser: { user: IUser; credentials: Credentials };
let testDepartment: ILivechatDepartment;
let testDepartment2: ILivechatDepartment;
before(async () => {
const bot = await createUser({ roles: ['bot', 'livechat-agent'] });
const credentials = await login(bot.username, password);
const other = await createUser({ roles: ['livechat-agent'] });
const otherCredentials = await login(other.username, password);
await makeAgentAvailable(otherCredentials);
botUser = { user: bot, credentials };
otherUser = { user: other, credentials: otherCredentials };
});
before(async () => {
testDepartment = await createDepartment([{ agentId: botUser.user._id }, { agentId: otherUser.user._id }]);
testDepartment2 = await createDepartment();
await updateSetting('Livechat_Routing_Method', 'Auto_Selection');
await updateSetting('Livechat_assign_new_conversation_to_bot', true);
await updateSetting('Livechat_accept_chats_with_no_agents', true);
});
after(async () => {
await deleteUser(botUser.user);
await updateSetting('Livechat_assign_new_conversation_to_bot', false);
await updateSetting('Livechat_accept_chats_with_no_agents', false);
});
it('should assign conversation to bot', async () => {
const visitor = await createVisitor(testDepartment._id);
const room = await createLivechatRoom(visitor.token);
const roomInfo = await getLivechatRoomInfo(room._id);
expect(roomInfo.servedBy?._id).to.be.equal(botUser.user._id);
});
it('should not assign conversation to bot if department has no bots', async () => {
const visitor = await createVisitor(testDepartment2._id);
const room = await createLivechatRoom(visitor.token);
expect(room.servedBy).to.be.undefined;
});
describe('with setting disabled', () => {
before(async () => {
await updateSetting('Livechat_assign_new_conversation_to_bot', false);
});
after(async () => {
await updateSetting('Livechat_assign_new_conversation_to_bot', true);
});
it('should not assign conversation to bot', async () => {
const visitor = await createVisitor(testDepartment._id);
const room = await createLivechatRoom(visitor.token);
expect(room.servedBy?._id).to.be.equal(otherUser.user._id);
});
});
});
describe('Bots - Auto selection & Waiting queue', () => {
let botUser: { user: IUser; credentials: Credentials };
let testDepartment: ILivechatDepartment;
let testDepartment2: ILivechatDepartment;
before(async () => {
const bot = await createUser({ roles: ['bot', 'livechat-agent'] });
const credentials = await login(bot.username, password);
botUser = { user: bot, credentials };
});
before(async () => {
testDepartment = await createDepartment([{ agentId: botUser.user._id }]);
testDepartment2 = await createDepartment();
await updateSetting('Livechat_Routing_Method', 'Auto_Selection');
await updateSetting('Livechat_waiting_queue', true);
await updateSetting('Livechat_assign_new_conversation_to_bot', true);
await updateSetting('Livechat_accept_chats_with_no_agents', true);
});
after(async () => {
await deleteUser(botUser.user);
await updateSetting('Livechat_waiting_queue', false);
await updateSetting('Livechat_assign_new_conversation_to_bot', false);
await updateSetting('Livechat_accept_chats_with_no_agents', false);
});
it('should assign conversation to bot', async () => {
const visitor = await createVisitor(testDepartment._id);
const room = await createLivechatRoom(visitor.token);
const roomInfo = await getLivechatRoomInfo(room._id);
expect(roomInfo.servedBy?._id).to.be.equal(botUser.user._id);
});
it('should not assign conversation to bot if department has no bots', async () => {
const visitor = await createVisitor(testDepartment2._id);
const room = await createLivechatRoom(visitor.token);
expect(room.servedBy).to.be.undefined;
});
describe('with setting disabled', () => {
before(async () => {
await updateSetting('Livechat_assign_new_conversation_to_bot', false);
});
after(async () => {
await updateSetting('Livechat_assign_new_conversation_to_bot', true);
});
it('should not assign conversation to bot', async () => {
const visitor = await createVisitor(testDepartment._id);
const room = await createLivechatRoom(visitor.token);
expect(room.servedBy).to.be.undefined;
});
});
});
describe('Auto-Selection', () => {
before(async () => {
await updateSetting('Livechat_Routing_Method', 'Auto_Selection');
});
let testUser: { user: IUser; credentials: Credentials };
let testUser2: { user: IUser; credentials: Credentials };
let testUser3: { user: IUser; credentials: Credentials };
let testDepartment: ILivechatDepartment;
let visitorEmail: string;
before(async () => {
const user = await createUser();
await createAgent(user.username);
const credentials2 = await login(user.username, password);
await makeAgentAvailable(credentials2);
testUser = {
user,
credentials: credentials2,
};
});
before(async () => {
const user = await createUser();
await createAgent(user.username);
const credentials2 = await login(user.username, password);
await makeAgentUnavailable(credentials2);
testUser2 = {
user,
credentials: credentials2,
};
});
before(async () => {
const user = await createUser();
await createAgent(user.username);
const credentials3 = await login(user.username, password);
await makeAgentAvailable(credentials3);
testUser3 = {
user,
credentials: credentials3,
};
});
before(async () => {
testDepartment = await createDepartment([{ agentId: testUser.user._id }, { agentId: testUser3.user._id }]);
const visitorName = faker.person.fullName();
visitorEmail = faker.internet.email().toLowerCase();
await request
.post(api('omnichannel/contacts'))
.set(credentials)
.send({
name: visitorName,
emails: [visitorEmail],
phones: [],
contactManager: testUser3.user._id,
});
});
after(async () =>
Promise.all([
deleteUser(testUser.user),
deleteUser(testUser2.user),
deleteUser(testUser3.user),
updateSetting('Livechat_enabled_when_agent_idle', true),
]),
);
it('should route a room to an available agent', async () => {
const visitor = await createVisitor(testDepartment._id);
const room = await createLivechatRoom(visitor.token);
const roomInfo = await getLivechatRoomInfo(room._id);
expect(roomInfo.servedBy).to.be.an('object');
expect(roomInfo.servedBy?._id).to.be.equal(testUser.user._id);
expect(roomInfo.servedBy?._id).to.not.be.equal(testUser2.user._id);
});
it('should not route to a not-available agent', async () => {
const visitor = await createVisitor(testDepartment._id);
const room = await createLivechatRoom(visitor.token);
const roomInfo = await getLivechatRoomInfo(room._id);
expect(roomInfo.servedBy).to.be.an('object');
expect(roomInfo.servedBy?._id).to.not.be.equal(testUser2.user._id);
});
it('should route to contact manager if it is online and Omnichannel_contact_manager_routing is enabled', async () => {
const visitor = await createVisitor(testDepartment._id, faker.person.fullName(), visitorEmail);
const room = await createLivechatRoom(visitor.token);
const roomInfo = await getLivechatRoomInfo(room._id);
expect(roomInfo.servedBy).to.be.an('object');
expect(roomInfo.servedBy?._id).to.be.equal(testUser3.user._id);
});
it('should fail to start a conversation if there is noone available and Livechat_accept_chats_with_no_agents is false', async () => {
await updateSetting('Livechat_accept_chats_with_no_agents', false);
await makeAgentUnavailable(testUser.credentials);
await makeAgentUnavailable(testUser3.credentials);
const visitor = await createVisitor(testDepartment._id);
const { body } = await request.get(api('livechat/room')).query({ token: visitor.token }).expect(400);
expect(body.error).to.be.equal('Sorry, no online agents [no-agent-online]');
});
it('should accept a conversation but not route to anyone when Livechat_accept_chats_with_no_agents is true', async () => {
await updateSetting('Livechat_accept_chats_with_no_agents', true);
const visitor = await createVisitor(testDepartment._id);
const room = await createLivechatRoom(visitor.token);
const roomInfo = await getLivechatRoomInfo(room._id);
expect(roomInfo.servedBy).to.be.undefined;
});
it('should not allow users to take more than Livechat_maximum_chats_per_agent chats', async () => {
await updateSetting('Livechat_maximum_chats_per_agent', 2);
const visitor = await createVisitor(testDepartment._id);
const room = await createLivechatRoom(visitor.token);
const roomInfo = await getLivechatRoomInfo(room._id);
expect(roomInfo.servedBy).to.be.undefined;
});
it('should ignore disabled users', async () => {
await updateSetting('Livechat_maximum_chats_per_agent', 0);
await setUserActiveStatus(testUser2.user._id, false);
const visitor = await createVisitor(testDepartment._id);
const room = await createLivechatRoom(visitor.token);
const roomInfo = await getLivechatRoomInfo(room._id);
expect(roomInfo.servedBy).to.be.undefined;
});
it('should route to another available agent if contact manager is unavailable and Omnichannel_contact_manager_routing is enabled', async () => {
await makeAgentAvailable(testUser.credentials);
const visitor = await createVisitor(testDepartment._id, faker.person.fullName(), visitorEmail);
const room = await createLivechatRoom(visitor.token);
const roomInfo = await getLivechatRoomInfo(room._id);
expect(roomInfo.servedBy).to.be.an('object');
expect(roomInfo.servedBy?._id).to.be.equal(testUser.user._id);
});
describe('with setting Omnichannel_contact_manager_routing disabled', () => {
let testDepartment2: ILivechatDepartment;
before(async () => {
await updateSetting('Omnichannel_contact_manager_routing', false);
// We should update the count of the user
});
after(async () => {
await updateSetting('Omnichannel_contact_manager_routing', true);
});
before(async () => {
// This will deprioritize the CM from normal routing. So the test is predictable
// IF the setting is off, the CM will be treated as a normal agent
// IF the setting is on, the CM will be treated as a CM and no matter the count it will get assigned
testDepartment2 = await createDepartment([{ agentId: testUser.user._id }, { agentId: testUser3.user._id, count: 10000 }]);
const visitorName = faker.person.fullName();
visitorEmail = faker.internet.email().toLowerCase();
await request
.post(api('omnichannel/contacts'))
.set(credentials)
.send({
name: visitorName,
emails: [visitorEmail],
phones: [],
contactManager: testUser3.user._id,
});
});
it('should not route to contact manager if it is online but setting is off', async () => {
const visitor = await createVisitor(testDepartment2._id, faker.person.fullName(), visitorEmail);
const room = await createLivechatRoom(visitor.token);
const roomInfo = await getLivechatRoomInfo(room._id);
expect(roomInfo.servedBy).to.be.an('object').that.has.property('_id').that.is.not.equal(testUser3.user._id);
});
});
describe('Auto_Selection - Livechat_accept_chats_with_no_agents', async () => {
let initialSettingValue: any;
let testUserInitialState: ILivechatAgent;
let testUser3InitialState: ILivechatAgent;
before(async () => {
testUserInitialState = await getAgent(testUser.user._id);
testUser3InitialState = await getAgent(testUser3.user._id);
initialSettingValue = await getSettingValueById('Livechat_accept_chats_with_no_agents');
});
after(async () => {
await updateSetting('Livechat_accept_chats_with_no_agents', initialSettingValue);
await switchLivechatStatus(testUserInitialState.statusLivechat, testUser.credentials);
await switchLivechatStatus(testUser3InitialState.statusLivechat, testUser3.credentials);
await setUserStatus(testUser3.credentials, testUser3InitialState.status);
});
describe('Livechat_accept_chats_with_no_agents is false', () => {
before(async () => {
await updateSetting('Livechat_accept_chats_with_no_agents', false);
await switchLivechatStatus('not-available', testUser.credentials);
await switchLivechatStatus('available', testUser3.credentials);
await setUserStatus(testUser3.credentials, UserStatus.OFFLINE);
});
it('should fail to start a conversation if there is an available but offline agent and Livechat_accept_chats_with_no_agents is false', async () => {
const visitor = await createVisitor(testDepartment._id);
const { body } = await request.get(api('livechat/room')).query({ token: visitor.token }).expect(400);
expect(body.error).to.be.equal('Sorry, no online agents [no-agent-online]');
});
});
describe('Livechat_accept_chats_with_no_agents is true', () => {
before(async () => {
await updateSetting('Livechat_accept_chats_with_no_agents', true);
await switchLivechatStatus('available', testUser3.credentials);
await setUserStatus(testUser3.credentials, UserStatus.OFFLINE);
});
it('should accept a conversation and route to an offline agent', async () => {
const visitor = await createVisitor(testDepartment._id);
const room = await createLivechatRoom(visitor.token);
const roomInfo = await getLivechatRoomInfo(room._id);
expect(roomInfo.servedBy).to.be.an('object');
expect(roomInfo.servedBy?._id).to.be.equal(testUser3.user._id);
});
});
});
});
describe('Load Balancing', () => {
before(async () => {
await updateSetting('Livechat_Routing_Method', 'Load_Balancing');
});
let testUser: { user: IUser; credentials: Credentials };
let testUser2: { user: IUser; credentials: Credentials };
let testDepartment: ILivechatDepartment;
before(async () => {
const user = await createUser();
await createAgent(user.username);
const credentials2 = await login(user.username, password);
await makeAgentAvailable(credentials2);
testUser = {
user,
credentials: credentials2,
};
});
before(async () => {
const user = await createUser();
await createAgent(user.username);
const credentials2 = await login(user.username, password);
await makeAgentUnavailable(credentials2);
testUser2 = {
user,
credentials: credentials2,
};
});
before(async () => {
testDepartment = await createDepartment([{ agentId: testUser.user._id }, { agentId: testUser2.user._id }]);
});
after(async () => {
await deleteUser(testUser.user);
await deleteUser(testUser2.user);
});
it('should route a room to an available agent', async () => {
const visitor = await createVisitor(testDepartment._id);
const room = await createLivechatRoom(visitor.token);
const roomInfo = await getLivechatRoomInfo(room._id);
expect(roomInfo.servedBy).to.be.an('object');
expect(roomInfo.servedBy?._id).to.be.equal(testUser.user._id);
expect(roomInfo.servedBy?._id).to.not.be.equal(testUser2.user._id);
});
it('should not route to a not-available agent', async () => {
const visitor = await createVisitor(testDepartment._id);
const room = await createLivechatRoom(visitor.token);
const roomInfo = await getLivechatRoomInfo(room._id);
expect(roomInfo.servedBy).to.be.an('object');
expect(roomInfo.servedBy?._id).to.not.be.equal(testUser2.user._id);
});
it('should route the chat to the less busy agent when both are available', async () => {
await makeAgentAvailable(testUser2.credentials);
const visitor = await createVisitor(testDepartment._id);
const room = await createLivechatRoom(visitor.token);
const roomInfo = await getLivechatRoomInfo(room._id);
expect(roomInfo.servedBy).to.be.an('object');
expect(roomInfo.servedBy?._id).to.be.equal(testUser2.user._id);
});
it('should route again to the less busy agent', async () => {
const visitor = await createVisitor(testDepartment._id);
const room = await createLivechatRoom(visitor.token);
const roomInfo = await getLivechatRoomInfo(room._id);
expect(roomInfo.servedBy).to.be.an('object');
expect(roomInfo.servedBy?._id).to.be.equal(testUser2.user._id);
});
describe('Load_Balancing - Livechat_accept_chats_with_no_agents', async () => {
let initialSettingValue: any;
let testUserInitialState: ILivechatAgent;
let testUser2InitialState: ILivechatAgent;
before(async () => {
testUserInitialState = await getAgent(testUser.user._id);
testUser2InitialState = await getAgent(testUser2.user._id);
initialSettingValue = await getSettingValueById('Livechat_accept_chats_with_no_agents');
});
after(async () => {
await updateSetting('Livechat_accept_chats_with_no_agents', initialSettingValue);
await switchLivechatStatus(testUserInitialState.statusLivechat, testUser.credentials);
await switchLivechatStatus(testUser2InitialState.statusLivechat, testUser2.credentials);
await setUserStatus(testUser2.credentials, testUser2InitialState.status);
});
describe('Livechat_accept_chats_with_no_agents is false', () => {
before(async () => {
await updateSetting('Livechat_accept_chats_with_no_agents', false);
await switchLivechatStatus('not-available', testUser.credentials);
await switchLivechatStatus('available', testUser2.credentials);
await setUserStatus(testUser2.credentials, UserStatus.OFFLINE);
});
it('should fail to start a conversation if there is an available but offline agent and Livechat_accept_chats_with_no_agents is false', async () => {
const visitor = await createVisitor(testDepartment._id);
const { body } = await request.get(api('livechat/room')).query({ token: visitor.token }).expect(400);
expect(body.error).to.be.equal('Sorry, no online agents [no-agent-online]');
});
});
describe('Livechat_accept_chats_with_no_agents is true', () => {
before(async () => {
await updateSetting('Livechat_accept_chats_with_no_agents', true);
await switchLivechatStatus('available', testUser2.credentials);
await setUserStatus(testUser2.credentials, UserStatus.OFFLINE);
});
it('should accept a conversation and route to an offline agent', async () => {
const visitor = await createVisitor(testDepartment._id);
const room = await createLivechatRoom(visitor.token);
const roomInfo = await getLivechatRoomInfo(room._id);
expect(roomInfo.servedBy).to.be.an('object');
expect(roomInfo.servedBy?._id).to.be.equal(testUser2.user._id);
});
});
});
});
describe('Load Rotation', () => {
before(async () => {
await updateSetting('Livechat_Routing_Method', 'Load_Rotation');
});
let testUser: { user: IUser; credentials: Credentials };
let testUser2: { user: IUser; credentials: Credentials };
let testDepartment: ILivechatDepartment;
before(async () => {
const user = await createUser();
await createAgent(user.username);
const credentials2 = await login(user.username, password);
await makeAgentAvailable(credentials2);
testUser = {
user,
credentials: credentials2,
};
});
before(async () => {
const user = await createUser();
await createAgent(user.username);
const credentials2 = await login(user.username, password);
await makeAgentUnavailable(credentials2);
testUser2 = {
user,
credentials: credentials2,
};
});
before(async () => {
testDepartment = await createDepartment([{ agentId: testUser.user._id }, { agentId: testUser2.user._id }]);
});
after(async () => {
await deleteUser(testUser.user);
await deleteUser(testUser2.user);
});
it('should route a room to an available agent', async () => {
const visitor = await createVisitor(testDepartment._id);
const room = await createLivechatRoom(visitor.token);
const roomInfo = await getLivechatRoomInfo(room._id);
expect(roomInfo.servedBy).to.be.an('object');
expect(roomInfo.servedBy?._id).to.be.equal(testUser.user._id);
expect(roomInfo.servedBy?._id).to.not.be.equal(testUser2.user._id);
});
it('should not route chat to an unavailable agent', async () => {
const visitor = await createVisitor(testDepartment._id);
const room = await createLivechatRoom(visitor.token);
const roomInfo = await getLivechatRoomInfo(room._id);
expect(roomInfo.servedBy).to.be.an('object');
expect(roomInfo.servedBy?._id).to.not.be.equal(testUser2.user._id);
});
it('should route the chat to the agent with the oldest routing time when both are available', async () => {
await makeAgentAvailable(testUser2.credentials);
const visitor = await createVisitor(testDepartment._id);
const room = await createLivechatRoom(visitor.token);
const roomInfo = await getLivechatRoomInfo(room._id);
expect(roomInfo.servedBy).to.be.an('object');
expect(roomInfo.servedBy?._id).to.be.equal(testUser2.user._id);
});
it('should route again to the agent with the oldest routing time', async () => {
const visitor = await createVisitor(testDepartment._id);
const room = await createLivechatRoom(visitor.token);
const roomInfo = await getLivechatRoomInfo(room._id);
expect(roomInfo.servedBy).to.be.an('object');
expect(roomInfo.servedBy?._id).to.be.equal(testUser.user._id);
});
describe('Load_Rotation - Livechat_accept_chats_with_no_agents', async () => {
let initialSettingValue: any;
let testUserInitialState: ILivechatAgent;
let testUser2InitialState: ILivechatAgent;
before(async () => {
testUserInitialState = await getAgent(testUser.user._id);
testUser2InitialState = await getAgent(testUser2.user._id);
initialSettingValue = await getSettingValueById('Livechat_accept_chats_with_no_agents');
});
after(async () => {
await updateSetting('Livechat_accept_chats_with_no_agents', initialSettingValue);
await switchLivechatStatus(testUserInitialState.statusLivechat, testUser.credentials);
await switchLivechatStatus(testUser2InitialState.statusLivechat, testUser2.credentials);
await setUserStatus(testUser2.credentials, testUser2InitialState.status);
});
describe('Livechat_accept_chats_with_no_agents is false', () => {
before(async () => {
await updateSetting('Livechat_accept_chats_with_no_agents', false);
await switchLivechatStatus('not-available', testUser.credentials);
await switchLivechatStatus('available', testUser2.credentials);
await setUserStatus(testUser2.credentials, UserStatus.OFFLINE);
});
it('should fail to start a conversation if there is an available but offline agent and Livechat_accept_chats_with_no_agents is false', async () => {
const visitor = await createVisitor(testDepartment._id);
const { body } = await request.get(api('livechat/room')).query({ token: visitor.token }).expect(400);
expect(body.error).to.be.equal('Sorry, no online agents [no-agent-online]');
});
});
describe('Livechat_accept_chats_with_no_agents is true', () => {
before(async () => {
await updateSetting('Livechat_accept_chats_with_no_agents', true);
await switchLivechatStatus('available', testUser2.credentials);
await setUserStatus(testUser2.credentials, UserStatus.OFFLINE);
});
it('should accept a conversation and route to an offline agent', async () => {
const visitor = await createVisitor(testDepartment._id);
const room = await createLivechatRoom(visitor.token);
const roomInfo = await getLivechatRoomInfo(room._id);
expect(roomInfo.servedBy).to.be.an('object');
expect(roomInfo.servedBy?._id).to.be.equal(testUser2.user._id);
});
});
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,297 @@
import type { ILivechatCustomField } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import type { Response } from 'supertest';
import { getCredentials, api, request, credentials } from '../../../data/api-data';
import { createCustomField, deleteCustomField } from '../../../data/livechat/custom-fields';
import {
getSettingValueById,
removePermissionFromAllRoles,
restorePermissionToRoles,
updateSetting,
} from '../../../data/permissions.helper';
describe('LIVECHAT - custom fields', () => {
let settingLivechatEnabled: boolean;
before((done) => getCredentials(done));
before(async () => {
settingLivechatEnabled = (await getSettingValueById('Livechat_enabled')) as boolean;
await updateSetting('Livechat_enabled', true);
});
after(async () => {
await updateSetting('Livechat_enabled', settingLivechatEnabled);
});
describe('livechat/custom-fields.save', () => {
let customFieldId: string;
after(async () => {
if (customFieldId) {
await deleteCustomField(customFieldId);
}
});
describe('Authentication/Authorization', () => {
it('should return an "unauthenticated error" when user is not logged in', async () => {
await request
.post(api('livechat/custom-fields.save'))
.send({
customFieldId: null,
customFieldData: {
field: 'test_field',
label: 'Test Field',
scope: 'visitor',
visibility: 'public',
},
})
.expect('Content-Type', 'application/json')
.expect(401);
});
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await removePermissionFromAllRoles('view-livechat-manager');
await request
.post(api('livechat/custom-fields.save'))
.set(credentials)
.send({
customFieldId: null,
customFieldData: {
field: 'test_field',
label: 'Test Field',
scope: 'visitor',
visibility: 'public',
},
})
.expect('Content-Type', 'application/json')
.expect(403);
await restorePermissionToRoles('view-livechat-manager');
});
});
describe('Create custom field', () => {
afterEach(async () => {
if (customFieldId) {
await deleteCustomField(customFieldId);
customFieldId = '';
}
});
it('should create a new custom field with minimal required fields', async () => {
const fieldName = `field_${Date.now()}`;
await request
.post(api('livechat/custom-fields.save'))
.set(credentials)
.send({
customFieldId: null,
customFieldData: {
field: fieldName,
label: 'Test Field',
scope: 'visitor',
visibility: 'public',
},
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('customField');
expect(res.body.customField).to.have.property('_id');
expect(res.body.customField).to.have.property('label', 'Test Field');
expect(res.body.customField).to.have.property('scope', 'visitor');
expect(res.body.customField).to.have.property('visibility', 'public');
customFieldId = res.body.customField._id;
});
});
it('should create a new custom field with scope "room"', async () => {
const fieldName = `room_field_${Date.now()}`;
await request
.post(api('livechat/custom-fields.save'))
.set(credentials)
.send({
customFieldId: null,
customFieldData: {
field: fieldName,
label: 'Room Test Field',
scope: 'room',
visibility: 'public',
},
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('customField');
expect(res.body.customField).to.have.property('scope', 'room');
customFieldId = res.body.customField._id;
});
});
it('should create a new custom field with all optional fields', async () => {
const fieldName = `full_field_${Date.now()}`;
await request
.post(api('livechat/custom-fields.save'))
.set(credentials)
.send({
customFieldId: null,
customFieldData: {
field: fieldName,
label: 'Full Test Field',
scope: 'visitor',
visibility: 'public',
type: 'input',
regexp: '^[A-Za-z]+$',
required: true,
defaultValue: 'default',
options: 'option1,option2,option3',
public: true,
searchable: true,
},
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('customField');
expect(res.body.customField).to.have.property('type', 'input');
expect(res.body.customField).to.have.property('regexp', '^[A-Za-z]+$');
expect(res.body.customField).to.have.property('required', true);
expect(res.body.customField).to.have.property('defaultValue', 'default');
expect(res.body.customField).to.have.property('options', 'option1,option2,option3');
expect(res.body.customField).to.have.property('public', true);
expect(res.body.customField).to.have.property('searchable', true);
customFieldId = res.body.customField._id;
});
});
it('should fail when trying to create a custom field with a field name that already exists', async () => {
const fieldName = `duplicate_field_${Date.now()}`;
// Create the first custom field
const { body } = await request
.post(api('livechat/custom-fields.save'))
.set(credentials)
.send({
customFieldId: null,
customFieldData: {
field: fieldName,
label: 'First Field',
scope: 'visitor',
visibility: 'public',
},
})
.expect(200);
customFieldId = body.customField._id;
// Try to create another with the same field name
await request
.post(api('livechat/custom-fields.save'))
.set(credentials)
.send({
customFieldId: null,
customFieldData: {
field: fieldName,
label: 'Second Field',
scope: 'visitor',
visibility: 'public',
},
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error');
});
});
});
describe('Update custom field', () => {
let existingField: ILivechatCustomField;
before(async () => {
const fieldName = `update_test_field_${Date.now()}`;
existingField = await createCustomField({
searchable: true,
field: fieldName,
label: 'Original Label',
defaultValue: 'original_default',
scope: 'visitor',
visibility: 'public',
regexp: '',
});
});
after(async () => {
if (existingField?._id) {
await deleteCustomField(existingField._id);
}
});
it('should fail when trying to update a non-existent custom field', async () => {
await request
.post(api('livechat/custom-fields.save'))
.set(credentials)
.send({
customFieldId: 'non-existent-id',
customFieldData: {
field: 'test_field',
label: 'Updated Label',
scope: 'visitor',
visibility: 'public',
},
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error');
});
});
it('should update an existing custom field with all optional fields', async () => {
await request
.post(api('livechat/custom-fields.save'))
.set(credentials)
.send({
customFieldId: existingField._id,
customFieldData: {
field: existingField._id,
label: 'Fully Updated Field',
scope: existingField.scope,
visibility: 'public',
type: 'select',
regexp: '^[0-9]+$',
required: true,
defaultValue: 'new_default',
options: 'opt1,opt2,opt3',
public: true,
searchable: false,
},
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body.customField).to.have.property('label', 'Fully Updated Field');
expect(res.body.customField).to.have.property('type', 'select');
expect(res.body.customField).to.have.property('regexp', '^[0-9]+$');
expect(res.body.customField).to.have.property('required', true);
expect(res.body.customField).to.have.property('defaultValue', 'new_default');
expect(res.body.customField).to.have.property('options', 'opt1,opt2,opt3');
expect(res.body.customField).to.have.property('public', true);
expect(res.body.customField).to.have.property('searchable', false);
});
});
});
});
});

View File

@@ -0,0 +1,140 @@
import { randomBytes } from 'crypto';
import type { ILoginCode, IUser } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import { MongoClient } from 'mongodb';
import { api, request, getCredentials } from '../../data/api-data';
import type { TestUser } from '../../data/users.helper';
import { createUser, deleteUser } from '../../data/users.helper';
import { URL_MONGODB } from '../../e2e/config/constants';
const CODE_TTL_MS = 60 * 1000;
async function insertLoginCode(connection: MongoClient, userId: string, insertExpiredToken: boolean = false): Promise<string> {
const now = new Date();
const code = randomBytes(32).toString('hex');
let expireAt = new Date(now.getTime() + CODE_TTL_MS);
if (insertExpiredToken) {
expireAt = new Date(now.getTime() - 1);
}
await connection.db().collection<ILoginCode>('rocketchat_login_codes').insertOne({
_id: code,
userId,
createdAt: now,
expireAt,
});
return code;
}
describe('[Login Codes]', () => {
let connection: MongoClient;
before((done) => getCredentials(done));
before(async () => {
connection = await MongoClient.connect(URL_MONGODB);
});
after(async () => {
await connection.close();
});
describe('POST [/loginCode.redeem]', () => {
let testUser: TestUser<IUser>;
before(async () => {
testUser = await createUser();
});
after(async () => {
await deleteUser(testUser);
});
it('should fail when code does not exist in the database', async () => {
await request
.post(api('loginCode.redeem'))
.send({ code: 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2' })
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
});
});
it('should fail when code is an empty string', async () => {
await request
.post(api('loginCode.redeem'))
.send({ code: '' })
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'invalid-params');
});
});
it('should return a loginToken and userId for a valid code', async () => {
const code = await insertLoginCode(connection, testUser._id);
await request
.post(api('loginCode.redeem'))
.send({ code })
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('loginToken').that.is.a('string').and.is.not.empty;
expect(res.body).to.have.property('userId', testUser._id);
});
});
it('should invalidate a code after it has been redeemed', async () => {
const code = await insertLoginCode(connection, testUser._id);
await request.post(api('loginCode.redeem')).send({ code }).expect(200);
await request
.post(api('loginCode.redeem'))
.send({ code })
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'error-invalid-code');
});
});
it('should return a loginToken that can be used for subsequent authenticated requests', async () => {
const code = await insertLoginCode(connection, testUser._id);
const redeemRes = await request.post(api('loginCode.redeem')).send({ code }).expect(200);
const { loginToken, userId } = redeemRes.body;
await request
.get(api('me'))
.set('X-Auth-Token', loginToken)
.set('X-User-Id', userId)
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('_id', testUser._id);
});
});
it('should fail when code is expired', async () => {
const code = await insertLoginCode(connection, testUser._id, true);
await request
.post(api('loginCode.redeem'))
.send({ code })
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'error-invalid-code');
});
});
});
});

View File

@@ -0,0 +1,114 @@
import type { Credentials } from '@rocket.chat/api-client';
import type { IUser } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import type { Response } from 'supertest';
import { getCredentials, api, request, credentials } from '../../data/api-data';
import { password } from '../../data/user';
import { createUser, deleteUser, login } from '../../data/users.helper';
describe('[Media Calls]', () => {
let user2: IUser;
let userCredentials: Credentials;
before((done) => getCredentials(done));
before(async () => {
user2 = await createUser();
userCredentials = await login(user2.username, password);
});
after(() => deleteUser(user2));
describe('[/media-calls.info]', () => {
it('should return valid internal call information', async () => {
await request
.get(api('media-calls.info'))
.set(credentials)
.query({
callId: 'rocketchat.internal.call.test',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('call').that.is.an('object');
const { call } = res.body;
expect(call).to.have.property('_id', 'rocketchat.internal.call.test');
expect(call).to.have.property('service', 'webrtc');
expect(call).to.have.property('kind', 'direct');
expect(call).to.have.property('state', 'hangup');
expect(call).to.have.property('ended', true);
expect(call).to.have.property('hangupReason', 'normal');
expect(call).to.have.property('createdBy').that.is.an('object');
expect(call.createdBy).to.have.property('type', 'user');
expect(call).to.have.property('caller').that.is.an('object');
expect(call.caller).to.have.property('type', 'user');
expect(call).to.have.property('callee').that.is.an('object');
expect(call.callee).to.have.property('type', 'user');
expect(call).to.have.property('endedBy').that.is.an('object');
expect(call.endedBy).to.have.property('type', 'user');
expect(call).to.have.property('uids').that.is.an('array');
expect(call.uids).to.have.lengthOf(2);
});
});
it('should return valid external call information', async () => {
await request
.get(api('media-calls.info'))
.set(credentials)
.query({
callId: 'rocketchat.external.call.test.outbound',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('call').that.is.an('object');
const { call } = res.body;
expect(call).to.have.property('_id', 'rocketchat.external.call.test.outbound');
expect(call).to.have.property('service', 'webrtc');
expect(call).to.have.property('kind', 'direct');
expect(call).to.have.property('state', 'hangup');
expect(call).to.have.property('ended', true);
expect(call).to.have.property('hangupReason', 'normal');
expect(call).to.have.property('createdBy').that.is.an('object');
expect(call).to.have.property('sipCallId', 'sipCallId3');
expect(call.createdBy).to.have.property('type', 'user');
expect(call).to.have.property('caller').that.is.an('object');
expect(call.caller).to.have.property('type', 'user');
expect(call).to.have.property('callee').that.is.an('object');
expect(call.callee).to.have.property('type', 'sip');
expect(call).to.have.property('endedBy').that.is.an('object');
expect(call.endedBy).to.have.property('type', 'user');
expect(call).to.have.property('uids').that.is.an('array');
expect(call.uids).to.have.lengthOf(1);
});
});
it('should not return invalid calls', async () => {
await request
.get(api('media-calls.info'))
.set(credentials)
.query({
callId: 'invalid.call',
})
.expect('Content-Type', 'application/json')
.expect(404);
});
it('should not return calls from other users', async () => {
await request
.get(api('media-calls.info'))
.set(userCredentials)
.query({
callId: 'rocketchat.internal.call.test',
})
.expect('Content-Type', 'application/json')
.expect(404);
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,162 @@
import type { IUser } from '@rocket.chat/core-typings';
import { Random } from '@rocket.chat/random';
import { expect } from 'chai';
import { before, describe, it, after } from 'mocha';
import speakeasy from 'speakeasy';
import { getCredentials, methodCall, request } from '../../../data/api-data';
import { password } from '../../../data/user';
import { createUser, deleteUser, login } from '../../../data/users.helper';
describe('2fa:enable', function () {
this.retries(0);
let totpSecret: string;
let user1: IUser;
let user2: IUser;
let user3: IUser;
let user1Credentials: { 'X-Auth-Token': string; 'X-User-Id': string };
let user2Credentials: { 'X-Auth-Token': string; 'X-User-Id': string };
let user3Credentials: { 'X-Auth-Token': string; 'X-User-Id': string };
before((done) => getCredentials(done));
before('create user', async () => {
[user1, user2, user3] = await Promise.all([
createUser({ username: Random.id(), email: `${Random.id()}@example.com`, verified: true }),
createUser({ username: Random.id(), email: `${Random.id()}@example.com}`, verified: true }),
createUser({ username: Random.id(), email: `${Random.id()}@example.com}`, verified: false }),
]);
[user1Credentials, user2Credentials, user3Credentials] = await Promise.all([
login(user1.username, password),
login(user2.username, password),
login(user3.username, password),
]);
});
after('remove user', async () => Promise.all([deleteUser(user1), deleteUser(user2), deleteUser(user3)]));
it('should return error when user is not logged in', async () => {
await request
.post(methodCall('2fa:enable'))
.send({
message: JSON.stringify({
msg: 'method',
id: 'id1',
method: '2fa:enable',
params: [],
}),
})
.expect(401)
.expect((res) => {
expect(res.body).to.have.property('status', 'error');
});
});
it('should return secret and qr code url even when user has unverified email', async () => {
await request
.post(methodCall('2fa:enable'))
.set(user3Credentials)
.send({
message: JSON.stringify({
msg: 'method',
id: 'id1',
method: '2fa:enable',
params: [],
}),
})
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
const parsedBody = JSON.parse(res.body.message);
expect(parsedBody).to.have.property('result');
expect(parsedBody.result).to.have.property('secret').of.a('string');
expect(parsedBody.result)
.to.have.property('url')
.of.a('string')
.match(/^otpauth:\/\//);
});
});
it('should return secret and qr code url when 2fa is disabled on user', async () => {
await request
.post(methodCall('2fa:enable'))
.set(user1Credentials)
.send({
message: JSON.stringify({
msg: 'method',
id: 'id1',
method: '2fa:enable',
params: [],
}),
})
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
const parsedBody = JSON.parse(res.body.message);
expect(parsedBody).to.have.property('result');
expect(parsedBody.result).to.have.property('secret').of.a('string');
expect(parsedBody.result)
.to.have.property('url')
.of.a('string')
.match(/^otpauth:\/\//);
});
});
it('should enable 2fa on the user', async () => {
const enableResponse = await request
.post(methodCall('2fa:enable'))
.set(user2Credentials)
.send({
message: JSON.stringify({
msg: 'method',
id: 'id2',
method: '2fa:enable',
params: [],
}),
})
.expect(200);
const enableData = JSON.parse(enableResponse.body.message);
totpSecret = enableData.result.secret;
await request
.post(methodCall('2fa:validateTempToken'))
.set(user2Credentials)
.send({
message: JSON.stringify({
msg: 'method',
id: 'id3',
method: '2fa:validateTempToken',
params: [speakeasy.totp({ secret: totpSecret, encoding: 'base32' })],
}),
})
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
const parsedBody = JSON.parse(res.body.message);
expect(parsedBody).to.have.property('result');
expect(parsedBody.result).to.have.property('codes').of.a('array');
});
});
it('should return error when 2fa is already enabled on the user', async () => {
await request
.post(methodCall('2fa:enable'))
.set(user2Credentials)
.send({
message: JSON.stringify({
msg: 'method',
id: 'id4',
method: '2fa:enable',
params: [],
}),
})
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
const parsedBody = JSON.parse(res.body.message);
expect(parsedBody).to.have.property('error');
expect(parsedBody).to.not.have.property('result');
});
});
});

View File

@@ -0,0 +1,795 @@
import type { Credentials } from '@rocket.chat/api-client';
import type { IInstanceStatus, IRoom, ITeam, IUser } from '@rocket.chat/core-typings';
import { TeamType } from '@rocket.chat/core-typings';
import type { IInstance } from '@rocket.chat/rest-typings';
import { AssertionError, expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import { MongoClient } from 'mongodb';
import { getCredentials, api, request, credentials } from '../../data/api-data';
import { updatePermission, updateSetting } from '../../data/permissions.helper';
import { createRoom, deleteRoom } from '../../data/rooms.helper';
import { createTeam, deleteTeam } from '../../data/teams.helper';
import { adminEmail, adminUsername, adminPassword, password } from '../../data/user';
import type { TestUser } from '../../data/users.helper';
import { createUser, deleteUser, login as doLogin } from '../../data/users.helper';
import { IS_EE, URL_MONGODB } from '../../e2e/config/constants';
describe('miscellaneous', () => {
before((done) => getCredentials(done));
describe('API default', () => {
// Required by mobile apps
describe('/info', () => {
let version: string;
it('should return "version", "build", "commit" and "marketplaceApiVersion" when the user is logged in', async () => {
const res = await request.get('/api/info').set(credentials).expect('Content-Type', 'application/json').expect(200);
expect(res.body).to.have.property('version').and.to.be.a('string');
expect(res.body.info).to.have.property('version').and.to.be.a('string');
expect(res.body.info).to.have.property('build').and.to.be.an('object');
expect(res.body.info).to.have.property('commit').and.to.be.an('object');
expect(res.body.info).to.have.property('marketplaceApiVersion').and.to.be.a('string');
version = res.body.info.version;
});
it('should return only "version" and the version should not have patch info when the user is not logged in', async () => {
const res = await request.get('/api/info').expect('Content-Type', 'application/json').expect(200);
expect(res.body).to.have.property('version');
expect(res.body).to.not.have.property('info');
expect(res.body.version).to.be.equal(version.replace(/(\d+\.\d+).*/, '$1'));
});
});
});
it('/login', () => {
expect(credentials).to.have.property('X-Auth-Token').with.lengthOf.at.least(1);
expect(credentials).to.have.property('X-User-Id').with.lengthOf.at.least(1);
});
it('/login (wrapper username)', async () => {
const res = await request
.post(api('login'))
.send({
user: {
username: adminUsername,
},
password: adminPassword,
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(res.body).to.have.property('status', 'success');
expect(res.body).to.have.property('data').and.to.be.an('object');
expect(res.body.data).to.have.property('userId');
expect(res.body.data).to.have.property('authToken');
expect(res.body.data).to.have.property('me');
});
it('/login (wrapper email)', async () => {
const res = await request
.post(api('login'))
.send({
user: {
email: adminEmail,
},
password: adminPassword,
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(res.body).to.have.property('status', 'success');
expect(res.body).to.have.property('data').and.to.be.an('object');
expect(res.body.data).to.have.property('userId');
expect(res.body.data).to.have.property('authToken');
expect(res.body.data).to.have.property('me');
});
it('/login by user', async () => {
const res = await request
.post(api('login'))
.send({
user: adminEmail,
password: adminPassword,
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(res.body).to.have.property('status', 'success');
expect(res.body).to.have.property('data').and.to.be.an('object');
expect(res.body.data).to.have.property('userId');
expect(res.body.data).to.have.property('authToken');
expect(res.body.data).to.have.property('me');
expect(res.body.data.me.services).to.not.have.nested.property('password.bcrypt');
expect(res.body.data.me.services).to.have.nested.property('password.exists', true);
});
it('/login by username', async () => {
const res = await request
.post(api('login'))
.send({
username: adminUsername,
password: adminPassword,
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(res.body).to.have.property('status', 'success');
expect(res.body).to.have.property('data').and.to.be.an('object');
expect(res.body.data).to.have.property('userId');
expect(res.body.data).to.have.property('authToken');
expect(res.body.data).to.have.property('me');
expect(res.body.data.me.services).to.not.have.nested.property('password.bcrypt');
expect(res.body.data.me.services).to.have.nested.property('password.exists', true);
});
it('/me', async () => {
const user = await createUser();
const userCredentials = await doLogin(user.username, password);
await request
.get(api('me'))
.set(userCredentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
const allUserPreferencesKeys = [
'alsoSendThreadToChannel',
// 'language',
'newRoomNotification',
'newMessageNotification',
'showThreadsInMainChannel',
// 'clockMode',
'useEmojis',
'convertAsciiEmoji',
'saveMobileBandwidth',
'collapseMediaByDefault',
'autoImageLoad',
'emailNotificationMode',
'unreadAlert',
'masterVolume',
'notificationsSoundVolume',
'voipRingerVolume',
'omnichannelTranscriptEmail',
IS_EE ? 'omnichannelTranscriptPDF' : false,
'desktopNotifications',
'pushNotifications',
'enableAutoAway',
// 'highlights',
'desktopNotificationRequireInteraction',
'hideUsernames',
'hideRoles',
'displayAvatars',
'hideFlexTab',
'sendOnEnter',
'idleTimeLimit',
'sidebarShowFavorites',
'sidebarShowUnread',
'themeAppearence',
'sidebarSortby',
'sidebarViewMode',
'sidebarDisplayAvatar',
'sidebarGroupByType',
'sidebarSectionsOrder',
'muteFocusedConversations',
'notifyCalendarEvents',
'enableMobileRinging',
'featuresPreview',
'desktopNotificationVoiceCalls',
].filter((p) => Boolean(p));
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('_id', user._id);
expect(res.body).to.have.property('username', user.username);
expect(res.body).to.have.property('active');
expect(res.body).to.have.property('name');
expect(res.body).to.have.property('roles').and.to.be.an('array');
expect(res.body).to.have.nested.property('emails[0].address', user.emails[0].address);
expect(res.body).to.have.nested.property('settings.preferences').and.to.be.an('object');
expect(res.body.settings.preferences).to.have.all.keys(allUserPreferencesKeys);
expect(res.body).to.have.property('isOAuthUser', false);
expect(res.body.services).to.not.have.nested.property('password.bcrypt');
expect(res.body.services).to.have.nested.property('password.exists', true);
});
await deleteUser(user);
});
describe('/directory', () => {
let user: TestUser<IUser>;
let userWithLeftoverFederationData: TestUser<IUser>;
let testChannel: IRoom;
let testGroup: IRoom;
let normalUserCredentials: Credentials;
const teamName = `new-team-name-${Date.now()}` as const;
let teamCreated: ITeam;
before(async () => {
await updatePermission('create-team', ['admin', 'user']);
user = await createUser();
userWithLeftoverFederationData = await createUser();
normalUserCredentials = await doLogin(user.username, password);
[testChannel, testGroup, teamCreated] = await Promise.all([
createRoom({ name: `channel.test.${Date.now()}`, type: 'c' }).then((res) => res.body.channel),
createRoom({ name: `group.test.${Date.now()}`, type: 'p' }).then((res) => res.body.group),
createTeam(normalUserCredentials, teamName, TeamType.PUBLIC),
]);
const connection = await MongoClient.connect(URL_MONGODB);
const updateResult = await connection
.db()
.collection('users')
.updateOne({ _id: userWithLeftoverFederationData._id as any }, { $set: { federation: { origin: 'example.com' } } });
await connection.close();
expect(updateResult.modifiedCount).to.equal(1);
});
after(async () => {
await Promise.all([
deleteTeam(normalUserCredentials, teamName),
deleteUser(user),
deleteUser(userWithLeftoverFederationData),
deleteRoom({ type: 'c', roomId: testChannel._id }),
deleteRoom({ type: 'p', roomId: testGroup._id }),
updatePermission('create-team', ['admin', 'user']),
]);
});
it('should return an array(result) when search by user and execute successfully', async () => {
const res = await request
.get(api('directory'))
.set(credentials)
.query({
text: user.username,
type: 'users',
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('result').and.to.be.an('array');
expect(res.body).to.have.property('offset');
expect(res.body).to.have.property('total');
expect(res.body).to.have.property('count');
expect(res.body.result[0]).to.have.property('_id');
expect(res.body.result[0]).to.have.property('createdAt');
expect(res.body.result[0]).to.have.property('username');
expect(res.body.result[0]).to.have.property('emails').and.to.be.an('array');
expect(res.body.result[0]).to.have.property('name');
});
it('should not return the emails field for non admins', async () => {
const res = await request
.get(api('directory'))
.set(normalUserCredentials)
.query({
text: user.username,
type: 'users',
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('result').and.to.be.an('array');
expect(res.body).to.have.property('offset');
expect(res.body).to.have.property('total');
expect(res.body).to.have.property('count');
expect(res.body.result[0]).to.have.property('_id');
expect(res.body.result[0]).to.have.property('createdAt');
expect(res.body.result[0]).to.have.property('username');
expect(res.body.result[0]).to.not.have.property('emails');
expect(res.body.result[0]).to.have.property('name');
});
it('should find local users that still have leftover federation data', async () => {
const res = await request
.get(api('directory'))
.set(credentials)
.query({
text: userWithLeftoverFederationData.username,
type: 'users',
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body.result).to.have.lengthOf(1);
expect(res.body.result[0]).to.have.property('_id', userWithLeftoverFederationData._id);
});
it('should return an array(result) when search by channel and execute successfully', async () => {
const res = await request
.get(api('directory'))
.set(credentials)
.query({
text: testChannel.name,
type: 'channels',
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('offset');
expect(res.body).to.have.property('total');
expect(res.body).to.have.property('count');
expect(res.body).to.have.property('result').and.to.be.an('array');
expect(res.body.result[0]).to.have.property('_id');
expect(res.body.result[0]).to.have.property('name');
expect(res.body.result[0]).to.have.property('usersCount').and.to.be.an('number');
expect(res.body.result[0]).to.have.property('ts');
});
it('should return private group when search by channel and execute successfully', async () => {
await request
.get(api('directory'))
.set(credentials)
.query({
text: testGroup.name,
type: 'channels',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('offset');
expect(res.body).to.have.property('total');
expect(res.body).to.have.property('count');
expect(res.body).to.have.property('result').and.to.be.an('array');
expect(res.body.result[0]).to.have.property('_id', testGroup._id);
expect(res.body.result[0]).to.have.property('t', 'p');
expect(res.body.result[0]).to.have.property('name');
expect(res.body.result[0]).to.have.property('usersCount').and.to.be.an('number');
expect(res.body.result[0]).to.have.property('ts');
});
});
it('should return an array(result) when search by channel with sort params correctly and execute successfully', async () => {
const res = await request
.get(api('directory'))
.set(credentials)
.query({
text: testChannel.name,
type: 'channels',
sort: JSON.stringify({
name: 1,
}),
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('offset');
expect(res.body).to.have.property('total');
expect(res.body).to.have.property('count');
expect(res.body).to.have.property('result').and.to.be.an('array');
expect(res.body.result[0]).to.have.property('_id');
expect(res.body.result[0]).to.have.property('name');
expect(res.body.result[0]).to.have.property('usersCount').and.to.be.an('number');
expect(res.body.result[0]).to.have.property('ts');
});
it('should return an error when send invalid query', async () => {
const res = await request
.get(api('directory'))
.set(credentials)
.query({
text: 'invalid channel',
type: 'invalid',
})
.expect('Content-Type', 'application/json')
.expect(400);
expect(res.body).to.have.property('success', false);
});
it('should return an error when have more than one sort parameter', async () => {
const res = await request
.get(api('directory'))
.set(credentials)
.query({
text: testChannel.name,
type: 'channels',
sort: JSON.stringify({
name: 1,
test: 1,
}),
})
.expect('Content-Type', 'application/json')
.expect(400);
expect(res.body).to.have.property('success', false);
});
it('should return an object containing rooms and totalCount from teams', async () => {
const res = await request
.get(api('directory'))
.set(normalUserCredentials)
.query({
text: '',
type: 'teams',
sort: JSON.stringify({
name: 1,
}),
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(res.body).to.have.property('result');
expect(res.body.result).to.be.an(`array`);
expect(res.body).to.have.property('total');
expect(res.body.total).to.be.an('number');
expect(res.body.result[0]).to.have.property('_id', teamCreated.roomId);
expect(res.body.result[0]).to.have.property('fname');
expect(res.body.result[0]).to.have.property('teamMain');
expect(res.body.result[0]).to.have.property('name');
expect(res.body.result[0]).to.have.property('t');
expect(res.body.result[0]).to.have.property('usersCount');
expect(res.body.result[0]).to.have.property('ts');
expect(res.body.result[0]).to.have.property('teamId');
expect(res.body.result[0]).to.have.property('default');
expect(res.body.result[0]).to.have.property('roomsCount');
});
});
describe('[/spotlight]', () => {
let user: TestUser<IUser>;
let userCredentials: Credentials;
let testChannel: IRoom;
let testTeam: ITeam;
let testChannelSpecialChars: IRoom;
const fnameSpecialCharsRoom = `test ГДΕληνικά-${Date.now()}`;
const teamName = `team-test-${Date.now()}`;
before(async () => {
user = await createUser();
userCredentials = await doLogin(user.username, password);
await updateSetting('UI_Allow_room_names_with_special_chars', true);
testChannelSpecialChars = (await createRoom({ type: 'c', name: fnameSpecialCharsRoom, credentials: userCredentials })).body.channel;
testChannel = (await createRoom({ type: 'c', name: `channel.test.${Date.now()}`, credentials: userCredentials })).body.channel;
testTeam = await createTeam(userCredentials, teamName, TeamType.PUBLIC);
});
after(async () => {
await Promise.all([
deleteUser(user),
updateSetting('UI_Allow_room_names_with_special_chars', false),
deleteTeam(userCredentials, teamName),
]);
});
it('should fail when does not have query param', async () => {
const res = await request.get(api('spotlight')).set(credentials).expect('Content-Type', 'application/json').expect(400);
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error');
});
it('should return object inside users array when search by a valid user', async () => {
const res = await request
.get(api('spotlight'))
.query({
query: `@${adminUsername}`,
})
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('users').and.to.be.an('array');
expect(res.body.users[0]).to.have.property('_id');
expect(res.body.users[0]).to.have.property('name');
expect(res.body.users[0]).to.have.property('username');
expect(res.body.users[0]).to.have.property('status');
expect(res.body).to.have.property('rooms').and.to.be.an('array');
});
it('must return the object inside the room array when searching for a valid room and that user is not a member of it', async () => {
const res = await request
.get(api('spotlight'))
.query({
query: `#${testChannel.name}`,
})
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('users').and.to.be.an('array');
expect(res.body).to.have.property('rooms').and.to.be.an('array');
expect(res.body.rooms[0]).to.have.property('_id');
expect(res.body.rooms[0]).to.have.property('name');
expect(res.body.rooms[0]).to.have.property('t');
});
it('must return the teamMain property when searching for a valid team that the user is not a member of', async () => {
const res = await request
.get(api('spotlight'))
.query({
query: `${testTeam.name}`,
})
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('users').and.to.be.an('array');
expect(res.body).to.have.property('rooms').and.to.be.an('array');
expect(res.body.rooms[0]).to.have.property('_id');
expect(res.body.rooms[0]).to.have.property('name');
expect(res.body.rooms[0]).to.have.property('t');
expect(res.body.rooms[0]).to.have.property('teamMain');
});
it('must return rooms when searching for a valid fname', async () => {
const res = await request
.get(api('spotlight'))
.query({
query: `#${fnameSpecialCharsRoom}`,
})
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('users').and.to.be.an('array');
expect(res.body).to.have.property('rooms').and.to.be.an('array');
expect(res.body.rooms[0]).to.have.property('_id', testChannelSpecialChars._id);
expect(res.body.rooms[0]).to.have.property('name', testChannelSpecialChars.name);
expect(res.body.rooms[0]).to.have.property('t', testChannelSpecialChars.t);
});
it('should not return users when the type param disables user search', async () => {
const res = await request
.get(api('spotlight'))
.query({
query: `${adminUsername}`,
type: JSON.stringify({ users: false, rooms: true }),
})
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('users').and.to.be.an('array').that.is.empty;
expect(res.body).to.have.property('rooms').and.to.be.an('array');
});
it('should exclude usernames passed in the usernames param from the results', async () => {
const res = await request
.get(api('spotlight'))
.query({
// Use a non-exact (prefix) query so the regex search path runs; the exact-username
// match branch in Spotlight.searchUsers does not honor the usernames exclusion list.
query: adminUsername.slice(0, -2),
usernames: adminUsername,
})
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('users').and.to.be.an('array');
expect(res.body.users.map((u: { username: string }) => u.username)).to.not.include(adminUsername);
});
describe('anonymous (unauthenticated) requests', () => {
after(() => updateSetting('Accounts_AllowAnonymousRead', false));
it('should return no rooms when anonymous read is disabled', async () => {
await updateSetting('Accounts_AllowAnonymousRead', false);
const res = await request
.get(api('spotlight'))
.query({
query: `#${testChannel.name}`,
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('rooms').and.to.be.an('array').that.is.empty;
expect(res.body).to.have.property('users').and.to.be.an('array').that.is.empty;
});
// An unprefixed query keeps `type.users` enabled, so it also runs the user search with no
// user id - the code path that used to throw before the anonymous callers were guarded.
it('should return no rooms or users for an unprefixed query when anonymous read is disabled', async () => {
await updateSetting('Accounts_AllowAnonymousRead', false);
const res = await request
.get(api('spotlight'))
.query({
query: testChannel.name,
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('rooms').and.to.be.an('array').that.is.empty;
expect(res.body).to.have.property('users').and.to.be.an('array').that.is.empty;
});
it('should return public rooms but no users when anonymous read is enabled', async () => {
await updateSetting('Accounts_AllowAnonymousRead', true);
const res = await request
.get(api('spotlight'))
.query({
query: `#${testChannel.name}`,
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body.rooms.map((r: { _id: string }) => r._id)).to.include(testChannel._id);
expect(res.body).to.have.property('users').and.to.be.an('array').that.is.empty;
});
it('should return public rooms but no users for an unprefixed query when anonymous read is enabled', async () => {
await updateSetting('Accounts_AllowAnonymousRead', true);
const res = await request
.get(api('spotlight'))
.query({
query: testChannel.name,
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body.rooms.map((r: { _id: string }) => r._id)).to.include(testChannel._id);
expect(res.body).to.have.property('users').and.to.be.an('array').that.is.empty;
});
});
});
describe('[/instances.get]', () => {
let unauthorizedUserCredentials: Credentials;
before(async () => {
const createdUser = await createUser();
unauthorizedUserCredentials = await doLogin(createdUser.username, password);
});
it('should fail if user is logged in but is unauthorized', async () => {
const res = await request
.get(api('instances.get'))
.set(unauthorizedUserCredentials)
.expect('Content-Type', 'application/json')
.expect(403);
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'User does not have the permissions required for this action [error-unauthorized]');
});
it('should fail if not logged in', async () => {
const res = await request.get(api('instances.get')).expect('Content-Type', 'application/json').expect(401);
expect(res.body).to.have.property('status', 'error');
expect(res.body).to.have.property('message');
});
it('should return instances if user is logged in and is authorized', async () => {
const res = await request.get(api('instances.get')).set(credentials).expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('instances').and.to.be.an('array').with.lengthOf(1);
const instances = res.body.instances as IInstance[];
const instanceName = IS_EE ? 'ddp-streamer' : 'rocket.chat';
const instance = instances.find((i): i is IInstance & { instanceRecord: IInstanceStatus } => i.instanceRecord?.name === instanceName);
if (!instance) throw new AssertionError(`no instance named "${instanceName}"`);
expect(instance).to.have.property('instanceRecord');
expect(instance).to.have.property('currentStatus');
expect(instance.currentStatus).to.have.property('connected');
expect(instance.instanceRecord).to.have.property('_id');
expect(instance.instanceRecord).to.have.property('extraInformation');
expect(instance.instanceRecord).to.have.property('name');
expect(instance.instanceRecord).to.have.property('pid');
if (!IS_EE) {
expect(instance).to.have.property('address');
expect(instance.currentStatus).to.have.property('lastHeartbeatTime');
expect(instance.currentStatus).to.have.property('local');
const { extraInformation } = instance.instanceRecord;
expect(extraInformation).to.have.property('host');
expect(extraInformation).to.have.property('port');
expect(extraInformation).to.have.property('os').and.to.have.property('cpus').to.be.a('number');
expect(extraInformation).to.have.property('nodeVersion');
}
});
});
describe('[/shield.svg]', () => {
before(() => updateSetting('API_Enable_Shields', false));
after(() => updateSetting('API_Enable_Shields', true));
it('should fail if API_Enable_Shields is disabled', async () => {
const res = await request
.get(api('shield.svg'))
.query({
type: 'online',
icon: true,
channel: 'general',
name: 'Rocket.Chat',
})
.expect('Content-Type', 'application/json')
.expect(400);
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'error-endpoint-disabled');
});
it('should succeed if API_Enable_Shields is enabled', async () => {
await updateSetting('API_Enable_Shields', true);
await request
.get(api('shield.svg'))
.query({
type: 'online',
icon: true,
channel: 'general',
name: 'Rocket.Chat',
})
.expect('Content-Type', 'image/svg+xml;charset=utf-8')
.expect(200);
});
});
describe('/pw.getPolicy', () => {
it('should return policies', async () => {
const res = await request.get(api('pw.getPolicy')).set(credentials).expect('Content-Type', 'application/json').expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('enabled');
expect(res.body).to.have.property('policy').and.to.be.an('array');
});
});
describe('/fingerprint', () => {
let unauthorizedUser: TestUser<IUser>;
let unauthorizedUserCredentials: Credentials;
before(async () => {
unauthorizedUser = await createUser();
unauthorizedUserCredentials = await doLogin(unauthorizedUser.username, password);
});
after(async () => {
await deleteUser(unauthorizedUser);
});
it('should return 401 when called without authentication', async () => {
const res = await request.post(api('fingerprint')).send({ setDeploymentAs: 'updated-configuration' });
expect(res.status).to.equal(401);
expect(res.body).to.have.property('status', 'error');
});
it('should return 403 when a user without the manage-cloud permission tries to acknowledge a deployment configuration change', async () => {
const res = await request
.post(api('fingerprint'))
.set(unauthorizedUserCredentials)
.send({ setDeploymentAs: 'updated-configuration' });
expect(res.status).to.equal(403);
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'User does not have the permissions required for this action [error-unauthorized]');
});
it('should return 403 when a user without the manage-cloud permission tries to deregister the workspace as a new workspace', async () => {
const res = await request.post(api('fingerprint')).set(unauthorizedUserCredentials).send({ setDeploymentAs: 'new-workspace' });
expect(res.status).to.equal(403);
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'User does not have the permissions required for this action [error-unauthorized]');
});
it('should return 200 when a user with the manage-cloud permission acknowledges a deployment configuration change', async () => {
const res = await request.post(api('fingerprint')).set(credentials).send({ setDeploymentAs: 'updated-configuration' });
expect(res.status).to.equal(200);
expect(res.body).to.have.property('success', true);
});
});
});

View File

@@ -0,0 +1,814 @@
import type { IMessage, IModerationAudit, IModerationReport, IUser } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import type { Response } from 'supertest';
import { getCredentials, api, request, credentials } from '../../data/api-data';
import { createUser, deleteUser } from '../../data/users.helper';
const makeModerationApiRequest = async (
url: 'moderation.reportUser' | 'moderation.user.reportsByUserId',
method: 'get' | 'post',
data?: any,
) => {
let res: any;
if (method === 'get') {
res = await request.get(api(url)).set(credentials).query(data);
} else if (method === 'post') {
res = await request.post(api(url)).set(credentials).send(data);
}
return res.body;
};
const reportUser = (userId: string, reason: string) => makeModerationApiRequest('moderation.reportUser', 'post', { userId, reason });
const getUsersReports = (userId: string) => makeModerationApiRequest('moderation.user.reportsByUserId', 'get', { userId });
describe('[Moderation]', () => {
before((done) => getCredentials(done));
describe('[/moderation.reportsByUsers]', () => {
it('should return an array of reports', async () => {
await request
.get(api('moderation.reportsByUsers'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('reports').and.to.be.an('array');
});
});
it('should return an array of reports even requested with count and offset params', async () => {
await request
.get(api('moderation.reportsByUsers'))
.set(credentials)
.query({
count: 5,
offset: 0,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('reports').and.to.be.an('array');
});
});
it('should return an array of reports even requested with oldest param', async () => {
await request
.get(api('moderation.reportsByUsers'))
.set(credentials)
.query({
oldest: new Date(),
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('reports').and.to.be.an('array');
});
});
it('should return an array of reports even requested with latest param', async () => {
await request
.get(api('moderation.reportsByUsers'))
.set(credentials)
.query({
latest: new Date(),
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('reports').and.to.be.an('array');
});
});
});
describe('[/moderation.userReports]', () => {
it('should return an array of reports', async () => {
await request
.get(api('moderation.userReports'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('reports').and.to.be.an('array');
});
});
it('should return an array of reports even requested with count and offset params', async () => {
await request
.get(api('moderation.userReports'))
.set(credentials)
.query({
count: 5,
offset: 0,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('reports').and.to.be.an('array');
});
});
it('should return an array of reports even requested with oldest param', async () => {
await request
.get(api('moderation.userReports'))
.set(credentials)
.query({
oldest: new Date(),
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('reports').and.to.be.an('array');
});
});
it('should return an array of reports even requested with latest param', async () => {
await request
.get(api('moderation.userReports'))
.set(credentials)
.query({
latest: new Date(),
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('reports').and.to.be.an('array');
});
});
});
// test for testing out the moderation.dismissReports endpoint
describe('[/moderation.dismissReports]', () => {
let reportedMessage: IModerationAudit;
let message: IMessage;
// post a new message to the channel 'general' by sending a request to chat.postMessage
before(async () => {
await request
.post(api('chat.sendMessage'))
.set(credentials)
.send({
message: {
rid: 'GENERAL',
msg: 'Sample message 0',
},
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
message = res.body.message;
});
});
beforeEach(async () => {
await request
.post(api('chat.reportMessage'))
.set(credentials)
.send({
messageId: message._id,
description: 'sample report',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
});
});
beforeEach(async () => {
await request
.get(api('moderation.reportsByUsers'))
.set(credentials)
.query({
count: 5,
offset: 0,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('reports').and.to.be.an('array');
reportedMessage = res.body.reports[0];
});
});
after(async () => {
await request
.post(api('chat.delete'))
.set(credentials)
.send({
roomId: 'GENERAL',
msgId: message._id,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
});
});
it('should hide reports of a user', async () => {
await request
.post(api('moderation.dismissReports'))
.set(credentials)
.send({
userId: reportedMessage.userId,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
});
});
it('should hide reports of a message', async () => {
await request
.post(api('moderation.dismissReports'))
.set(credentials)
.send({
msgId: message._id,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
});
});
it('should return an error when the userId && msgId is not provided', async () => {
await request
.post(api('moderation.dismissReports'))
.set(credentials)
.send({
userId: '',
msgId: '',
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error').and.to.be.a('string');
});
});
});
describe('[/moderation.user.reportsByUserId]', () => {
let reportedUser: IUser;
before(async () => {
reportedUser = await createUser();
await reportUser(reportedUser._id, 'sample report');
});
after(async () => {
await deleteUser(reportedUser);
});
it('should return an array of reports', async () => {
await request
.get(api('moderation.user.reportsByUserId'))
.set(credentials)
.query({
userId: reportedUser._id,
count: 5,
offset: 0,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect(async (res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('reports').and.to.be.an('array').and.to.have.lengthOf(1);
});
});
it('should return an error when the userId is not provided', async () => {
await request
.get(api('moderation.user.reportsByUserId'))
.set(credentials)
.query({
userId: '',
count: 5,
offset: 0,
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect(async (res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error');
expect(res.body).to.have.property('errorType', 'invalid-params');
});
});
});
describe('[/moderation.dismissUserReports', () => {
let reportedUser: IUser;
before(async () => {
reportedUser = await createUser();
await reportUser(reportedUser._id, 'sample report');
});
after(async () => {
await deleteUser(reportedUser);
});
it('should hide reports of a user', async () => {
await request
.post(api('moderation.dismissUserReports'))
.set(credentials)
.send({
userId: reportedUser._id,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
});
await getUsersReports(reportedUser._id).then((res) => {
expect(res.reports).to.have.lengthOf(0);
});
});
it('should return an error when the userId is not provided', async () => {
await request
.post(api('moderation.dismissUserReports'))
.set(credentials)
.send({
userId: '',
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error').and.to.be.a('string');
});
});
});
// test for testing out the moderation.reports endpoint
describe('[/moderation.reports]', () => {
let message: IMessage;
// post a new message to the channel 'general' by sending a request to chat.postMessage
before(async () => {
await request
.post(api('chat.postMessage'))
.set(credentials)
.send({
channel: 'general',
text: 'messageId',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('message').and.to.be.an('object');
message = res.body.message;
});
});
// create a reported message by sending a request to chat.reportMessage
before(async () => {
await request
.post(api('chat.reportMessage'))
.set(credentials)
.send({
messageId: message._id,
description: 'sample report',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
});
});
after(async () => {
await request
.post(api('chat.delete'))
.set(credentials)
.send({
roomId: 'GENERAL',
msgId: message._id,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
});
});
it('should return the reports for a message', async () => {
await request
.get(api('moderation.reports'))
.set(credentials)
.query({
msgId: message._id,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('reports').and.to.be.an('array');
});
});
it('should return an error when the msgId is not provided', async () => {
await request
.get(api('moderation.reports'))
.set(credentials)
.query({
msgId: '',
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error').and.to.be.a('string');
});
});
});
// test for testing out the moderation.reportInfo endpoint
describe('[/moderation.reportInfo]', () => {
let message: IMessage;
let reportedMessage: IModerationReport;
// post a new message to the channel 'general' by sending a request to chat.postMessage
before(async () => {
await request
.post(api('chat.postMessage'))
.set(credentials)
.send({
channel: 'general',
text: 'messageId',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('message').and.to.be.an('object');
message = res.body.message;
});
});
// create a reported message by sending a request to chat.reportMessage
before(async () => {
await request
.post(api('chat.reportMessage'))
.set(credentials)
.send({
messageId: message._id,
description: 'sample report',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
});
});
// get the report information by sending a request to moderation.reports
before(async () => {
await request
.get(api('moderation.reports'))
.set(credentials)
.query({
msgId: message._id,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('reports').and.to.be.an('array');
reportedMessage = res.body.reports[0];
});
});
after(async () => {
await request
.post(api('chat.delete'))
.set(credentials)
.send({
roomId: 'GENERAL',
msgId: message._id,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
});
});
it('should return the report information', async () => {
await request
.get(api('moderation.reportInfo'))
.set(credentials)
.query({
reportId: reportedMessage._id,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('report').and.to.be.an('object');
expect(res.body.report).to.have.property('_id', reportedMessage._id);
});
});
it('should return an error when the reportId is not provided', async () => {
await request
.get(api('moderation.reportInfo'))
.set(credentials)
.query({
reportId: '',
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error').and.to.be.a('string');
});
});
it('should return an error when the reportId is invalid', async () => {
await request
.get(api('moderation.reportInfo'))
.set(credentials)
.query({
reportId: 'invalid',
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error').and.to.be.a('string');
});
});
it('should return an error when the reportId is not found', async () => {
await request
.get(api('moderation.reportInfo'))
.set(credentials)
.query({
reportId: '123456789012345678901234',
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error').and.to.be.a('string');
});
});
});
// test for testing out the moderation.user.reportedMessages endpoint
describe('[/moderation.user.reportedMessages]', () => {
let message: IMessage;
let reportedMessage: IModerationReport;
// post a new message to the channel 'general' by sending a request to chat.postMessage
before(async () => {
await request
.post(api('chat.postMessage'))
.set(credentials)
.send({
channel: 'general',
text: 'messageId',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('message').and.to.be.an('object');
message = res.body.message;
});
});
// create a reported message by sending a request to chat.reportMessage
before(async () => {
await request
.post(api('chat.reportMessage'))
.set(credentials)
.send({
messageId: message._id,
description: 'sample report',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
});
});
// get the report information by sending a request to moderation.reports
before(async () => {
await request
.get(api('moderation.reports'))
.set(credentials)
.query({
msgId: message._id,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('reports').and.to.be.an('array');
reportedMessage = res.body.reports[0];
});
});
after(async () => {
await request
.post(api('chat.delete'))
.set(credentials)
.send({
roomId: 'GENERAL',
msgId: message._id,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
});
});
it('should return the message history', async () => {
await request
.get(api('moderation.user.reportedMessages'))
.set(credentials)
.query({
userId: reportedMessage.reportedBy._id,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('messages').and.to.be.an('array');
});
});
it('should return an error when the userId is not provided', async () => {
await request
.get(api('moderation.user.reportedMessages'))
.set(credentials)
.query({
userId: '',
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error').and.to.be.a('string');
});
});
});
// test for testing out the moderation.user.deleteReportedMessages endpoint
describe('[/moderation.user.deleteReportedMessages]', () => {
let message: IMessage;
let reportedMessage: IModerationReport;
// post a new message to the channel 'general' by sending a request to chat.postMessage
before(async () => {
await request
.post(api('chat.postMessage'))
.set(credentials)
.send({
channel: 'general',
text: 'messageId',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('message').and.to.be.an('object');
message = res.body.message;
});
});
// create a reported message by sending a request to chat.reportMessage
before(async () => {
await request
.post(api('chat.reportMessage'))
.set(credentials)
.send({
messageId: message._id,
description: 'sample report',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
});
});
// get the report information by sending a request to moderation.reports
before(async () => {
await request
.get(api('moderation.reports'))
.set(credentials)
.query({
msgId: message._id,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('reports').and.to.be.an('array');
reportedMessage = res.body.reports[0];
});
});
it('should delete the message history', async () => {
await request
.post(api('moderation.user.deleteReportedMessages'))
.set(credentials)
.send({
userId: reportedMessage.reportedBy._id,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
});
});
it('should return an error when the userId is not provided', async () => {
await request
.post(api('moderation.user.deleteReportedMessages'))
.set(credentials)
.send({
userId: '',
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error').and.to.be.a('string');
});
});
});
describe('[/moderation.reportUser]', () => {
let userToBeReported: IUser;
before(async () => {
userToBeReported = await createUser();
});
after(async () => {
await deleteUser(userToBeReported);
});
it('should report an user', async () => {
await request
.post(api('moderation.reportUser'))
.set(credentials)
.send({
userId: userToBeReported?._id,
description: 'sample report',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
});
});
it('should fail to report an user if not provided description', async () => {
await request
.post(api('moderation.reportUser'))
.set(credentials)
.send({
userId: userToBeReported?._id,
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error');
expect(res.body).to.have.property('errorType', 'invalid-params');
});
});
});
});

View File

@@ -0,0 +1,426 @@
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import type { Response } from 'supertest';
import { getCredentials, api, request, credentials } from '../../data/api-data';
import { password } from '../../data/user';
import { createUser, deleteUser, login } from '../../data/users.helper';
async function authorizeAndExchange(loginToken: string, cId: string, cSecret: string, redirectUri: string) {
const authRes = await request
.post(`/oauth/authorize`)
.type('form')
.send({
token: loginToken,
client_id: cId,
response_type: 'code',
redirect_uri: redirectUri,
state: 'test-state',
allow: 'yes',
})
.expect(302);
const location = new URL(authRes.headers.location);
const code = location.searchParams.get('code') as string;
const tokenRes = await request
.post(`/oauth/token`)
.type('form')
.send({
grant_type: 'authorization_code',
code,
client_id: cId,
client_secret: cSecret,
redirect_uri: redirectUri,
})
.expect(200);
return { accessToken: tokenRes.body.access_token as string, refreshToken: tokenRes.body.refresh_token as string };
}
describe('[OAuth Server]', () => {
let oAuthAppId: string;
let clientId: string;
let clientSecret: string;
let code: string;
let refreshToken: string;
let accessToken: string;
let refreshedAccessToken: string;
const redirectUri = 'http://asd.com';
before((done) => getCredentials(done));
after(async () => {
await request
.post(api('oauth-apps.delete'))
.set(credentials)
.send({ appId: oAuthAppId })
.expect('Content-Type', 'application/json')
.expect(200);
});
describe('[/oauth-apps.create]', () => {
it('should create the oauth app', async () => {
const data = {
name: 'api-test',
redirectUri: 'http://test.com,http://asd.com',
active: true,
};
await request
.post(api('oauth-apps.create'))
.set(credentials)
.send(data)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('application');
expect(res.body.application).to.have.property('_id');
expect(res.body.application).to.have.property('name', data.name);
expect(res.body.application).to.have.property('redirectUri', data.redirectUri);
expect(res.body.application).to.have.property('active', data.active);
expect(res.body.application).to.have.property('clientId');
expect(res.body.application).to.have.property('clientSecret');
oAuthAppId = res.body.application._id;
clientId = res.body.application.clientId;
clientSecret = res.body.application.clientSecret;
});
});
it('should authorize oauth to retrieve code', async () => {
const params = new URLSearchParams({
scope: 'user',
response_type: 'token,code',
response_mode: 'form_post',
state: 'xus2t6ix57g',
});
await request
.post(`/oauth/authorize?${params.toString()}`)
.type('form')
.send({
token: credentials['X-Auth-Token'],
client_id: clientId,
response_type: 'code',
redirect_uri: redirectUri,
allow: 'yes',
})
.expect(302)
.expect((res: Response) => {
expect(res.headers).to.have.property('location');
const location = new URL(res.headers.location);
expect(location.origin).to.be.equal(redirectUri);
expect(location.searchParams.get('code')).to.be.string;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
code = location.searchParams.get('code')!;
});
});
it('should use code to retrieve access_token', async () => {
await request
.post(`/oauth/token`)
.type('form')
.send({
grant_type: 'authorization_code',
code,
client_id: clientId,
client_secret: clientSecret,
redirect_uri: redirectUri,
})
.expect('Content-Type', 'application/json; charset=utf-8')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('token_type', 'Bearer');
expect(res.body).to.have.property('access_token');
expect(res.body).to.have.property('expires_in');
expect(res.body).to.have.property('refresh_token');
accessToken = res.body.access_token;
refreshToken = res.body.refresh_token;
});
});
it('should return bad request if payload has non string parameters in refresh_token grant', async () => {
await request
.post(`/oauth/token`)
.send({
grant_type: 'refresh_token',
client_id: { $ne: null },
client_secret: { $ne: null },
refresh_token: { $ne: null },
})
.expect((res: Response) => {
expect(res.status).to.be.equal(400);
expect(res.body).to.have.property('error').that.is.a('string').and.equal('invalid_request');
});
});
it('should return bad request if payload has non string parameters in authorization_code grant', async () => {
await request
.post(`/oauth/token`)
.send({
grant_type: 'authorization_code',
client_id: { $ne: null },
client_secret: { $ne: null },
code: { $ne: null },
redirect_uri: { $ne: null },
})
.expect((res: Response) => {
expect(res.status).to.be.equal(400);
expect(res.body).to.have.property('error').that.is.a('string').and.equal('invalid_request');
});
});
it('should be able to refresh the access_token', async () => {
await request
.post(`/oauth/token`)
.type('form')
.send({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: clientId,
client_secret: clientSecret,
})
.expect('Content-Type', 'application/json; charset=utf-8')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('token_type', 'Bearer');
expect(res.body).to.have.property('access_token').and.not.be.equal(accessToken);
expect(res.body).to.have.property('expires_in');
expect(res.body).to.have.property('refresh_token');
refreshedAccessToken = res.body.access_token;
});
});
it('should not be able to get user info with old access_token', async () => {
await request.get(`/oauth/userinfo`).auth(accessToken, { type: 'bearer' }).expect(401);
});
it('should be able to get user info with refreshed access_token', async () => {
await request
.get(`/oauth/userinfo`)
.auth(refreshedAccessToken, { type: 'bearer' })
.expect('Content-Type', 'application/json; charset=utf-8')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('sub', 'rocketchat.internal.admin.test');
});
});
});
describe('[oauth credentials]', () => {
let accessToken: string;
before(() => {
accessToken = refreshedAccessToken;
});
it('should be able to use oauth credentials to access v1 endpoints (/v1/me)', async () => {
await request
.get(api('me'))
.auth(accessToken, { type: 'bearer' })
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('_id', 'rocketchat.internal.admin.test');
});
});
it('should be able to use oauth credentials to access v1 endpoints (/v1/users.info)', async () => {
await request
.get(api('users.info'))
.query({ username: 'rocketchat.internal.admin.test' })
.auth(accessToken, { type: 'bearer' })
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.nested.property('user._id', 'rocketchat.internal.admin.test');
});
});
const malformedTokenPayloads = [
{ query: { 'access_token[$ne]': 'null' }, description: '$ne operator' },
{ query: { 'access_token[$exists]': 'true' }, description: '$exists operator' },
{ query: { 'access_token[$gt]': '' }, description: '$gt operator' },
{ query: { 'access_token[$regex]': '.*' }, description: '$regex operator' },
{ query: { access_token: 'invalid-token' }, description: 'invalid string token' },
];
malformedTokenPayloads.forEach(({ query, description }) => {
it(`should reject access_token with ${description}`, async () => {
await request
.get(api('me'))
.query(query)
.expect('Content-Type', 'application/json')
.expect(401)
.expect((res: Response) => {
expect(res.body).to.have.property('status', 'error');
expect(res.body).to.have.property('message', 'You must be logged in to do this.');
});
});
});
});
describe('[user deactivation revokes OAuth tokens]', () => {
let testUser: Awaited<ReturnType<typeof createUser>>;
let testUserCredentials: { 'X-Auth-Token': string; 'X-User-Id': string };
let deactivationClientId: string;
let deactivationClientSecret: string;
let deactivationAppId: string;
let userAccessToken: string;
let userRefreshToken: string;
const redirectUri = 'http://asd.com';
before(async () => {
testUser = await createUser();
testUserCredentials = await login(testUser.username, password);
const appRes = await request
.post(api('oauth-apps.create'))
.set(credentials)
.send({ name: 'deactivation-test-app', redirectUri: `http://test.com,${redirectUri}`, active: true })
.expect(200);
deactivationAppId = appRes.body.application._id;
deactivationClientId = appRes.body.application.clientId;
deactivationClientSecret = appRes.body.application.clientSecret;
const tokens = await authorizeAndExchange(
testUserCredentials['X-Auth-Token'],
deactivationClientId,
deactivationClientSecret,
redirectUri,
);
userAccessToken = tokens.accessToken;
userRefreshToken = tokens.refreshToken;
// Verify tokens work before deactivation
await request.get(api('me')).auth(userAccessToken, { type: 'bearer' }).expect(200);
// Deactivate the user
await request.post(api('users.setActiveStatus')).set(credentials).send({ userId: testUser._id, activeStatus: false }).expect(200);
});
after(async () => {
await request.post(api('oauth-apps.delete')).set(credentials).send({ appId: deactivationAppId }).expect(200);
await deleteUser(testUser);
});
it('should reject the access token after user deactivation', async () => {
await request.get(api('me')).auth(userAccessToken, { type: 'bearer' }).expect(401);
});
it('should reject the access token on /oauth/userinfo after user deactivation', async () => {
await request.get(`/oauth/userinfo`).auth(userAccessToken, { type: 'bearer' }).expect(401);
});
it('should reject the refresh token grant after user deactivation', async () => {
await request
.post(`/oauth/token`)
.type('form')
.send({
grant_type: 'refresh_token',
refresh_token: userRefreshToken,
client_id: deactivationClientId,
client_secret: deactivationClientSecret,
})
.expect((res: Response) => {
expect(res.status).to.not.equal(200);
expect(res.body).to.have.property('error');
expect(res.body).to.not.have.property('access_token');
});
});
it('should still reject the access token after user reactivation (token was deleted, not just blocked)', async () => {
await request.post(api('users.setActiveStatus')).set(credentials).send({ userId: testUser._id, activeStatus: true }).expect(200);
await request.get(api('me')).auth(userAccessToken, { type: 'bearer' }).expect(401);
// Reactivated user can obtain new tokens via a fresh OAuth flow
const reactivatedCredentials = await login(testUser.username, password);
const newTokens = await authorizeAndExchange(
reactivatedCredentials['X-Auth-Token'],
deactivationClientId,
deactivationClientSecret,
redirectUri,
);
await request
.get(api('me'))
.auth(newTokens.accessToken, { type: 'bearer' })
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('_id', testUser._id);
});
});
});
describe('[users.deactivateIdle revokes OAuth tokens]', () => {
let idleUser: Awaited<ReturnType<typeof createUser>>;
let idleUserCredentials: { 'X-Auth-Token': string; 'X-User-Id': string };
let idleClientId: string;
let idleClientSecret: string;
let idleAppId: string;
let idleAccessToken: string;
let idleRefreshToken: string;
const redirectUri = 'http://asd.com';
before(async () => {
idleUser = await createUser();
idleUserCredentials = await login(idleUser.username, password);
const appRes = await request
.post(api('oauth-apps.create'))
.set(credentials)
.send({ name: 'idle-deactivation-test-app', redirectUri: `http://test.com,${redirectUri}`, active: true })
.expect(200);
idleAppId = appRes.body.application._id;
idleClientId = appRes.body.application.clientId;
idleClientSecret = appRes.body.application.clientSecret;
const tokens = await authorizeAndExchange(idleUserCredentials['X-Auth-Token'], idleClientId, idleClientSecret, redirectUri);
idleAccessToken = tokens.accessToken;
idleRefreshToken = tokens.refreshToken;
// Verify tokens work before deactivation
await request.get(api('me')).auth(idleAccessToken, { type: 'bearer' }).expect(200);
// Deactivate via deactivateIdle using daysIdle=0 to catch all users with no recent login
await request
.post(api('users.deactivateIdle'))
.set(credentials)
.send({ daysIdle: 0, role: idleUser.roles?.[0] ?? 'user' })
.expect(200);
});
after(async () => {
await request.post(api('oauth-apps.delete')).set(credentials).send({ appId: idleAppId }).expect(200);
await deleteUser(idleUser);
});
it('should reject the access token after idle deactivation', async () => {
await request.get(api('me')).auth(idleAccessToken, { type: 'bearer' }).expect(401);
});
it('should reject the refresh token grant after idle deactivation', async () => {
await request
.post(`/oauth/token`)
.type('form')
.send({
grant_type: 'refresh_token',
refresh_token: idleRefreshToken,
client_id: idleClientId,
client_secret: idleClientSecret,
})
.expect((res: Response) => {
expect(res.status).to.not.equal(200);
expect(res.body).to.have.property('error');
expect(res.body).to.not.have.property('access_token');
});
});
});
});

View File

@@ -0,0 +1,420 @@
import type { IOAuthApps } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { before, describe, it, after } from 'mocha';
import { getCredentials, api, request, credentials } from '../../data/api-data';
import { updatePermission } from '../../data/permissions.helper';
describe('[OAuthApps]', () => {
const createdAppsIds: IOAuthApps['_id'][] = [];
before((done) => getCredentials(done));
after(() =>
Promise.all([
updatePermission('manage-oauth-apps', ['admin']),
...createdAppsIds.map((appId) =>
request.post(api(`oauth-apps.delete`)).set(credentials).send({
appId,
}),
),
]),
);
describe('[/oauth-apps.list]', () => {
it('should return an error when the user does not have the necessary permission', async () => {
await updatePermission('manage-oauth-apps', []);
const res = await request.get(api('oauth-apps.list')).set(credentials).expect(403);
expect(res.body).to.have.property('success', false);
expect(res.body.error).to.be.equal('User does not have the permissions required for this action [error-unauthorized]');
});
it('should return an array of oauth apps', async () => {
await updatePermission('manage-oauth-apps', ['admin']);
const res = await request.get(api('oauth-apps.list')).set(credentials).expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('oauthApps').and.to.be.an('array');
});
});
describe('[/oauth-apps.create]', () => {
it('should return an error when the user does not have the necessary permission', async () => {
await updatePermission('manage-oauth-apps', []);
await request
.post(api('oauth-apps.create'))
.set(credentials)
.send({
name: 'error',
redirectUri: 'error',
active: false,
})
.expect('Content-Type', 'application/json')
.expect(403)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body.error).to.be.equal('User does not have the permissions required for this action [error-unauthorized]');
});
await updatePermission('manage-oauth-apps', ['admin']);
});
it("should return an error when the 'name' property is invalid", async () => {
await request
.post(api('oauth-apps.create'))
.set(credentials)
.send({
name: '',
redirectUri: 'error',
active: false,
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'error-invalid-name');
});
});
it("should return an error when the 'redirectUri' property is invalid", async () => {
await request
.post(api('oauth-apps.create'))
.set(credentials)
.send({
name: 'error',
redirectUri: '',
active: false,
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'error-invalid-redirectUri');
});
});
it("should return an error when the 'active' property is not a boolean", async () => {
await request
.post(api('oauth-apps.create'))
.set(credentials)
.send({
name: 'error',
redirectUri: 'error',
active: 'anything',
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'invalid-params');
});
});
it('should create an oauthApp', async () => {
const name = `new app ${Date.now()}`;
const redirectUri = 'http://localhost:3000';
const active = true;
await request
.post(api('oauth-apps.create'))
.set(credentials)
.send({
name,
redirectUri,
active,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.nested.property('application.name', name);
expect(res.body).to.have.nested.property('application.redirectUri', redirectUri);
expect(res.body).to.have.nested.property('application.active', active);
createdAppsIds.push(res.body.application._id);
});
});
});
describe('[/oauth-apps.get]', () => {
let clientId = '';
let _id = '';
let clientSecret = '';
before(async () => {
await updatePermission('manage-oauth-apps', ['admin']);
const res = await request
.post(api('oauth-apps.create'))
.set(credentials)
.send({
name: `new app ${Date.now()}`,
redirectUri: 'http://localhost:3000',
active: true,
});
if (res.statusCode !== 200 || !res.body?.success || !res.body.application?._id || !res.body.application?.clientId) {
console.error(res);
throw new Error('Failed to create oauth app for tests');
}
clientId = res.body.application.clientId;
_id = res.body.application._id;
clientSecret = res.body.application.clientSecret;
createdAppsIds.push(_id);
});
after(() => updatePermission('manage-oauth-apps', ['admin']));
it('should return a single oauthApp by client id', () => {
return request
.get(api('oauth-apps.get'))
.query({ clientId })
.set(credentials)
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('oauthApp');
expect(res.body.oauthApp._id).to.be.equal(_id);
expect(res.body.oauthApp).to.have.property('clientSecret');
if (clientSecret) {
expect(res.body.oauthApp.clientSecret).to.be.equal(clientSecret);
}
});
});
it('should return a single oauthApp by _id', () => {
return request
.get(api('oauth-apps.get'))
.query({ _id })
.set(credentials)
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('oauthApp');
expect(res.body.oauthApp._id).to.be.equal(_id);
expect(res.body.oauthApp.clientId).to.be.equal(clientId);
expect(res.body.oauthApp).to.have.property('clientSecret');
if (clientSecret) {
expect(res.body.oauthApp.clientSecret).to.be.equal(clientSecret);
}
});
});
it('should return only non sensitive information if user does not have the permission to manage oauth apps when searching by clientId', async () => {
await updatePermission('manage-oauth-apps', []);
await request
.get(api('oauth-apps.get'))
.query({ clientId })
.set(credentials)
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('oauthApp');
expect(res.body.oauthApp._id).to.be.equal(_id);
expect(res.body.oauthApp.clientId).to.be.equal(clientId);
expect(res.body.oauthApp).to.not.have.property('clientSecret');
});
});
it('should return only non sensitive information if user does not have the permission to manage oauth apps when searching by _id', async () => {
await updatePermission('manage-oauth-apps', []);
await request
.get(api('oauth-apps.get'))
.query({ _id })
.set(credentials)
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('oauthApp');
expect(res.body.oauthApp._id).to.be.equal(_id);
expect(res.body.oauthApp.clientId).to.be.equal(clientId);
expect(res.body.oauthApp).to.not.have.property('clientSecret');
});
});
it('should fail returning an oauth app when an invalid id is provided (avoid NoSQL injections)', () => {
return request
.get(api('oauth-apps.get'))
.query({ _id: { $ne: '' } })
.set(credentials)
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'error-invalid-params');
expect(res.body).to.have.property('error');
expect(res.body.error).to.include('must be string').and.include('must match exactly one schema in oneOf');
});
});
it('should fail returning an oauth app when an invalid id string is provided (avoid NoSQL injections)', () => {
return request
.get(api('oauth-apps.get'))
.query({ _id: '{ "$ne": "" }' })
.set(credentials)
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'OAuth app not found.');
});
});
it('should fail returning an oauth app when an invalid clientId is provided (avoid NoSQL injections)', () => {
return request
.get(api('oauth-apps.get'))
.query({ clientId: { $ne: '' } })
.set(credentials)
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'error-invalid-params');
expect(res.body).to.have.property('error');
expect(res.body.error).to.include('must be string').and.include('must match exactly one schema in oneOf');
});
});
it('should fail returning an oauth app when an invalid clientId string is provided (avoid NoSQL injections)', () => {
return request
.get(api('oauth-apps.get'))
.query({ clientId: '{ "$ne": "" }' })
.set(credentials)
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'OAuth app not found.');
});
});
});
describe('[/oauth-apps.update]', () => {
let appId: IOAuthApps['_id'];
before(async () => {
await updatePermission('manage-oauth-apps', ['admin']);
const name = 'test-oauth-app';
const redirectUri = 'https://test.com';
const active = true;
const res = await request
.post(api('oauth-apps.create'))
.set(credentials)
.send({
name,
redirectUri,
active,
})
.expect('Content-Type', 'application/json')
.expect(200);
appId = res.body.application._id;
createdAppsIds.push(appId);
});
after(async () => {
await updatePermission('manage-oauth-apps', ['admin']);
});
it("should update an app's name, its Active and Redirect URI fields correctly by its id", async () => {
const name = `new app ${Date.now()}`;
const redirectUri = 'http://localhost:3000';
const active = false;
await request
.post(api(`oauth-apps.update`))
.set(credentials)
.send({
appId,
name,
redirectUri,
active,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('active', active);
expect(res.body).to.have.property('redirectUri', redirectUri);
expect(res.body).to.have.property('name', name);
});
});
it('should fail updating an app if user does NOT have the manage-oauth-apps permission', async () => {
const name = `new app ${Date.now()}`;
const redirectUri = 'http://localhost:3000';
const active = false;
await updatePermission('manage-oauth-apps', []);
await request
.post(api(`oauth-apps.update`))
.set(credentials)
.send({
appId,
name,
redirectUri,
active,
})
.expect('Content-Type', 'application/json')
.expect(403)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body.error).to.be.equal('User does not have the permissions required for this action [error-unauthorized]');
});
});
});
describe('[/oauth-apps.delete]', () => {
let appId: IOAuthApps['_id'];
before(async () => {
await updatePermission('manage-oauth-apps', ['admin']);
const name = 'test-oauth-app';
const redirectUri = 'https://test.com';
const active = true;
const res = await request
.post(api('oauth-apps.create'))
.set(credentials)
.send({
name,
redirectUri,
active,
})
.expect('Content-Type', 'application/json')
.expect(200);
appId = res.body.application._id;
});
after(async () => {
await updatePermission('manage-oauth-apps', ['admin']);
});
it('should delete an app by its id', async () => {
await request
.post(api(`oauth-apps.delete`))
.set(credentials)
.send({
appId,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.equals(true);
});
});
it('should fail deleting an app by its id if user does NOT have the manage-oauth-apps permission', async () => {
await updatePermission('manage-oauth-apps', []);
await request
.post(api(`oauth-apps.delete`))
.set(credentials)
.send({
appId,
})
.expect('Content-Type', 'application/json')
.expect(403)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body.error).to.be.equal('User does not have the permissions required for this action [error-unauthorized]');
});
});
});
});

View File

@@ -0,0 +1,651 @@
import type { Credentials } from '@rocket.chat/api-client';
import type { IIntegration, IUser } from '@rocket.chat/core-typings';
import { assert, expect } from 'chai';
import { after, afterEach, before, describe, it } from 'mocha';
import { getCredentials, api, request, credentials } from '../../data/api-data';
import { createIntegration, removeIntegration } from '../../data/integration.helper';
import { updatePermission } from '../../data/permissions.helper';
import { password } from '../../data/user';
import type { TestUser } from '../../data/users.helper';
import { createUser, deleteUser, login } from '../../data/users.helper';
describe('[Outgoing Integrations]', () => {
let integrationCreatedByAnUser: IIntegration;
let user: TestUser<IUser>;
let userCredentials: Credentials;
before((done) => getCredentials(done));
before(async () => {
user = await createUser();
userCredentials = await login(user.username, password);
await updatePermission('manage-outgoing-integrations', ['user']);
integrationCreatedByAnUser = await createIntegration(
{
type: 'webhook-outgoing',
name: 'Guggy',
enabled: true,
username: 'rocket.cat',
urls: ['http://text2gif.guggy.com/guggify'],
scriptEnabled: false,
channel: '#general',
triggerWords: ['!guggy'],
alias: 'guggy',
avatar: 'http://res.guggy.com/logo_128.png',
emoji: ':ghost:',
event: 'sendMessage',
},
userCredentials,
);
});
before(async () => {
await Promise.all([
updatePermission('manage-incoming-integrations', []),
updatePermission('manage-own-incoming-integrations', []),
updatePermission('manage-own-outgoing-integrations', []),
updatePermission('manage-outgoing-integrations', []),
]);
});
after(async () =>
Promise.all([
updatePermission('manage-incoming-integrations', ['admin']),
updatePermission('manage-own-incoming-integrations', ['admin']),
updatePermission('manage-own-outgoing-integrations', ['admin']),
updatePermission('manage-outgoing-integrations', ['admin']),
deleteUser(user),
]),
);
let integration: IIntegration;
describe('[/integrations.create]', () => {
it('should return an error when the user DOES NOT have the permission "manage-outgoing-integrations" to add an outgoing integration', (done) => {
void updatePermission('manage-outgoing-integrations', []).then(() => {
void request
.post(api('integrations.create'))
.set(credentials)
.send({
type: 'webhook-outgoing',
name: 'Guggy',
enabled: true,
username: 'rocket.cat',
urls: ['http://text2gif.guggy.com/guggify'],
scriptEnabled: false,
channel: '#general',
triggerWords: ['!guggy'],
alias: 'guggy',
avatar: 'http://res.guggy.com/logo_128.png',
emoji: ':ghost:',
event: 'sendMessage',
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'not_authorized');
})
.end(done);
});
});
it('should return an error when the user DOES NOT have the permission "manage-own-outgoing-integrations" to add an outgoing integration', (done) => {
void updatePermission('manage-own-outgoing-integrations', []).then(() => {
void request
.post(api('integrations.create'))
.set(credentials)
.send({
type: 'webhook-outgoing',
name: 'Guggy',
enabled: true,
username: 'rocket.cat',
urls: ['http://text2gif.guggy.com/guggify'],
scriptEnabled: false,
channel: '#general',
triggerWords: ['!guggy'],
alias: 'guggy',
avatar: 'http://res.guggy.com/logo_128.png',
emoji: ':ghost:',
event: 'sendMessage',
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'not_authorized');
})
.end(done);
});
});
it('should return an error when the user sends an invalid type of integration', (done) => {
void request
.post(api('integrations.create'))
.set(credentials)
.send({
type: 'webhook-outgoing-invalid',
name: 'Guggy',
enabled: true,
username: 'rocket.cat',
urls: ['http://text2gif.guggy.com/guggify'],
scriptEnabled: false,
channel: '#general',
triggerWords: ['!guggy'],
alias: 'guggy',
avatar: 'http://res.guggy.com/logo_128.png',
emoji: ':ghost:',
event: 'sendMessage',
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'Invalid integration type.');
})
.end(done);
});
it('should add the integration successfully when the user ONLY has the permission "manage-outgoing-integrations" to add an outgoing integration', (done) => {
let integrationId: IIntegration['_id'];
void updatePermission('manage-outgoing-integrations', ['admin']).then(() => {
void request
.post(api('integrations.create'))
.set(credentials)
.send({
type: 'webhook-outgoing',
name: 'Guggy',
enabled: true,
username: 'rocket.cat',
urls: ['http://text2gif.guggy.com/guggify'],
scriptEnabled: false,
channel: '#general',
triggerWords: ['!guggy'],
alias: 'guggy',
avatar: 'http://res.guggy.com/logo_128.png',
emoji: ':ghost:',
event: 'sendMessage',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('integration').and.to.be.an('object');
integrationId = res.body.integration._id;
})
.end(() => removeIntegration(integrationId, 'outgoing').then(done));
});
});
it('should add the integration successfully when the user ONLY has the permission "manage-own-outgoing-integrations" to add an outgoing integration', (done) => {
void updatePermission('manage-outgoing-integrations', []).then(() => {
void updatePermission('manage-own-outgoing-integrations', ['admin']).then(() => {
void request
.post(api('integrations.create'))
.set(credentials)
.send({
type: 'webhook-outgoing',
name: 'Guggy',
enabled: true,
username: 'rocket.cat',
urls: ['http://text2gif.guggy.com/guggify'],
scriptEnabled: false,
channel: '#general',
triggerWords: ['!guggy'],
alias: 'guggy',
avatar: 'http://res.guggy.com/logo_128.png',
emoji: ':ghost:',
event: 'sendMessage',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('integration').and.to.be.an('object');
integration = res.body.integration;
})
.end(done);
});
});
});
it('should create an outgoing integration successfully', (done) => {
let integrationId: IIntegration['_id'];
void request
.post(api('integrations.create'))
.set(credentials)
.send({
type: 'webhook-outgoing',
name: 'Guggy',
enabled: true,
username: 'rocket.cat',
urls: ['http://text2gif.guggy.com/guggify'],
scriptEnabled: false,
channel: '#general',
triggerWords: ['!guggy'],
alias: 'guggy',
avatar: 'http://res.guggy.com/logo_128.png',
emoji: ':ghost:',
event: 'sendMessage',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.nested.property('integration.name', 'Guggy');
expect(res.body).to.have.nested.property('integration.type', 'webhook-outgoing');
expect(res.body).to.have.nested.property('integration.enabled', true);
expect(res.body).to.have.nested.property('integration.username', 'rocket.cat');
expect(res.body).to.have.nested.property('integration.event', 'sendMessage');
integrationId = res.body.integration._id;
})
.end(() => removeIntegration(integrationId, 'outgoing').then(done));
});
});
describe('[/integrations.list]', () => {
it('should return the list of outgoing integrations', (done) => {
void request
.get(api('integrations.list'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
const integrationCreatedByAdmin = (res.body.integrations as IIntegration[]).find(
(createdIntegration) => createdIntegration._id === integration._id,
);
assert.isDefined(integrationCreatedByAdmin);
expect(integrationCreatedByAdmin).to.be.an('object');
expect(integrationCreatedByAdmin._id).to.be.equal(integration._id);
expect(res.body).to.have.property('offset');
expect(res.body).to.have.property('items');
expect(res.body).to.have.property('total');
})
.end(done);
});
it('should return the list create by the user only', (done) => {
void updatePermission('manage-outgoing-integrations', []).then(() => {
void updatePermission('manage-own-outgoing-integrations', ['user']).then(() => {
void request
.get(api('integrations.list'))
.set(userCredentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
const integrationCreatedByAdmin = (res.body.integrations as IIntegration[]).find(
(createdIntegration) => createdIntegration._id === integration._id,
);
expect(integrationCreatedByAdmin).to.be.equal(undefined);
expect(res.body).to.have.property('offset');
expect(res.body).to.have.property('items');
expect(res.body).to.have.property('total');
})
.end(done);
});
});
});
it('should return unauthorized error when the user does not have any integrations permissions', async () => {
await Promise.all([
updatePermission('manage-incoming-integrations', []),
updatePermission('manage-own-incoming-integrations', []),
updatePermission('manage-outgoing-integrations', []),
updatePermission('manage-outgoing-integrations', []),
]);
await request
.get(api('integrations.list'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(403)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'User does not have the permissions required for this action [error-unauthorized]');
});
});
});
describe('[/integrations.history]', () => {
it('should return an error when the user DOES NOT the necessary permission', async () => {
await updatePermission('manage-outgoing-integrations', []);
await updatePermission('manage-own-outgoing-integrations', []);
await request
.get(api('integrations.history'))
.set(credentials)
.query({
id: integration._id,
})
.expect('Content-Type', 'application/json')
.expect(403)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'User does not have the permissions required for this action [error-unauthorized]');
});
});
it('should return the history of outgoing integrations', (done) => {
void updatePermission('manage-outgoing-integrations', ['admin']).then(() => {
void request
.get(api('integrations.history'))
.set(credentials)
.query({
id: integration._id,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('history').and.to.be.an('array');
expect(res.body).to.have.property('offset');
expect(res.body).to.have.property('items');
expect(res.body).to.have.property('total');
})
.end(done);
});
});
});
describe('[/integrations.get]', () => {
it('should return an error when the required "integrationId" query parameters is not sent', (done) => {
void request
.get(api('integrations.get'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', `must have required property 'integrationId'`);
})
.end(done);
});
it('should return an error when the user DOES NOT have the permission "manage-outgoing-integrations" to get an outgoing integration', (done) => {
void updatePermission('manage-outgoing-integrations', []).then(() => {
void request
.get(api('integrations.get'))
.query({ integrationId: integration._id })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'not-authorized');
})
.end(done);
});
});
it('should return an error when the user DOES NOT have the permission "manage-outgoing-integrations" to get an outgoing integration created by another user', (done) => {
void updatePermission('manage-outgoing-integrations', []).then(() => {
void request
.get(api('integrations.get'))
.query({ integrationId: integrationCreatedByAnUser._id })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'not-authorized');
})
.end(done);
});
});
it('should return an error when the user sends an invalid integration', (done) => {
void updatePermission('manage-outgoing-integrations', ['admin']).then(() => {
void request
.get(api('integrations.get'))
.query({ integrationId: 'invalid' })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'The integration does not exists.');
})
.end(done);
});
});
it('should return the integration successfully when the user is able to see only your own integrations', (done) => {
void updatePermission('manage-outgoing-integrations', [])
.then(() => updatePermission('manage-own-outgoing-integrations', ['user']))
.then(() => {
void request
.get(api('integrations.get'))
.query({ integrationId: integrationCreatedByAnUser._id })
.set(userCredentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('integration');
expect(res.body.integration._id).to.be.equal(integrationCreatedByAnUser._id);
})
.end(done);
});
});
it('should return the integration successfully', (done) => {
void updatePermission('manage-outgoing-integrations', ['admin']).then(() => {
void request
.get(api('integrations.get'))
.query({ integrationId: integration._id })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('integration');
expect(res.body.integration._id).to.be.equal(integration._id);
})
.end(done);
});
});
});
describe('[/integrations.remove]', () => {
it('should return an error when the user DOES NOT have the permission "manage-outgoing-integrations" to remove an outgoing integration', (done) => {
void updatePermission('manage-outgoing-integrations', []).then(() => {
void request
.post(api('integrations.remove'))
.set(credentials)
.send({
integrationId: integration._id,
type: 'webhook-outgoing',
})
.expect('Content-Type', 'application/json')
.expect(403)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'User does not have the permissions required for this action [error-unauthorized]');
})
.end(done);
});
});
it('should return an error when the user DOES NOT have the permission "manage-own-outgoing-integrations" to remove an outgoing integration', (done) => {
void updatePermission('manage-own-incoming-integrations', []).then(() => {
void request
.post(api('integrations.remove'))
.set(credentials)
.send({
integrationId: integration._id,
type: 'webhook-outgoing',
})
.expect('Content-Type', 'application/json')
.expect(403)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'User does not have the permissions required for this action [error-unauthorized]');
})
.end(done);
});
});
it('should return an error when the user sends an invalid type of integration', (done) => {
void updatePermission('manage-own-outgoing-integrations', ['admin']).then(() => {
void request
.post(api('integrations.remove'))
.set(credentials)
.send({
integrationId: integration._id,
type: 'invalid-type',
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error').include(`must match exactly one schema in oneOf`);
})
.end(done);
});
});
it('should remove the integration successfully when the user at least one of the necessary permission to remove an outgoing integration', (done) => {
void updatePermission('manage-outgoing-integrations', ['admin']).then(() => {
void request
.post(api('integrations.remove'))
.set(credentials)
.send({
integrationId: integration._id,
type: integration.type,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
})
.end(done);
});
});
it('the normal user should remove the integration successfully when the user have the "manage-own-outgoing-integrations" to remove an outgoing integration', (done) => {
void updatePermission('manage-own-outgoing-integrations', ['user']).then(() => {
void request
.post(api('integrations.remove'))
.set(userCredentials)
.send({
integrationId: integrationCreatedByAnUser._id,
type: integrationCreatedByAnUser.type,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
})
.end(done);
});
});
});
describe('[/integrations.clearHistory] and [/integrations.replayOutgoing]', () => {
let integrationId: IIntegration['_id'];
before(async () => {
await updatePermission('manage-outgoing-integrations', ['admin']);
const created = await createIntegration(
{
type: 'webhook-outgoing',
name: 'clear-replay-test',
enabled: true,
username: 'rocket.cat',
urls: ['http://example.com/hook'],
scriptEnabled: false,
channel: '#general',
triggerWords: ['!clearreplaytest'],
event: 'sendMessage',
},
credentials,
);
integrationId = created._id;
});
after(async () => {
await updatePermission('manage-outgoing-integrations', ['admin']);
await removeIntegration(integrationId, 'outgoing');
await Promise.all([updatePermission('manage-outgoing-integrations', []), updatePermission('manage-own-outgoing-integrations', [])]);
});
// the shared method enforces the permission (no route-level permissionsRequired), so a permission
// failure surfaces as 400 + errorType 'not_authorized', matching integrations.create above.
afterEach(() => updatePermission('manage-outgoing-integrations', ['admin']));
describe('[/integrations.clearHistory]', () => {
it('should fail when unauthenticated', () => request.post(api('integrations.clearHistory')).send({ integrationId }).expect(401));
it('should fail with 400 when integrationId is not provided', () =>
request.post(api('integrations.clearHistory')).set(credentials).send({}).expect(400));
it('should fail with 400 when the integration does not exist', () =>
request
.post(api('integrations.clearHistory'))
.set(credentials)
.send({ integrationId: 'does-not-exist' })
.expect(400)
.expect((res) => expect(res.body).to.have.property('success', false)));
it('should fail with 400 when the user lacks the manage-outgoing-integrations permission', async () => {
await Promise.all([updatePermission('manage-outgoing-integrations', []), updatePermission('manage-own-outgoing-integrations', [])]);
await request
.post(api('integrations.clearHistory'))
.set(credentials)
.send({ integrationId })
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'not_authorized');
});
});
it('should clear the integration history', () =>
request
.post(api('integrations.clearHistory'))
.set(credentials)
.send({ integrationId })
.expect(200)
.expect((res) => expect(res.body).to.have.property('success', true)));
});
describe('[/integrations.replayOutgoing]', () => {
it('should fail when unauthenticated', () =>
request.post(api('integrations.replayOutgoing')).send({ integrationId, historyId: 'x' }).expect(401));
it('should fail with 400 when required params are missing', () =>
request.post(api('integrations.replayOutgoing')).set(credentials).send({ integrationId }).expect(400));
it('should fail with 400 when the integration does not exist', () =>
request
.post(api('integrations.replayOutgoing'))
.set(credentials)
.send({ integrationId: 'does-not-exist', historyId: 'x' })
.expect(400)
.expect((res) => expect(res.body).to.have.property('success', false)));
it('should fail with 400 when the history does not exist', () =>
request
.post(api('integrations.replayOutgoing'))
.set(credentials)
.send({ integrationId, historyId: 'does-not-exist' })
.expect(400)
.expect((res) => expect(res.body).to.have.property('success', false)));
it('should fail with 400 when the user lacks permission (integration becomes inaccessible)', async () => {
await Promise.all([updatePermission('manage-outgoing-integrations', []), updatePermission('manage-own-outgoing-integrations', [])]);
await request
.post(api('integrations.replayOutgoing'))
.set(credentials)
.send({ integrationId, historyId: 'x' })
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'error-invalid-integration');
});
});
});
});
});

View File

@@ -0,0 +1,270 @@
import type { Credentials } from '@rocket.chat/api-client';
import type { IUser } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { before, describe, it, after } from 'mocha';
import { getCredentials, api, request, credentials } from '../../data/api-data';
import { updatePermission } from '../../data/permissions.helper';
import { password } from '../../data/user';
import { createUser, deleteUser, login } from '../../data/users.helper';
import type { TestUser } from '../../data/users.helper.js';
describe('[Permissions]', () => {
before((done) => getCredentials(done));
after(() => updatePermission('add-oauth-service', ['admin']));
describe('[/permissions.listAll]', () => {
it('should return an array with update and remove properties', async () => {
const res = await request.get(api('permissions.listAll')).set(credentials).expect('Content-Type', 'application/json').expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('update').and.to.be.an('array');
expect(res.body).to.have.property('remove').and.to.be.an('array');
});
it('should return an array with update and remov properties when search by "updatedSince" query parameter', async () => {
const res = await request
.get(api('permissions.listAll'))
.query({ updatedSince: '2018-11-27T13:52:01Z' })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('update').and.to.be.an('array');
expect(res.body).to.have.property('remove').and.to.be.an('array');
});
it('should return an error when updatedSince query parameter is not a valid ISODate string', async () => {
const res = await request
.get(api('permissions.listAll'))
.query({ updatedSince: 'fsafdf' })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(400);
expect(res.body).to.have.property('success', false);
});
});
describe('[/permissions.update]', () => {
let testUser: TestUser<IUser>;
let testUserCredentials: Credentials;
before(async () => {
testUser = await createUser();
testUserCredentials = await login(testUser.username, password);
await updatePermission('access-permissions', ['admin']);
});
after(async () => {
await updatePermission('access-permissions', ['admin']);
await deleteUser(testUser);
});
it('should change the permissions on the server', async () => {
const permissions = [
{
_id: 'add-oauth-service',
roles: ['admin', 'user'],
},
];
const res = await request
.post(api('permissions.update'))
.set(credentials)
.send({ permissions })
.expect('Content-Type', 'application/json')
.expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('permissions');
const firstElement = res.body.permissions[0];
expect(firstElement).to.have.property('_id');
expect(firstElement).to.have.property('roles').and.to.be.a('array');
expect(firstElement).to.have.property('_updatedAt');
});
it('should 400 when trying to set an unknown permission', async () => {
const permissions = [
{
_id: 'this-permission-does-not-exist',
roles: ['admin'],
},
];
const res = await request
.post(api('permissions.update'))
.set(credentials)
.send({ permissions })
.expect('Content-Type', 'application/json')
.expect(400);
expect(res.body).to.have.property('success', false);
});
it('should 400 when trying to assign a permission to an unknown role', async () => {
const permissions = [
{
_id: 'add-oauth-service',
roles: ['this-role-does-not-exist'],
},
];
const res = await request
.post(api('permissions.update'))
.set(credentials)
.send({ permissions })
.expect('Content-Type', 'application/json')
.expect(400);
expect(res.body).to.have.property('success', false);
});
it('should 400 when trying to set permissions to a string', async () => {
const permissions = '';
const res = await request
.post(api('permissions.update'))
.set(credentials)
.send({ permissions })
.expect('Content-Type', 'application/json')
.expect(400);
expect(res.body).to.have.property('success', false);
});
it('should fail updating permission if user does NOT have the access-permissions permission', async () => {
const permissions = [
{
_id: 'add-oauth-service',
roles: ['admin', 'user'],
},
];
await request
.post(api('permissions.update'))
.set(testUserCredentials)
.send({ permissions })
.expect('Content-Type', 'application/json')
.expect(403)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'User does not have the permissions required for this action [error-unauthorized]');
});
});
});
describe('[/permissions.addRole]', () => {
const testPermission = 'add-oauth-service';
const testRole = 'moderator';
let testUser: TestUser<IUser>;
let testUserCredentials: Credentials;
before(async () => {
await updatePermission('access-permissions', ['admin']);
testUser = await createUser();
testUserCredentials = await login(testUser.username, password);
});
after(async () => {
await request.post(api('permissions.removeRole')).set(credentials).send({ permissionId: testPermission, role: testRole });
await deleteUser(testUser);
await updatePermission('access-permissions', ['admin']);
});
it('should fail when unauthenticated', () =>
request.post(api('permissions.addRole')).send({ permissionId: testPermission, role: testRole }).expect(401));
it('should fail with 400 when the permission does not exist', () =>
request
.post(api('permissions.addRole'))
.set(credentials)
.send({ permissionId: 'this-permission-does-not-exist', role: testRole })
.expect(400)
.expect((res) => expect(res.body).to.have.property('success', false)));
it('should fail with 400 when the role does not exist', () =>
request
.post(api('permissions.addRole'))
.set(credentials)
.send({ permissionId: testPermission, role: 'this-role-does-not-exist' })
.expect(400)
.expect((res) => expect(res.body).to.have.property('success', false)));
it('should fail with 403 when the user lacks the access-permissions permission', () =>
request
.post(api('permissions.addRole'))
.set(testUserCredentials)
.send({ permissionId: testPermission, role: testRole })
.expect(403)
.expect((res) => expect(res.body).to.have.property('success', false)));
it('should add the role to the permission', async () => {
await request
.post(api('permissions.addRole'))
.set(credentials)
.send({ permissionId: testPermission, role: testRole })
.expect(200)
.expect((res) => expect(res.body).to.have.property('success', true));
await request
.get(api('permissions.listAll'))
.set(credentials)
.expect(200)
.expect((res) => {
const permission = res.body.update.find((p: { _id: string; roles: string[] }) => p._id === testPermission);
expect(permission).to.be.an('object');
expect(permission.roles).to.include(testRole);
});
});
});
describe('[/permissions.removeRole]', () => {
const testPermission = 'add-oauth-service';
const testRole = 'moderator';
let testUser: TestUser<IUser>;
let testUserCredentials: Credentials;
before(async () => {
await updatePermission('access-permissions', ['admin']);
testUser = await createUser();
testUserCredentials = await login(testUser.username, password);
await request.post(api('permissions.addRole')).set(credentials).send({ permissionId: testPermission, role: testRole });
});
after(async () => {
await deleteUser(testUser);
await updatePermission('access-permissions', ['admin']);
});
it('should fail when unauthenticated', () =>
request.post(api('permissions.removeRole')).send({ permissionId: testPermission, role: testRole }).expect(401));
it('should fail with 403 when the user lacks the access-permissions permission', () =>
request
.post(api('permissions.removeRole'))
.set(testUserCredentials)
.send({ permissionId: testPermission, role: testRole })
.expect(403)
.expect((res) => expect(res.body).to.have.property('success', false)));
it('should remove the role from the permission', async () => {
await request
.post(api('permissions.removeRole'))
.set(credentials)
.send({ permissionId: testPermission, role: testRole })
.expect(200)
.expect((res) => expect(res.body).to.have.property('success', true));
await request
.get(api('permissions.listAll'))
.set(credentials)
.expect(200)
.expect((res) => {
const permission = res.body.update.find((p: { _id: string; roles: string[] }) => p._id === testPermission);
expect(permission).to.be.an('object');
expect(permission.roles).to.not.include(testRole);
});
});
});
});

View File

@@ -0,0 +1,130 @@
import { expect } from 'chai';
import { before, describe, it, after } from 'mocha';
import type { Response } from 'supertest';
import { getCredentials, api, request, credentials } from '../../data/api-data';
import { updatePermission } from '../../data/permissions.helper';
import { password } from '../../data/user';
import { createUser, deleteUser, login } from '../../data/users.helper';
describe('[Presence]', () => {
let createdUser: any;
before((done) => getCredentials(done));
let unauthorizedUserCredentials: any;
before(async () => {
createdUser = await createUser();
unauthorizedUserCredentials = await login(createdUser.username, password);
});
after(() => Promise.all([updatePermission('manage-user-status', ['admin']), deleteUser(createdUser)]));
describe('[/presence.getConnections]', () => {
it('should throw an error if not authenticated', async () => {
await request
.get(api('presence.getConnections'))
.expect('Content-Type', 'application/json')
.expect(401)
.expect((res: Response) => {
expect(res.body).to.have.property('status', 'error');
expect(res.body).to.have.property('message');
});
});
it('should throw an error if user is unauthorized', async () => {
await request
.get(api('presence.getConnections'))
.set(unauthorizedUserCredentials)
.expect('Content-Type', 'application/json')
.expect(403)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error');
});
});
it("should throw an error if doesn't have required permission", async () => {
await updatePermission('manage-user-status', []);
await request
.get(api('presence.getConnections'))
.set(unauthorizedUserCredentials)
.expect('Content-Type', 'application/json')
.expect(403)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error');
});
await updatePermission('manage-user-status', ['admin']);
});
it('should return current and max connections of 200', async () => {
await request
.get(api('presence.getConnections'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('current').to.be.a('number');
expect(res.body).to.have.property('max').to.be.a('number').and.to.be.equal(200);
});
});
});
describe('[/presence.enableBroadcast]', () => {
it('should throw an error if not authenticated', async () => {
await request
.post(api('presence.enableBroadcast'))
.expect('Content-Type', 'application/json')
.expect(401)
.expect((res: Response) => {
expect(res.body).to.have.property('status', 'error');
expect(res.body).to.have.property('message');
});
});
it('should throw an error if user is unauthorized', async () => {
await request
.post(api('presence.enableBroadcast'))
.set(unauthorizedUserCredentials)
.expect('Content-Type', 'application/json')
.expect(403)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error');
});
});
it("should throw an error if doesn't have required permission", async () => {
await updatePermission('manage-user-status', []);
await request
.post(api('presence.enableBroadcast'))
.set(unauthorizedUserCredentials)
.expect('Content-Type', 'application/json')
.expect(403)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error');
});
await updatePermission('manage-user-status', ['admin']);
});
it('should return success', async () => {
await request
.post(api('presence.enableBroadcast'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
});
});
});
// describe('[/presence.enableBroadcast]', () => {});
});

View File

@@ -0,0 +1,286 @@
import { expect } from 'chai';
import { before, describe, it, after } from 'mocha';
import { getCredentials, api, request, credentials } from '../../data/api-data';
import { updateSetting } from '../../data/permissions.helper';
describe('[Push]', () => {
before((done) => getCredentials(done));
describe('POST [/push.token]', () => {
it('should succeed with a valid gcm token', async () => {
await request
.post(api('push.token'))
.set(credentials)
.send({
type: 'gcm',
value: 'token',
appName: 'com.example.rocketchat',
})
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('result').and.to.be.an('object');
});
});
it('should succeed with a valid apn token', async () => {
await request
.post(api('push.token'))
.set(credentials)
.send({
type: 'apn',
value: 'token',
appName: 'com.example.rocketchat',
})
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('result').and.to.be.an('object');
});
});
it('should fail if not logged in', async () => {
await request
.post(api('push.token'))
.send({
type: 'gcm',
value: 'token',
appName: 'com.example.rocketchat',
})
.expect(401)
.expect((res) => {
expect(res.body).to.have.property('status', 'error');
expect(res.body).to.have.property('message');
});
});
it('should fail if missing type', async () => {
await request
.post(api('push.token'))
.set(credentials)
.send({
value: 'token',
appName: 'com.example.rocketchat',
})
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'invalid-params');
expect(res.body).to.have.property('error').that.includes(`must have required property 'type'`);
});
});
it('should fail if missing value', async () => {
await request
.post(api('push.token'))
.set(credentials)
.send({
type: 'gcm',
appName: 'com.example.rocketchat',
})
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'invalid-params');
expect(res.body).to.have.property('error').that.includes(`must have required property 'value'`);
});
});
it('should fail if missing appName', async () => {
await request
.post(api('push.token'))
.set(credentials)
.send({
type: 'gcm',
value: 'token',
})
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'invalid-params');
expect(res.body).to.have.property('error').that.includes(`must have required property 'appName'`);
});
});
it('should fail if type param is unknown', async () => {
await request
.post(api('push.token'))
.set(credentials)
.send({
type: 'unknownPlatform',
value: 'token',
appName: 'com.example.rocketchat',
})
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'invalid-params');
expect(res.body).to.have.property('error').that.includes(`must be equal to one of the allowed values`);
});
});
it('should fail if token param is empty', async () => {
await request
.post(api('push.token'))
.set(credentials)
.send({
type: 'gcm',
appName: 'com.example.rocketchat',
value: '',
})
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'invalid-params');
expect(res.body).to.have.property('error').that.includes(`must NOT have fewer than 1 characters`);
});
});
});
describe('DELETE [/push.token]', () => {
it('should fail if not logged in', async () => {
await request
.delete(api('push.token'))
.send({
token: 'token',
})
.expect(401)
.expect((res) => {
expect(res.body).to.have.property('status', 'error');
expect(res.body).to.have.property('message');
});
});
it('should fail if missing token key', async () => {
await request
.delete(api('push.token'))
.set(credentials)
.send({})
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'invalid-params');
expect(res.body).to.have.property('error').that.includes(`must have required property 'token'`);
});
});
it('should fail if token is empty', async () => {
await request
.delete(api('push.token'))
.set(credentials)
.send({
token: '',
})
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'invalid-params');
expect(res.body).to.have.property('error').that.includes(`must NOT have fewer than 1 characters`);
});
});
it('should fail if token is invalid', async () => {
await request
.delete(api('push.token'))
.set(credentials)
.send({
token: '123',
})
.expect(404)
.expect((res) => {
expect(res.body).to.have.property('success', false);
});
});
it('should delete a token if valid', async () => {
await request
.delete(api('push.token'))
.set(credentials)
.send({
token: 'token',
})
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
});
});
it('should fail if token is already deleted', async () => {
await request
.delete(api('push.token'))
.set(credentials)
.send({
token: 'token',
})
.expect(404)
.expect((res) => {
expect(res.body).to.have.property('success', false);
});
});
});
describe('[/push.test]', () => {
before(() => updateSetting('Push_enable', false));
it('should fail if not logged in', async () => {
await request
.post(api('push.test'))
.expect(401)
.expect((res) => {
expect(res.body).to.have.property('status', 'error');
expect(res.body).to.have.property('message');
});
});
it('should fail if push is disabled', async () => {
await request
.post(api('push.test'))
.set(credentials)
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'error-push-disabled');
});
});
});
describe('[/push.info]', () => {
before(() => Promise.all([updateSetting('Push_gateway', 'https://random-gateway.rocket.chat')]));
after(() => Promise.all([updateSetting('Push_gateway', 'https://gateway.rocket.chat')]));
it('should fail if not logged in', async () => {
await request
.get(api('push.info'))
.expect(401)
.expect((res) => {
expect(res.body).to.have.property('status', 'error');
expect(res.body).to.have.property('message');
});
});
it('should succesfully retrieve non default push notification info', async () => {
await request
.get(api('push.info'))
.set(credentials)
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('pushGatewayEnabled', false);
expect(res.body).to.have.property('defaultPushGateway', false);
});
});
it('should succesfully retrieve default push notification info', async () => {
await updateSetting('Push_gateway', 'https://gateway.rocket.chat');
await request
.get(api('push.info'))
.set(credentials)
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('pushGatewayEnabled', false);
expect(res.body).to.have.property('defaultPushGateway', true);
});
});
});
});

View File

@@ -0,0 +1,575 @@
import type { IUser } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import type { Response } from 'supertest';
import { getCredentials, api, request, credentials } from '../../data/api-data';
import { updatePermission } from '../../data/permissions.helper';
import { password, adminUsername } from '../../data/user';
import { createUser, deleteUser, login } from '../../data/users.helper';
describe('[Roles]', function () {
this.retries(0);
const isEnterprise = Boolean(process.env.IS_EE);
before((done) => getCredentials(done));
describe.skip('[/roles.create]', () => {
const testRoleName = `role.test.${Date.now()}`;
it('should throw an error when not running EE to create a role', async function () {
// TODO this is not the right way to do it. We're doing this way for now just because we have separate CI jobs for EE and CE,
// ideally we should have a single CI job that adds a license and runs both CE and EE tests.
if (isEnterprise) {
this.skip();
}
await request
.post(api('roles.create'))
.set(credentials)
.send({
name: testRoleName,
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'This is an enterprise feature [error-action-not-allowed]');
expect(res.body).to.have.property('errorType', 'error-action-not-allowed');
});
});
it('should successfully create a role in EE', async function () {
// TODO this is not the right way to do it. We're doing this way for now just because we have separate CI jobs for EE and CE,
// ideally we should have a single CI job that adds a license and runs both CE and EE tests.
if (!isEnterprise) {
this.skip();
}
await request
.post(api('roles.create'))
.set(credentials)
.send({
name: testRoleName,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('role');
expect(res.body.role).to.have.property('name', testRoleName);
});
});
});
describe('[/roles.update]', () => {
const testRoleName = `role.test.${Date.now()}`;
const newTestRoleName = `role.test.updated.${Date.now()}`;
let testRoleId = '';
before('Create a new role with Users scope', async () => {
if (!isEnterprise) {
return;
}
await request
.post(api('roles.create'))
.set(credentials)
.send({
name: testRoleName,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('role');
expect(res.body.role).to.have.property('name', testRoleName);
testRoleId = res.body.role._id;
});
});
after(async () => {
if (!isEnterprise) {
return;
}
await request.post(api('roles.delete')).set(credentials).send({
roleId: testRoleId,
});
});
it('should throw an error when not running EE to update a role', async function () {
// TODO this is not the right way to do it. We're doing this way for now just because we have separate CI jobs for EE and CE,
// ideally we should have a single CI job that adds a license and runs both CE and EE tests.
if (isEnterprise) {
this.skip();
}
await request
.post(api('roles.update'))
.set(credentials)
.send({
name: testRoleName,
roleId: testRoleId,
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'This is an enterprise feature [error-action-not-allowed]');
expect(res.body).to.have.property('errorType', 'error-action-not-allowed');
});
});
it('should successfully update a role in EE', async function () {
// TODO this is not the right way to do it. We're doing this way for now just because we have separate CI jobs for EE and CE,
// ideally we should have a single CI job that adds a license and runs both CE and EE tests.
if (!isEnterprise) {
this.skip();
}
await request
.post(api('roles.update'))
.set(credentials)
.send({
name: newTestRoleName,
roleId: testRoleId,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('role');
expect(res.body.role).to.have.property('name', newTestRoleName);
});
});
});
describe('[/roles.getUsersInRole]', () => {
let testUser: IUser;
let testUserCredentials: { 'X-Auth-Token': string; 'X-User-Id': string };
const testRoleName = `role.getUsersInRole.${Date.now()}`;
let testRoleId = '';
before(async () => {
await updatePermission('access-permissions', ['admin']);
testUser = await createUser();
testUserCredentials = await login(testUser.username, password);
if (!isEnterprise) {
return;
}
await request
.post(api('roles.create'))
.set(credentials)
.send({
name: testRoleName,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('role');
expect(res.body.role).to.have.property('name', testRoleName);
testRoleId = res.body.role._id;
});
});
after(async () => {
await deleteUser(testUser);
if (!isEnterprise) {
return;
}
await request.post(api('roles.delete')).set(credentials).send({
roleId: testRoleId,
});
});
it('should successfully get an empty list of users in a role if no user has been assigned to it', async function () {
if (!isEnterprise) {
this.skip();
}
await request
.get(api('roles.getUsersInRole'))
.set(credentials)
.query({
role: testRoleId,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('total', 0);
expect(res.body).to.have.property('users').that.is.an('array').that.is.empty;
});
});
it('should successfully get a list of users in a role', async function () {
if (!isEnterprise) {
this.skip();
}
await request
.post(api('roles.addUserToRole'))
.set(credentials)
.send({
roleId: testRoleId,
username: adminUsername,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
});
await request
.get(api('roles.getUsersInRole'))
.set(credentials)
.query({
role: testRoleId,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('total', 1);
expect(res.body).to.have.property('users').that.is.an('array').of.length(1);
expect(res.body.users[0]).to.have.property('_id', credentials['X-User-Id']);
});
});
it('should fail getting a list of users in a role in case an invalid role is provided', async function () {
if (!isEnterprise) {
this.skip();
}
await request
.get(api('roles.getUsersInRole'))
.set(credentials)
.query({
role: 'invalid-role',
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'error-invalid-roleId');
});
});
it('should fail when user does NOT have the access-permissions permission', async () => {
await request
.get(api('roles.getUsersInRole'))
.set(testUserCredentials)
.query({
role: 'admin',
})
.expect('Content-Type', 'application/json')
.expect(403)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'User does not have the permissions required for this action [error-unauthorized]');
});
});
(isEnterprise ? it : it.skip)('should fail getting a list of users in a role in case a role name is provided', async () => {
await request
.get(api('roles.getUsersInRole'))
.set(credentials)
.query({
role: testRoleName,
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'error-invalid-roleId');
});
});
});
describe('[/roles.delete]', () => {
let testUser: IUser;
let testUserCredentials: { 'X-Auth-Token': string; 'X-User-Id': string };
const testRoleName = `role.delete.${Date.now()}`;
let testRoleId = '';
before(async () => {
if (!isEnterprise) {
return;
}
testUser = await createUser();
testUserCredentials = await login(testUser.username, password);
await updatePermission('access-permissions', ['admin']);
await request
.post(api('roles.create'))
.set(credentials)
.send({
name: testRoleName,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('role');
expect(res.body.role).to.have.property('name', testRoleName);
testRoleId = res.body.role._id;
});
});
after(async () => {
if (!isEnterprise) {
return;
}
await deleteUser(testUser);
});
it('should fail deleting a role when user does NOT have the access-permissions permission', async function () {
// TODO this is not the right way to do it. We're doing this way for now just because we have separate CI jobs for EE and CE,
// ideally we should have a single CI job that adds a license and runs both CE and EE tests.
if (!isEnterprise) {
this.skip();
}
await request
.post(api('roles.delete'))
.set(testUserCredentials)
.send({
roleId: testRoleId,
})
.expect('Content-Type', 'application/json')
.expect(403)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'User does not have the permissions required for this action [error-unauthorized]');
});
});
it('should fail deleting a role in EE in case an invalid role is provided', async function () {
// TODO this is not the right way to do it. We're doing this way for now just because we have separate CI jobs for EE and CE,
// ideally we should have a single CI job that adds a license and runs both CE and EE tests.
if (!isEnterprise) {
this.skip();
}
await request
.post(api('roles.delete'))
.set(credentials)
.send({
roleId: 'invalid-role',
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'error-invalid-roleId');
expect(res.body).to.have.property('error', 'This role does not exist [error-invalid-roleId]');
});
});
it('should successfully delete a role in EE', async function () {
// TODO this is not the right way to do it. We're doing this way for now just because we have separate CI jobs for EE and CE,
// ideally we should have a single CI job that adds a license and runs both CE and EE tests.
if (!isEnterprise) {
this.skip();
}
await request
.post(api('roles.delete'))
.set(credentials)
.send({
roleId: testRoleId,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
});
});
});
describe('[/roles.removeUserFromRole]', () => {
let testUser: IUser;
let testUserCredentials: { 'X-Auth-Token': string; 'X-User-Id': string };
const testRoleName = `role.removeUsersFromRole.${Date.now()}`;
let testRoleId = '';
before(async () => {
await updatePermission('access-permissions', ['admin']);
testUser = await createUser();
testUserCredentials = await login(testUser.username, password);
if (!isEnterprise) {
return;
}
await request
.post(api('roles.create'))
.set(credentials)
.send({
name: testRoleName,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('role');
expect(res.body.role).to.have.property('name', testRoleName);
testRoleId = res.body.role._id;
});
await request
.post(api('roles.addUserToRole'))
.set(credentials)
.send({
roleId: testRoleId,
username: testUser.username,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
});
});
after(async () => {
await request.post(api('roles.delete')).set(credentials).send({
roleId: testRoleId,
});
await deleteUser(testUser);
});
it('should fail removing a user from a role when user does NOT have the access-permissions permission', async () => {
await request
.post(api('roles.removeUserFromRole'))
.set(testUserCredentials)
.send({
roleId: testRoleId,
username: testUser.username,
})
.expect('Content-Type', 'application/json')
.expect(403)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'User does not have the permissions required for this action [error-unauthorized]');
});
});
it('should fail removing an invalid user from a role', async function () {
// TODO this is not the right way to do it. We're doing this way for now just because we have separate CI jobs for EE and CE,
// ideally we should have a single CI job that adds a license and runs both CE and EE tests.
if (!isEnterprise) {
this.skip();
}
await request
.post(api('roles.removeUserFromRole'))
.set(credentials)
.send({
roleId: testRoleId,
username: 'invalid-username',
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'error-invalid-user');
expect(res.body).to.have.property('error', 'There is no user with this username [error-invalid-user]');
});
});
it('should fail removing a user from an invalid role', async function () {
// TODO this is not the right way to do it. We're doing this way for now just because we have separate CI jobs for EE and CE,
// ideally we should have a single CI job that adds a license and runs both CE and EE tests.
if (!isEnterprise) {
this.skip();
}
await request
.post(api('roles.removeUserFromRole'))
.set(credentials)
.send({
roleId: 'invalid-role',
username: testUser.username,
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'error-invalid-roleId');
expect(res.body).to.have.property('error', 'This role does not exist [error-invalid-roleId]');
});
});
it('should fail removing a user from a role they do not have', async function () {
// TODO this is not the right way to do it. We're doing this way for now just because we have separate CI jobs for EE and CE,
// ideally we should have a single CI job that adds a license and runs both CE and EE tests.
if (!isEnterprise) {
this.skip();
}
await request
.post(api('roles.removeUserFromRole'))
.set(credentials)
.send({
roleId: 'admin',
username: testUser.username,
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'error-user-not-in-role');
expect(res.body).to.have.property('error', 'User is not in this role [error-user-not-in-role]');
});
});
it('should successfully remove a user from a role', async function () {
// TODO this is not the right way to do it. We're doing this way for now just because we have separate CI jobs for EE and CE,
// ideally we should have a single CI job that adds a license and runs both CE and EE tests.
if (!isEnterprise) {
this.skip();
}
await request
.post(api('roles.removeUserFromRole'))
.set(credentials)
.send({
roleId: testRoleId,
username: testUser.username,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
});
await request
.get(api('roles.getUsersInRole'))
.set(credentials)
.query({
role: testRoleId,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('total', 0);
expect(res.body).to.have.property('users');
expect(res.body.users).to.be.an('array').that.is.empty;
});
});
});
describe('[/roles.getUsersInPublicRoles]', () => {
it('should return public roles', async () => {
await request
.get(api('roles.getUsersInPublicRoles'))
.set(credentials)
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('users');
expect(res.body.users).to.be.an('array');
// payload schema is checked in the endpoint
expect(res.body.users.length).to.be.greaterThan(0);
});
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,861 @@
import type { IServerEvents, LoginServiceConfiguration } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { before, describe, it, after, afterEach } from 'mocha';
import { getCredentials, api, request, credentials } from '../../data/api-data';
import { updatePermission, updateSetting, getSettingValueById } from '../../data/permissions.helper';
import { IS_EE } from '../../e2e/config/constants';
describe('[Settings]', () => {
before((done) => getCredentials(done));
describe('[/settings.public]', () => {
it('should return public settings', async () => {
const res = await request.get(api('settings.public')).expect('Content-Type', 'application/json').expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('settings');
expect(res.body).to.have.property('count');
});
it('should return public settings even requested with count and offset params', async () => {
const res = await request
.get(api('settings.public'))
.query({
count: 5,
offset: 0,
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(res.body).to.have.property('success').and.to.be.true;
expect(res.body).to.have.property('settings').and.to.be.an('array').and.to.have.lengthOf(5);
expect(res.body).to.have.property('count').and.to.be.a('number').and.to.equal(5);
});
it('should return public settings even requested with _id param', async () => {
const res = await request
.get(api('settings.public'))
.query({
_id: 'Site_Url',
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(res.body).to.have.property('success').and.to.be.true;
expect(res.body).to.have.property('settings').and.to.be.an('array').and.to.have.lengthOf(1);
expect(res.body).to.have.property('count').and.to.be.a('number').and.to.equal(1);
});
it('should return public settings even requested with _id param as an array', async () => {
const res = await request
.get(api('settings.public'))
.query({
_id: 'Site_Url,LDAP_Enable',
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(res.body).to.have.property('success').and.to.be.true;
expect(res.body).to.have.property('settings').and.to.be.an('array').and.to.have.lengthOf(2);
expect(res.body).to.have.property('count').and.to.be.a('number').and.to.equal(2);
});
it('should return an empty response when requesting public settings with a broken _id param', async () => {
const res = await request
.get(api('settings.public'))
.query({
_id: 10,
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(res.body).to.have.property('success').and.to.be.true;
expect(res.body).to.have.property('settings').and.to.be.an('array').and.to.be.empty;
expect(res.body).to.have.property('count').and.to.be.a('number').and.to.equal(0);
expect(res.body).to.have.property('offset').and.to.be.a('number').and.to.equal(0);
expect(res.body).to.have.property('total').and.to.be.a('number').and.to.equal(0);
});
});
describe('[/settings]', () => {
it('should return private settings', async () => {
const res = await request.get(api('settings')).set(credentials).expect('Content-Type', 'application/json').expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('settings');
expect(res.body).to.have.property('count');
});
it('should return the default values of the settings when includeDefaults is true', async () => {
return request
.get(api('settings'))
.query({ includeDefaults: true })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('settings');
expect(res.body).to.have.property('count');
expect(res.body.settings[0]).to.have.property('packageValue');
});
});
describe('POST (bulk)', () => {
before(async () => {
await updatePermission('edit-privileged-setting', ['admin']);
});
after(async () => {
await updatePermission('edit-privileged-setting', ['admin']);
});
it('should return unauthorized when not authenticated', async () => {
await request
.post(api('settings'))
.send({ settings: [{ _id: 'LDAP_Enable', value: false }] })
.expect(401);
});
it('should return bad request when settings is missing', async () => {
await request.post(api('settings')).set(credentials).send({}).expect(400);
});
it('should return bad request when settings is empty', async () => {
await request.post(api('settings')).set(credentials).send({ settings: [] }).expect(400);
});
it('should return bad request when an item is missing value', async () => {
await request
.post(api('settings'))
.set(credentials)
.send({ settings: [{ _id: 'LDAP_Enable' }] })
.expect(400);
});
it('should reject an empty integer value without changing the setting', async () => {
const settingId = 'UserData_ProcessingFrequency';
const originalValue = await getSettingValueById(settingId);
await request
.post(api('settings'))
.set(credentials)
.send({ settings: [{ _id: settingId, value: null }] })
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
});
expect(await getSettingValueById(settingId)).to.equal(originalValue);
});
it('should successfully update multiple settings in a single request', async () => {
await request
.post(api('settings'))
.set(credentials)
.send({
settings: [
{ _id: 'LDAP_Enable', value: false },
{ _id: 'Accounts_Default_User_Preferences_masterVolume', value: 50 },
],
})
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
});
await request
.get(api('settings/LDAP_Enable'))
.set(credentials)
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('value', false);
});
await request
.get(api('settings/Accounts_Default_User_Preferences_masterVolume'))
.set(credentials)
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('value', 50);
});
});
it('should accept an optional editor for a setting', async () => {
await request
.post(api('settings'))
.set(credentials)
.send({ settings: [{ _id: 'Livechat_title_color', value: '#000000', editor: 'color' }] })
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
});
await request
.get(api('settings/Livechat_title_color'))
.set(credentials)
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('value', '#000000');
});
});
it('should return bad request when editor is an empty string', async () => {
await request
.post(api('settings'))
.set(credentials)
.send({ settings: [{ _id: 'Livechat_title_color', value: '#C1272D', editor: '' }] })
.expect(400);
});
it('should fail when the user does not have edit-privileged-setting permission', async () => {
await updatePermission('edit-privileged-setting', []);
await request
.post(api('settings'))
.set(credentials)
.send({ settings: [{ _id: 'LDAP_Enable', value: false }] })
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body.error).to.match(/error-action-not-allowed/);
});
});
});
});
describe('[/settings/:_id]', () => {
before(async () => {
await updatePermission('view-privileged-setting', ['admin']);
await updatePermission('edit-privileged-setting', ['admin']);
});
after(async () => {
await updatePermission('view-privileged-setting', ['admin']);
await updatePermission('edit-privileged-setting', ['admin']);
});
it('should successfully return one setting (GET)', async () => {
return request
.get(api('settings/Site_Url'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('_id', 'Site_Url');
expect(res.body).to.have.property('value');
});
});
it('should fail returning a setting if user does NOT have the view-privileged-setting permission (GET)', async () => {
await updatePermission('view-privileged-setting', []);
return request
.get(api('settings/Site_Url'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(403)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'User does not have the permissions required for this action [error-unauthorized]');
});
});
it('should successfully set the value of a setting (POST)', async () => {
return request
.post(api('settings/LDAP_Enable'))
.set(credentials)
.send({
value: false,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
});
});
it('should fail updating the value of a setting less than its minValue (POST)', async () => {
return request
.post(api('settings/Accounts_Default_User_Preferences_masterVolume'))
.set(credentials)
.send({
value: '-1',
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property(
'error',
'Value for setting Accounts_Default_User_Preferences_masterVolume must be greater than or equal to 0 [error-invalid-setting-value]',
);
});
});
it('should fail updating the value of a setting greater than its maxValue (POST)', async () => {
return request
.post(api('settings/Accounts_Default_User_Preferences_masterVolume'))
.set(credentials)
.send({
value: '101',
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property(
'error',
'Value for setting Accounts_Default_User_Preferences_masterVolume must be less than or equal to 100 [error-invalid-setting-value]',
);
});
});
it('should fail updating the value of a setting if user does NOT have the edit-privileged-setting permission (POST)', async () => {
await updatePermission('edit-privileged-setting', []);
return request
.post(api('settings/LDAP_Enable'))
.set(credentials)
.send({
value: false,
})
.expect('Content-Type', 'application/json')
.expect(403)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'User does not have the permissions required for this action [error-unauthorized]');
});
});
});
describe('[Room_Toolbox_Layout]', () => {
const validLayout = [
{ roomType: ['c', 'p'], maxVisibleNormal: 3, items: [{ id: 'members-list', featured: true, order: 1 }] },
{ roomType: ['d'], maxVisibleNormal: 1 },
];
const saveLayout = (value: string) => request.post(api('settings/Room_Toolbox_Layout')).set(credentials).send({ value });
after(() => updateSetting('Room_Toolbox_Layout', ''));
it('should reject a value that is not valid JSON', async () => {
await saveLayout('not json {')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'Room_Toolbox_Layout_Invalid');
expect(res.body).to.have.property('errorType', 'error-setting-validation-failed');
});
});
it('should reject an entry declaring an unsupported room type', async () => {
await saveLayout(JSON.stringify([{ roomType: ['l'] }]))
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'Room_Toolbox_Layout_Invalid');
expect(res.body).to.have.property('errorType', 'error-setting-validation-failed');
});
});
it('should reject entries claiming the same room type', async () => {
await saveLayout(JSON.stringify([{ roomType: ['c', 'p'] }, { roomType: ['c'] }]))
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'Room_Toolbox_Layout_Invalid');
expect(res.body).to.have.property('errorType', 'error-setting-validation-failed');
});
});
it('should reject an entry repeating a room type', async () => {
await saveLayout(JSON.stringify([{ roomType: ['c', 'c'] }]))
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'Room_Toolbox_Layout_Invalid');
expect(res.body).to.have.property('errorType', 'error-setting-validation-failed');
});
});
it('should reject an entry carrying an unknown key', async () => {
await saveLayout(JSON.stringify([{ roomType: ['c'], somethingElse: true }]))
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'Room_Toolbox_Layout_Invalid');
expect(res.body).to.have.property('errorType', 'error-setting-validation-failed');
});
});
it('should accept a document matching the schema', async () => {
await saveLayout(JSON.stringify(validLayout))
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
});
});
it('should accept an empty value, leaving the setting unconfigured', async () => {
await saveLayout('')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
});
});
});
describe('[/service.configurations]', () => {
it('should return service configurations', async () => {
const res = await request.get(api('service.configurations')).set(credentials).expect('Content-Type', 'application/json').expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('configurations');
});
describe('With OAuth enabled', () => {
before(() => updateSetting('Accounts_OAuth_Google', true));
after(() => updateSetting('Accounts_OAuth_Google', false));
it('should include the OAuth service in the response', (done) => {
// wait 3 seconds before getting the service list so the server has had time to update it
setTimeout(() => {
void request
.get(api('service.configurations'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('configurations');
expect((res.body.configurations as LoginServiceConfiguration[]).find(({ service }) => service === 'google')).to.exist;
})
.end(done);
}, 3000);
});
});
describe('With OAuth disabled', () => {
before(() => updateSetting('Accounts_OAuth_Google', false));
after(() => updateSetting('Accounts_OAuth_Google', false));
it('should not include the OAuth service in the response', (done) => {
// wait 3 seconds before getting the service list so the server has had time to update it
setTimeout(() => {
void request
.get(api('service.configurations'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('configurations');
expect((res.body.configurations as LoginServiceConfiguration[]).find(({ service }) => service === 'google')).to.not.exist;
})
.end(done);
}, 3000);
});
});
});
describe('/settings.oauth', () => {
it('should have return list of available oauth services when user is not logged', async () => {
const res = await request.get(api('settings.oauth')).expect('Content-Type', 'application/json').expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('services').and.to.be.an('array');
});
it('should have return list of available oauth services when user is logged', async () => {
const res = await request.get(api('settings.oauth')).set(credentials).expect('Content-Type', 'application/json').expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('services').and.to.be.an('array');
});
});
(IS_EE ? describe : describe.skip)('/audit.settings', () => {
const formatDate = (date: Date) => date.toISOString();
let startDate: Date;
let endDate: Date;
before(async () => {
// Changing these settings to true and then false ensures that we have audited events
// So we can test filtering reliably
startDate = new Date();
await Promise.all([updateSetting('Accounts_AllowAnonymousRead', true), updateSetting('Accounts_AllowDeleteOwnAccount', true)]);
await Promise.all([updateSetting('Accounts_AllowAnonymousRead', false), updateSetting('Accounts_AllowDeleteOwnAccount', false)]);
endDate = new Date();
});
it('should return list of settings changed (no filters)', async () => {
await request
.get(api('audit.settings'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('events').and.to.be.an('array');
expect(res.body.events.length).to.be.greaterThanOrEqual(4);
});
});
it('should return list of settings between date ranges', async () => {
await request
.get(api('audit.settings'))
.query({ start: formatDate(startDate), end: formatDate(endDate) })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('events').and.to.be.an('array');
expect(res.body.events.length).to.be.greaterThanOrEqual(4);
(res.body.events as IServerEvents['settings.changed'][]).forEach((event) => {
expect(new Date(event.ts).getTime()).to.be.greaterThanOrEqual(startDate.getTime());
expect(new Date(event.ts).getTime()).to.be.lessThanOrEqual(endDate.getTime());
});
});
});
it('should throw error when sending invalid dates', async () => {
const startDate = new Date();
const endDate = 'abcdef';
await request
.get(api('audit.settings'))
.query({ start: formatDate(startDate), end: endDate })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'The "end" query parameter must be a valid date.');
});
});
it('should return list of settings changed filtered by an actor', async () => {
await request
.get(api('audit.settings'))
.query({ actor: { type: 'user' }, count: 10 })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('events').and.to.be.an('array');
expect(res.body.events.length).to.be.greaterThanOrEqual(1);
(res.body.events as IServerEvents['settings.changed'][]).forEach((sEvent) => expect(sEvent.actor.type).to.be.equal('user'));
});
});
it('should return list of changes of an specific setting', async () => {
await request
.get(api('audit.settings'))
.query({ settingId: 'Site_Url' })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('events').and.to.be.an('array');
(res.body.events as IServerEvents['settings.changed'][]).forEach((event) => {
expect(event.data[0].key).to.be.equal('id');
expect(event.data[0].value).to.be.equal('Site_Url');
});
});
});
it('should return list of changes filtered by an actor between date ranges', async () => {
await request
.get(api('audit.settings'))
.query({ actor: { type: 'user' }, start: formatDate(startDate), end: formatDate(endDate) })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('events').and.to.be.an('array');
expect(res.body.events.length).to.be.greaterThanOrEqual(4);
(res.body.events as IServerEvents['settings.changed'][]).forEach((event) => {
expect(event.actor.type).to.be.equal('user');
expect(new Date(event.ts).getTime()).to.be.greaterThanOrEqual(startDate.getTime());
expect(new Date(event.ts).getTime()).to.be.lessThanOrEqual(endDate.getTime());
});
});
});
describe('Masking sensitive settings', () => {
let originalSmtpPassword: string | undefined;
let originalSmtpUsername: string | undefined;
let maskingStart: Date;
let maskingEnd: Date;
const testPassword = 'testpassword123';
const testUsername = 'testuser@example.com';
before(async () => {
maskingStart = new Date();
originalSmtpPassword = (await getSettingValueById('SMTP_Password')) as string | undefined;
originalSmtpUsername = (await getSettingValueById('SMTP_Username')) as string | undefined;
await updateSetting('SMTP_Password', testPassword);
await updateSetting('SMTP_Username', testUsername);
maskingEnd = new Date();
});
after(async () => {
await updateSetting('SMTP_Password', originalSmtpPassword || '');
await updateSetting('SMTP_Username', originalSmtpUsername || '');
});
it('should mask sensitive settings in audit logs', async () => {
await request
.get(api('audit.settings'))
.query({ settingId: 'SMTP_Password', start: formatDate(maskingStart), end: formatDate(maskingEnd) })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('events').and.to.be.an('array');
expect(res.body.events.length).to.be.greaterThanOrEqual(1);
const passwordEvents = res.body.events as IServerEvents['settings.changed'][];
const passwordEvent = passwordEvents.find((event) => {
const settingId = event.data.find((item) => item.key === 'id')?.value;
return settingId === 'SMTP_Password';
});
expect(passwordEvent).to.exist;
if (passwordEvent) {
const previous = passwordEvent.data.find((item) => item.key === 'previous')?.value;
const current = passwordEvent.data.find((item) => item.key === 'current')?.value;
if (previous && typeof previous === 'string' && previous.length > 0) {
expect(previous).to.include('*');
expect(previous).to.not.equal(testPassword);
if (previous.length > 8) {
expect(previous.substring(0, 3)).to.equal(originalSmtpPassword?.substring(0, 3));
expect(previous.substring(3)).to.match(/^\*+$/);
}
}
if (current && typeof current === 'string' && current.length > 0) {
expect(current).to.include('*');
expect(current).to.not.equal(testPassword);
if (testPassword.length > 8) {
expect(current.substring(0, 3)).to.equal(testPassword.substring(0, 3));
expect(current.substring(3)).to.match(/^\*+$/);
} else {
expect(current).to.match(/^\*+$/);
}
}
}
});
await request
.get(api('audit.settings'))
.query({ settingId: 'SMTP_Username', start: formatDate(maskingStart), end: formatDate(maskingEnd) })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('events').and.to.be.an('array');
expect(res.body.events.length).to.be.greaterThanOrEqual(1);
const usernameEvents = res.body.events as IServerEvents['settings.changed'][];
const usernameEvent = usernameEvents.find((event) => {
const settingId = event.data.find((item) => item.key === 'id')?.value;
return settingId === 'SMTP_Username';
});
expect(usernameEvent).to.exist;
if (usernameEvent) {
const previous = usernameEvent.data.find((item) => item.key === 'previous')?.value;
const current = usernameEvent.data.find((item) => item.key === 'current')?.value;
if (previous && typeof previous === 'string' && previous.length > 0) {
expect(previous).to.include('*');
expect(previous).to.not.equal(originalSmtpUsername);
}
if (current && typeof current === 'string' && current.length > 0) {
expect(current).to.include('*');
expect(current).to.not.equal(testUsername);
if (testUsername.length > 8) {
expect(current.substring(0, 3)).to.equal(testUsername.substring(0, 3));
expect(current.substring(3)).to.match(/^\*+$/);
} else {
expect(current).to.match(/^\*+$/);
}
}
}
});
});
});
describe('Sorting', () => {
const isSortedAscending = (events: IServerEvents['settings.changed'][]) =>
events.every((event, i) => i === 0 || new Date(event.ts).getTime() >= new Date(events[i - 1].ts).getTime());
const isSortedDescending = (events: IServerEvents['settings.changed'][]) =>
events.every((event, i) => i === 0 || new Date(event.ts).getTime() <= new Date(events[i - 1].ts).getTime());
it('should sort by timestamp ascending', async () => {
const response = await request
.get(api('audit.settings'))
.query({ sort: JSON.stringify({ ts: 1 }), start: formatDate(startDate), end: formatDate(endDate) })
.set(credentials)
.expect(200);
expect(response.body).to.have.property('success', true);
expect(response.body.events).to.have.length.greaterThanOrEqual(2);
expect(isSortedAscending(response.body.events)).to.be.true;
});
it('should sort by timestamp descending', async () => {
const response = await request
.get(api('audit.settings'))
.query({ sort: JSON.stringify({ ts: -1 }), start: formatDate(startDate), end: formatDate(endDate) })
.set(credentials)
.expect(200);
expect(response.body).to.have.property('success', true);
expect(response.body.events).to.have.length.greaterThanOrEqual(2);
expect(isSortedDescending(response.body.events)).to.be.true;
});
it('should sort by timestamp descending by default', async () => {
const response = await request.get(api('audit.settings')).set(credentials).expect(200);
expect(response.body).to.have.property('success', true);
expect(response.body.events).to.have.length.greaterThanOrEqual(2);
expect(isSortedDescending(response.body.events)).to.be.true;
});
});
});
describe('Custom OAuth admin endpoints', () => {
const oauthName = 'apitest';
const enableSettingId = 'Accounts_OAuth_Custom-Apitest';
before(() => updatePermission('add-oauth-service', ['admin']));
// guarantee the permission is restored even if a permission-denied assertion fails mid-test,
// so a failure cannot cascade into the following cases
afterEach(() => updatePermission('add-oauth-service', ['admin']));
after(async () => {
await updatePermission('add-oauth-service', ['admin']);
// best-effort cleanup in case a test left the custom service behind
await request.post(api('settings.removeCustomOAuth')).set(credentials).send({ name: oauthName });
});
describe('[/settings.addCustomOAuth]', () => {
it('should fail when unauthenticated', () => request.post(api('settings.addCustomOAuth')).send({ name: oauthName }).expect(401));
it('should fail when the "name" param is not provided', () =>
request.post(api('settings.addCustomOAuth')).set(credentials).send({}).expect(400));
it('should fail when the user does not have the add-oauth-service permission', async () => {
await updatePermission('add-oauth-service', []);
await request
.post(api('settings.addCustomOAuth'))
.set(credentials)
.send({ name: oauthName })
.expect((res) => {
expect(res.status).to.equal(403);
expect(res.body).to.have.property('success', false);
});
});
it('should add a custom oauth service', async () => {
await request
.post(api('settings.addCustomOAuth'))
.set(credentials)
.send({ name: oauthName })
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
});
const value = await getSettingValueById(enableSettingId);
expect(value).to.be.a('boolean');
});
});
describe('[/settings.refreshOAuthServices]', () => {
it('should fail when unauthenticated', () => request.post(api('settings.refreshOAuthServices')).expect(401));
it('should fail when the user does not have the add-oauth-service permission', async () => {
await updatePermission('add-oauth-service', []);
await request
.post(api('settings.refreshOAuthServices'))
.set(credentials)
.expect((res) => {
expect(res.status).to.equal(403);
expect(res.body).to.have.property('success', false);
});
});
it('should refresh the oauth services', () =>
request
.post(api('settings.refreshOAuthServices'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
}));
});
describe('[/settings.removeCustomOAuth]', () => {
it('should fail when unauthenticated', () => request.post(api('settings.removeCustomOAuth')).send({ name: oauthName }).expect(401));
it('should fail when the "name" param is not provided', () =>
request.post(api('settings.removeCustomOAuth')).set(credentials).send({}).expect(400));
it('should fail when the "name" normalizes to an empty string', () =>
request
.post(api('settings.removeCustomOAuth'))
.set(credentials)
.send({ name: '!!!' })
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
}));
it('should fail when the user does not have the add-oauth-service permission', async () => {
await updatePermission('add-oauth-service', []);
await request
.post(api('settings.removeCustomOAuth'))
.set(credentials)
.send({ name: oauthName })
.expect((res) => {
expect(res.status).to.equal(403);
expect(res.body).to.have.property('success', false);
});
});
it('should remove a custom oauth service', async () => {
await request
.post(api('settings.removeCustomOAuth'))
.set(credentials)
.send({ name: oauthName })
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
});
// the provider settings should be gone: settings/:_id answers 400 for an unknown id
await request.get(`/api/v1/settings/${enableSettingId}`).set(credentials).expect(400);
});
});
});
});

View File

@@ -0,0 +1,87 @@
import type { ISetting } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import type { Response } from 'supertest';
import { getCredentials, api, request, credentials } from '../../data/api-data';
import { getSettingValueById, updateSetting } from '../../data/permissions.helper';
describe('[SetupWizard]', function () {
this.retries(0);
before((done) => getCredentials(done));
describe('[/setupWizard.parameters]', () => {
it('should return the wizard parameters without authentication', async () => {
return request
.get(api('setupWizard.parameters'))
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('serverAlreadyRegistered').that.is.a('boolean');
expect(res.body).to.have.property('settings').that.is.an('array');
});
});
it('should only return settings that belong to the wizard', async () => {
return request
.get(api('setupWizard.parameters'))
.expect(200)
.expect((res: Response) => {
expect(res.body.settings).to.not.be.empty;
res.body.settings.forEach((setting: Record<string, unknown>) => {
expect(setting).to.have.property('wizard');
expect(setting).to.have.property('_id');
});
});
});
it('should return the wizard parameters when authenticated', async () => {
return request
.get(api('setupWizard.parameters'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('settings').that.is.an('array');
});
});
describe('when the setup wizard is already completed', () => {
let previousState: ISetting['value'];
before(async () => {
previousState = await getSettingValueById('Show_Setup_Wizard');
await updateSetting('Show_Setup_Wizard', 'completed');
});
after(async () => updateSetting('Show_Setup_Wizard', previousState));
it('should not expose the wizard parameters to unauthenticated requests', async () => {
return request
.get(api('setupWizard.parameters'))
.expect('Content-Type', 'application/json')
.expect(403)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.not.have.property('settings');
expect(res.body).to.not.have.property('serverAlreadyRegistered');
});
});
it('should not expose the wizard parameters to authenticated requests', async () => {
return request
.get(api('setupWizard.parameters'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(403)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.not.have.property('settings');
});
});
});
});
});

View File

@@ -0,0 +1,334 @@
import type { Credentials } from '@rocket.chat/api-client';
import type { ISidebarCategory, IUser } from '@rocket.chat/core-typings';
import { Random } from '@rocket.chat/random';
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import { getCredentials, api, request, credentials } from '../../data/api-data';
import { createRoom, deleteRoom, getSubscriptionByRoomId, addUserToRoom } from '../../data/rooms.helper';
import { password } from '../../data/user';
import { createUser, login, deleteUser } from '../../data/users.helper';
import { IS_EE } from '../../e2e/config/constants';
const experimentalEndpoint = (path: string) => `/api/experimental/${path}`;
/**
* Persistence + validation of the `sidebarCategories` user preference (custom sidebar categories).
*
* The feature has no dedicated endpoint — categories are stored on the user preferences and round-trip
* through `users.setPreferences` / `users.getPreferences`. The REST schema validates each entry with
* `required: ['_id', 'name']` and `additionalProperties: false`, so malformed payloads are rejected.
*/
describe('[Sidebar Custom Categories]', () => {
let testUser: IUser;
let testUserCredentials: Credentials;
const category = (overrides: Partial<ISidebarCategory> = {}): ISidebarCategory => ({
_id: Random.id(),
name: `category-${Random.id()}`,
...overrides,
});
before((done) => getCredentials(done));
before(async () => {
testUser = await createUser();
testUserCredentials = await login(testUser.username, password);
});
after(() => deleteUser(testUser));
const setCategories = (data: unknown, asCredentials: Credentials = testUserCredentials) =>
request
.post(api('users.setPreferences'))
.set(asCredentials)
.send({ data: { sidebarCategories: data } });
describe('persistence', () => {
it('should persist a minimal category (only the required _id + name)', async () => {
const categories = [category({ name: 'Projects' })];
await setCategories(categories)
.expect(200)
.expect('Content-Type', 'application/json')
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body.user.settings.preferences).to.have.property('sidebarCategories').that.is.an('array').with.lengthOf(1);
});
await request
.get(api('users.getPreferences'))
.set(testUserCredentials)
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body.preferences.sidebarCategories).to.deep.equal(categories);
});
});
it('should persist the optional showUnreads field verbatim', async () => {
const categories = [category({ name: 'Team', showUnreads: false }), category({ name: 'Personal', showUnreads: true })];
await setCategories(categories).expect(200);
await request
.get(api('users.getPreferences'))
.set(testUserCredentials)
.expect(200)
.expect((res) => {
expect(res.body.preferences.sidebarCategories).to.deep.equal(categories);
});
});
it('should preserve the array order (order is the render order)', async () => {
const categories = [category({ name: 'A' }), category({ name: 'B' }), category({ name: 'C' })];
await setCategories(categories).expect(200);
await request
.get(api('users.getPreferences'))
.set(testUserCredentials)
.expect(200)
.expect((res) => {
expect(res.body.preferences.sidebarCategories.map((c: ISidebarCategory) => c.name)).to.deep.equal(['A', 'B', 'C']);
});
});
it('should replace the whole array on each write (read-modify-write semantics)', async () => {
await setCategories([category({ name: 'first' })]).expect(200);
const replacement = [category({ name: 'second' })];
await setCategories(replacement).expect(200);
await request
.get(api('users.getPreferences'))
.set(testUserCredentials)
.expect(200)
.expect((res) => {
expect(res.body.preferences.sidebarCategories).to.have.lengthOf(1);
expect(res.body.preferences.sidebarCategories[0]).to.have.property('name', 'second');
});
});
it('should accept an empty array (clearing all categories)', async () => {
await setCategories([category({ name: 'temp' })]).expect(200);
await setCategories([]).expect(200);
await request
.get(api('users.getPreferences'))
.set(testUserCredentials)
.expect(200)
.expect((res) => {
expect(res.body.preferences.sidebarCategories).to.be.an('array').with.lengthOf(0);
});
});
});
describe('validation', () => {
const expectInvalid = (res: { body: { success: boolean; errorType: string } }) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'invalid-params');
};
// NOTE: the shared ajv instance runs with `coerceTypes`, so a scalar type mismatch on a coercible
// field (e.g. a numeric `name`, or a numeric entry in `rooms`) is coerced to the schema type and
// accepted rather than rejected. The assertions below therefore cover the constraints coercion
// cannot satisfy: missing required fields, unknown properties, a non-array root, and a value that
// cannot be coerced to the target type.
it('should reject an entry missing the required "name"', async () => {
await setCategories([{ _id: Random.id() }])
.expect(400)
.expect(expectInvalid);
});
it('should reject an entry missing the required "_id"', async () => {
await setCategories([{ name: 'no-id' }])
.expect(400)
.expect(expectInvalid);
});
it('should reject an unknown property on an entry (additionalProperties: false)', async () => {
await setCategories([{ _id: Random.id(), name: 'x', color: 'red' }])
.expect(400)
.expect(expectInvalid);
});
it('should reject a non-array value', async () => {
await setCategories({ _id: Random.id(), name: 'not-an-array' }).expect(400).expect(expectInvalid);
});
it('should reject a "showUnreads" that cannot be coerced to a boolean', async () => {
await setCategories([{ _id: Random.id(), name: 'x', showUnreads: 'yes' }])
.expect(400)
.expect(expectInvalid);
});
it('should return 401 when not authenticated', async () => {
await request
.post(api('users.setPreferences'))
.send({ data: { sidebarCategories: [] } })
.expect(401);
});
});
describe('permissions', () => {
it("should let an admin set another user's sidebarCategories", async () => {
const categories = [category({ name: 'set-by-admin' })];
await request
.post(api('users.setPreferences'))
.set(credentials)
.send({ userId: testUser._id, data: { sidebarCategories: categories } })
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
});
await request
.get(api('users.getPreferences'))
.set(testUserCredentials)
.expect(200)
.expect((res) => {
expect(res.body.preferences.sidebarCategories).to.deep.equal(categories);
});
});
});
(IS_EE ? describe : describe.skip)('[Experimental] rooms.setCategory', () => {
let catId: string;
let roomId: string;
let roomId2: string;
const setCategory = (body: object, creds: Credentials = testUserCredentials) =>
request.post(experimentalEndpoint('rooms.setCategory')).set(creds).send(body);
before(async () => {
catId = Random.id();
await setCategories([{ _id: catId, name: 'E2E Cat' }]);
// Create rooms as admin (regular users lack create-c permission by default),
// then invite the test user so they are subscribed.
const r1 = await createRoom({ type: 'c', name: `setcat-${Random.id()}` }).expect(200);
roomId = r1.body.channel._id;
await addUserToRoom({ usernames: [testUser.username as string], rid: roomId, type: 'c' });
const r2 = await createRoom({ type: 'c', name: `setcat-${Random.id()}` }).expect(200);
roomId2 = r2.body.channel._id;
await addUserToRoom({ usernames: [testUser.username as string], rid: roomId2, type: 'c' });
});
after(async () => {
await setCategories([]);
await Promise.all([
...(roomId ? [deleteRoom({ type: 'c', roomId })] : []),
...(roomId2 ? [deleteRoom({ type: 'c', roomId: roomId2 })] : []),
]);
});
it('should assign a room to a category and persist it on the subscription', async () => {
await setCategory({ roomIds: [roomId], category: catId })
.expect(200)
.expect((res) => expect(res.body).to.have.property('success', true));
const sub = await getSubscriptionByRoomId(roomId, testUserCredentials);
expect(sub).to.have.property('category', catId);
});
it('should unassign a room from its category when category is null', async () => {
await setCategory({ roomIds: [roomId], category: catId }).expect(200);
await setCategory({ roomIds: [roomId], category: null }).expect(200);
const sub = await getSubscriptionByRoomId(roomId, testUserCredentials);
expect(sub).to.not.have.property('category');
});
it('should assign multiple rooms to a category in one call', async () => {
await setCategory({ roomIds: [roomId, roomId2], category: catId }).expect(200);
const [s1, s2] = await Promise.all([
getSubscriptionByRoomId(roomId, testUserCredentials),
getSubscriptionByRoomId(roomId2, testUserCredentials),
]);
expect(s1).to.have.property('category', catId);
expect(s2).to.have.property('category', catId);
});
it('should reject duplicate roomIds', async () => {
await setCategory({ roomIds: [roomId, roomId], category: catId }).expect(400);
});
it('should return 401 when not authenticated', async () => {
await request
.post(experimentalEndpoint('rooms.setCategory'))
.send({ roomIds: [roomId], category: catId })
.expect(401);
});
describe('validation', () => {
it('should reject a missing roomIds field', async () => {
await setCategory({ category: catId }).expect(400);
});
it('should reject an empty roomIds array (minItems: 1)', async () => {
await setCategory({ roomIds: [], category: catId }).expect(400);
});
it('should reject a missing category field', async () => {
await setCategory({ roomIds: [roomId] }).expect(400);
});
it('should reject unknown properties (additionalProperties: false)', async () => {
await setCategory({ roomIds: [roomId], category: catId, extra: true }).expect(400);
});
});
describe('business logic', () => {
it('should reject a category id not found in the user preferences', async () => {
await setCategory({ roomIds: [roomId], category: Random.id() })
.expect(400)
.expect((res) => expect(res.body).to.have.property('success', false));
});
it('should silently skip a room the user is not subscribed to', async () => {
const adminRoom = await createRoom({ type: 'c', name: `admin-only-${Random.id()}` }).expect(200);
const adminRoomId = adminRoom.body.channel._id;
try {
await setCategory({ roomIds: [adminRoomId], category: catId }).expect(200);
// The subscription for adminRoom belongs to the admin, not testUser — setCategoryByRoomIdsAndUserId
// filters by userId, so the unsubscribed room is silently ignored.
const sub = await getSubscriptionByRoomId(adminRoomId, credentials);
expect(sub).to.not.have.property('category');
} finally {
await deleteRoom({ type: 'c', roomId: adminRoomId });
}
});
it('should remove a room from favorites when assigning it to a category', async () => {
// First favorite the room
await request.post(api('rooms.favorite')).set(testUserCredentials).send({ roomId, favorite: true }).expect(200);
let sub = await getSubscriptionByRoomId(roomId, testUserCredentials);
expect(sub).to.have.property('f', true);
expect(sub).to.not.have.property('category');
// Now assign it to a category — server atomically unsets f
await setCategory({ roomIds: [roomId], category: catId }).expect(200);
sub = await getSubscriptionByRoomId(roomId, testUserCredentials);
expect(sub).to.have.property('category', catId);
expect(sub).to.have.property('f', false);
});
it('should remove a room from its category when favoriting it', async () => {
// First assign the room to a category
await setCategory({ roomIds: [roomId], category: catId }).expect(200);
let sub = await getSubscriptionByRoomId(roomId, testUserCredentials);
expect(sub).to.have.property('category', catId);
// Now favorite it — server atomically unsets category
await request.post(api('rooms.favorite')).set(testUserCredentials).send({ roomId, favorite: true }).expect(200);
sub = await getSubscriptionByRoomId(roomId, testUserCredentials);
expect(sub).to.have.property('f', true);
expect(sub).to.not.have.property('category');
});
});
});
});

View File

@@ -0,0 +1,86 @@
import { expect } from 'chai';
import { before, describe, it, after } from 'mocha';
import { getCredentials, api, request, credentials } from '../../data/api-data';
import { updatePermission } from '../../data/permissions.helper';
describe('[Statistics]', () => {
before((done) => getCredentials(done));
after(() => updatePermission('view-statistics', ['admin']));
describe('[/statistics]', () => {
let lastUptime: unknown;
it('should return an error when the user does not have the necessary permission', async () => {
await updatePermission('view-statistics', []);
const res = await request.get(api('statistics')).set(credentials).expect(400);
expect(res.body).to.have.property('success', false);
expect(res.body.error).to.be.equal('error-not-allowed');
});
it('should return an object with the statistics', async () => {
await updatePermission('view-statistics', ['admin']);
const res = await request.get(api('statistics')).set(credentials).expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('process');
expect(res.body.process).to.have.property('uptime');
lastUptime = res.body.process.uptime;
});
it('should update the statistics when is provided the "refresh:true" query parameter', async () => {
const res = await request.get(api('statistics')).query({ refresh: 'true' }).set(credentials).expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('process');
expect(res.body.process).to.have.property('uptime');
expect(lastUptime).to.not.be.equal(res.body.process.uptime);
});
});
describe('[/statistics.list]', () => {
it('should return an error when the user does not have the necessary permission', async () => {
await updatePermission('view-statistics', []);
const res = await request.get(api('statistics.list')).set(credentials).expect(400);
expect(res.body).to.have.property('success', false);
expect(res.body.error).to.be.equal('error-not-allowed');
});
it('should return an array with the statistics', async () => {
await updatePermission('view-statistics', ['admin']);
const res = await request.get(api('statistics.list')).set(credentials).expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('statistics').and.to.be.an('array');
expect(res.body).to.have.property('offset');
expect(res.body).to.have.property('total');
expect(res.body).to.have.property('count');
});
it('should return an array with the statistics even requested with count and offset params', async () => {
await updatePermission('view-statistics', ['admin']);
const res = await request
.get(api('statistics.list'))
.set(credentials)
.query({
count: 5,
offset: 0,
})
.expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('statistics').and.to.be.an('array');
expect(res.body).to.have.property('offset');
expect(res.body).to.have.property('total');
expect(res.body).to.have.property('count');
});
});
});

View File

@@ -0,0 +1,373 @@
import type { Credentials } from '@rocket.chat/api-client';
import type { IRoom, ITeam, IUser } from '@rocket.chat/core-typings';
import { TeamType, UserStatus } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import { api, credentials, getCredentials, request } from '../../data/api-data';
import { updateEESetting } from '../../data/permissions.helper';
import { createRoom, deleteRoom } from '../../data/rooms.helper';
import { createTeam, deleteTeam } from '../../data/teams.helper';
import { password } from '../../data/user';
import { createUser, deleteUser, login, setUserStatus } from '../../data/users.helper';
import { IS_EE } from '../../e2e/config/constants';
(IS_EE ? describe : describe.skip)('[Status Visibility] (Enterprise Only)', function () {
this.retries(0);
let hider: IUser & { username: string };
let viewer: IUser & { username: string };
let bystander: IUser & { username: string };
let hiderCredentials: Credentials;
let viewerCredentials: Credentials;
let bystanderCredentials: Credentials;
let channel: IRoom;
let group: IRoom;
let team: ITeam;
const usernamesOf = (members: { username?: string }[]) => members.map((member) => member.username);
const statusOf = (members: { username?: string; status?: string }[], username: string) =>
members.find((member) => member.username === username)?.status;
before((done) => getCredentials(done));
before(async () => {
await updateEESetting('Accounts_StatusVisibility_Enabled', true);
[hider, viewer, bystander] = (await Promise.all([
createUser({ joinDefaultChannels: false }),
createUser({ joinDefaultChannels: false }),
createUser({ joinDefaultChannels: false }),
])) as (IUser & { username: string })[];
[hiderCredentials, viewerCredentials, bystanderCredentials] = await Promise.all([
login(hider.username, password),
login(viewer.username, password),
login(bystander.username, password),
]);
await setUserStatus(hiderCredentials, UserStatus.ONLINE);
await request
.post(api('users.setPreferences'))
.set(hiderCredentials)
.send({ data: { statusVisibilityDenied: [viewer.username] } })
.expect(200);
const members = [hider.username, viewer.username, bystander.username];
channel = (await createRoom({ type: 'c', name: `status-visibility-c-${Date.now()}`, members, credentials })).body.channel;
group = (await createRoom({ type: 'p', name: `status-visibility-p-${Date.now()}`, members, credentials })).body.group;
team = await createTeam(credentials, `status-visibility-t-${Date.now()}`, TeamType.PUBLIC, members);
});
after(async () => {
await Promise.all([deleteRoom({ type: 'c', roomId: channel._id }), deleteRoom({ type: 'p', roomId: group._id })]);
await deleteTeam(credentials, team.name);
await Promise.all([deleteUser(hider), deleteUser(viewer), deleteUser(bystander)]);
await updateEESetting('Accounts_StatusVisibility_Enabled', false);
});
describe('[/rooms.membersOrderedByRole]', () => {
it('should not return the hider when the viewer filters by online', async () => {
await request
.get(api('rooms.membersOrderedByRole'))
.set(viewerCredentials)
.query({ 'roomId': channel._id, 'status[]': UserStatus.ONLINE })
.expect(200)
.expect((res) => {
expect(usernamesOf(res.body.members)).to.not.include(hider.username);
expect(res.body.total).to.be.equal(res.body.members.length);
});
});
it('should return the hider as online to a user they do not hide from', async () => {
await request
.get(api('rooms.membersOrderedByRole'))
.set(bystanderCredentials)
.query({ 'roomId': channel._id, 'status[]': UserStatus.ONLINE })
.expect(200)
.expect((res) => {
expect(statusOf(res.body.members, hider.username)).to.be.equal(UserStatus.ONLINE);
});
});
it('should return the hider as offline when the viewer does not filter', async () => {
await request
.get(api('rooms.membersOrderedByRole'))
.set(viewerCredentials)
.query({ roomId: channel._id })
.expect(200)
.expect((res) => {
expect(usernamesOf(res.body.members)).to.include(hider.username);
expect(statusOf(res.body.members, hider.username)).to.be.equal(UserStatus.OFFLINE);
});
});
it('should return the hider when the viewer filters by offline', async () => {
await request
.get(api('rooms.membersOrderedByRole'))
.set(viewerCredentials)
.query({ 'roomId': channel._id, 'status[]': UserStatus.OFFLINE })
.expect(200)
.expect((res) => {
expect(usernamesOf(res.body.members)).to.include(hider.username);
expect(statusOf(res.body.members, hider.username)).to.be.equal(UserStatus.OFFLINE);
});
});
});
describe('[/channels.members]', () => {
it('should not return the hider when the viewer filters by online', async () => {
await request
.get(api('channels.members'))
.set(viewerCredentials)
.query({ 'roomId': channel._id, 'status[]': UserStatus.ONLINE })
.expect(200)
.expect((res) => {
expect(usernamesOf(res.body.members)).to.not.include(hider.username);
expect(res.body.total).to.be.equal(res.body.members.length);
});
});
it('should return the hider as online to a user they do not hide from', async () => {
await request
.get(api('channels.members'))
.set(bystanderCredentials)
.query({ 'roomId': channel._id, 'status[]': UserStatus.ONLINE })
.expect(200)
.expect((res) => {
expect(statusOf(res.body.members, hider.username)).to.be.equal(UserStatus.ONLINE);
});
});
});
describe('[/channels.online]', () => {
it('should not return the hider to the viewer', async () => {
await request
.get(api('channels.online'))
.set(viewerCredentials)
.query({ _id: channel._id })
.expect(200)
.expect((res) => {
expect(usernamesOf(res.body.online)).to.not.include(hider.username);
});
});
it('should return the hider to a user they do not hide from', async () => {
await request
.get(api('channels.online'))
.set(bystanderCredentials)
.query({ _id: channel._id })
.expect(200)
.expect((res) => {
expect(usernamesOf(res.body.online)).to.include(hider.username);
});
});
});
describe('[/groups.members]', () => {
it('should not return the hider when the viewer filters by online', async () => {
await request
.get(api('groups.members'))
.set(viewerCredentials)
.query({ 'roomId': group._id, 'status[]': UserStatus.ONLINE })
.expect(200)
.expect((res) => {
expect(usernamesOf(res.body.members)).to.not.include(hider.username);
expect(res.body.total).to.be.equal(res.body.members.length);
});
});
it('should return the hider as online to a user they do not hide from', async () => {
await request
.get(api('groups.members'))
.set(bystanderCredentials)
.query({ 'roomId': group._id, 'status[]': UserStatus.ONLINE })
.expect(200)
.expect((res) => {
expect(statusOf(res.body.members, hider.username)).to.be.equal(UserStatus.ONLINE);
});
});
});
describe('[/groups.online]', () => {
it('should not return the hider to the viewer', async () => {
await request
.get(api('groups.online'))
.set(viewerCredentials)
.query({ _id: group._id })
.expect(200)
.expect((res) => {
expect(usernamesOf(res.body.online)).to.not.include(hider.username);
});
});
it('should return the hider to a user they do not hide from', async () => {
await request
.get(api('groups.online'))
.set(bystanderCredentials)
.query({ _id: group._id })
.expect(200)
.expect((res) => {
expect(usernamesOf(res.body.online)).to.include(hider.username);
});
});
});
describe('[/teams.members]', () => {
const teamUsernamesOf = (members: { user: IUser }[]) => members.map(({ user }) => user.username);
it('should not return the hider when the viewer filters by online', async () => {
await request
.get(api('teams.members'))
.set(viewerCredentials)
.query({ 'teamId': team._id, 'status[]': UserStatus.ONLINE })
.expect(200)
.expect((res) => {
expect(teamUsernamesOf(res.body.members)).to.not.include(hider.username);
});
});
it('should return the hider to a user they do not hide from', async () => {
await request
.get(api('teams.members'))
.set(bystanderCredentials)
.query({ 'teamId': team._id, 'status[]': UserStatus.ONLINE })
.expect(200)
.expect((res) => {
expect(teamUsernamesOf(res.body.members)).to.include(hider.username);
});
});
it('should not expose the block list of any member', async () => {
await request
.get(api('teams.members'))
.set(viewerCredentials)
.query({ teamId: team._id })
.expect(200)
.expect((res) => {
res.body.members.forEach(({ user }: { user: IUser }) => {
expect(user.settings?.preferences ?? {}).to.not.have.property('statusVisibilityDenied');
});
});
});
});
describe('[/users.getStatus]', () => {
it('should report the hider as offline to the viewer', async () => {
await request
.get(api('users.getStatus'))
.set(viewerCredentials)
.query({ userId: hider._id })
.expect(200)
.expect((res) => {
expect(res.body.status).to.be.equal(UserStatus.OFFLINE);
expect(res.body).to.not.have.property('statusSource');
});
});
it('should report the real status to a user they do not hide from', async () => {
await request
.get(api('users.getStatus'))
.set(bystanderCredentials)
.query({ userId: hider._id })
.expect(200)
.expect((res) => {
expect(res.body.status).to.be.equal(UserStatus.ONLINE);
});
});
});
describe('[/users.getPresence]', () => {
it('should report the hider as offline to the viewer', async () => {
await request
.get(api('users.getPresence'))
.set(viewerCredentials)
.query({ userId: hider._id })
.expect(200)
.expect((res) => {
expect(res.body.presence).to.be.equal(UserStatus.OFFLINE);
});
});
it('should report the real presence to a user they do not hide from', async () => {
await request
.get(api('users.getPresence'))
.set(bystanderCredentials)
.query({ userId: hider._id })
.expect(200)
.expect((res) => {
expect(res.body.presence).to.be.equal(UserStatus.ONLINE);
});
});
});
describe('[/users.autocomplete]', () => {
const selector = (conditions?: object) => JSON.stringify({ term: hider.username, exceptions: [], ...(conditions && { conditions }) });
it('should list the hider as offline to the viewer', async () => {
await request
.get(api('users.autocomplete'))
.set(viewerCredentials)
.query({ selector: selector() })
.expect(200)
.expect((res) => {
expect(statusOf(res.body.items, hider.username)).to.be.equal(UserStatus.OFFLINE);
});
});
it('should list the real status to a user they do not hide from', async () => {
await request
.get(api('users.autocomplete'))
.set(bystanderCredentials)
.query({ selector: selector() })
.expect(200)
.expect((res) => {
expect(statusOf(res.body.items, hider.username)).to.be.equal(UserStatus.ONLINE);
});
});
it('should not return the hider when the viewer filters by online through the selector conditions', async () => {
await request
.get(api('users.autocomplete'))
.set(viewerCredentials)
.query({ selector: selector({ status: UserStatus.ONLINE }) })
.expect(200)
.expect((res) => {
expect(usernamesOf(res.body.items)).to.not.include(hider.username);
});
});
it('should return the hider through the selector conditions to a user they do not hide from', async () => {
await request
.get(api('users.autocomplete'))
.set(bystanderCredentials)
.query({ selector: selector({ status: UserStatus.ONLINE }) })
.expect(200)
.expect((res) => {
expect(usernamesOf(res.body.items)).to.include(hider.username);
});
});
});
describe('[/im.members]', () => {
let dm: IRoom;
before(async () => {
dm = (await createRoom({ type: 'd', username: hider.username, credentials: viewerCredentials })).body.room;
});
it('should return the hider when the viewer filters by offline', async () => {
await request
.get(api('im.members'))
.set(viewerCredentials)
.query({ 'roomId': dm._id, 'status[]': UserStatus.OFFLINE })
.expect(200)
.expect((res) => {
expect(usernamesOf(res.body.members)).to.include(hider.username);
expect(statusOf(res.body.members, hider.username)).to.be.equal(UserStatus.OFFLINE);
});
});
});
});

View File

@@ -0,0 +1,432 @@
import type { Credentials } from '@rocket.chat/api-client';
import type { IRoom, IThreadMessage, IUser } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import { getCredentials, api, request, credentials } from '../../data/api-data';
import { createRoom, deleteRoom } from '../../data/rooms.helper';
import { adminUsername } from '../../data/user';
import type { TestUser } from '../../data/users.helper';
import { createUser, deleteUser, login } from '../../data/users.helper';
describe('[Subscriptions]', () => {
before((done) => getCredentials(done));
let testChannel: IRoom;
before(async () => {
testChannel = (await createRoom({ type: 'c', name: `channel.test.${Date.now()}` })).body.channel;
});
after(() => deleteRoom({ type: 'c', roomId: testChannel._id }));
it('/subscriptions.get', async () => {
const res = await request.get(api('subscriptions.get')).set(credentials).expect('Content-Type', 'application/json').expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('update');
expect(res.body).to.have.property('remove');
});
it('/subscriptions.get?updatedSince', async () => {
const res = await request
.get(api('subscriptions.get'))
.set(credentials)
.query({
updatedSince: new Date(),
})
.expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('update').that.have.lengthOf(0);
expect(res.body).to.have.property('remove').that.have.lengthOf(0);
});
describe('/subscriptions.getOne', () => {
it('should fail if no roomId provided', async () => {
const res = await request.get(api('subscriptions.getOne')).set(credentials).expect('Content-Type', 'application/json').expect(400);
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', "must have required property 'roomId'");
});
it('should fail if not logged in', async () => {
const res = await request
.get(api('subscriptions.getOne'))
.query({
roomId: testChannel._id,
})
.expect('Content-Type', 'application/json')
.expect(401);
expect(res.body).to.have.property('status', 'error');
expect(res.body).to.have.property('message');
});
it('should return the subscription with success', async () => {
const res = await request
.get(api('subscriptions.getOne'))
.set(credentials)
.query({
roomId: testChannel._id,
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('subscription').and.to.be.an('object');
});
it('should keep subscription as read after sending a message', async () => {
await request
.get(api('subscriptions.getOne'))
.set(credentials)
.query({
roomId: testChannel._id,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('subscription').and.to.be.an('object');
expect(res.body.subscription).to.have.property('alert', false);
});
await request
.post(api('chat.sendMessage'))
.set(credentials)
.send({
message: {
rid: testChannel._id,
msg: 'Sample message',
},
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('message').and.to.be.an('object');
});
await request
.get(api('subscriptions.getOne'))
.set(credentials)
.query({
roomId: testChannel._id,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('subscription').and.to.be.an('object');
expect(res.body.subscription).to.have.property('alert', false);
});
});
});
describe('[/subscriptions.read]', () => {
let testChannel: IRoom;
let testGroup: IRoom;
let testDM: IRoom;
let user: TestUser<IUser>;
before(async () => {
user = await createUser();
testChannel = (await createRoom({ type: 'c', name: `channel.test.${Date.now()}` })).body.channel;
testGroup = (await createRoom({ type: 'p', name: `group.test.${Date.now()}` })).body.group;
testDM = (await createRoom({ type: 'd', username: user.username })).body.room;
});
after(() =>
Promise.all([
deleteRoom({ type: 'd', roomId: testDM._id }),
deleteRoom({ type: 'c', roomId: testChannel._id }),
deleteRoom({ type: 'p', roomId: testGroup._id }),
deleteUser(user),
]),
);
it('should mark public channels as read', async () => {
const res = await request
.post(api('subscriptions.read'))
.set(credentials)
.send({
rid: testChannel._id,
})
.expect(200);
expect(res.body).to.have.property('success', true);
});
it('should mark groups as read', async () => {
const res = await request
.post(api('subscriptions.read'))
.set(credentials)
.send({
rid: testGroup._id,
})
.expect(200);
expect(res.body).to.have.property('success', true);
});
it('should mark DMs as read', async () => {
const res = await request
.post(api('subscriptions.read'))
.set(credentials)
.send({
rid: testDM._id,
})
.expect(200);
expect(res.body).to.have.property('success', true);
});
it('should fail on two params with different ids', async () => {
const res = await request
.post(api('subscriptions.read'))
.set(credentials)
.send({
rid: testDM._id,
roomId: testChannel._id,
})
.expect(400);
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'invalid-params');
});
it('should fail on mark inexistent public channel as read', async () => {
const res = await request
.post(api('subscriptions.read'))
.set(credentials)
.send({
rid: 'foobar123-somechannel',
})
.expect(400);
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'error-invalid-subscription');
});
it('should fail on mark inexistent group as read', async () => {
const res = await request
.post(api('subscriptions.read'))
.set(credentials)
.send({
rid: 'foobar123-somegroup',
})
.expect(400);
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'error-invalid-subscription');
});
it('should fail on mark inexistent DM as read', async () => {
const res = await request
.post(api('subscriptions.read'))
.set(credentials)
.send({
rid: 'foobar123-somedm',
})
.expect(400);
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'error-invalid-subscription');
});
it('should fail on invalid params', async () => {
const res = await request
.post(api('subscriptions.read'))
.set(credentials)
.send({
rid: 12345,
})
.expect(400);
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'error-invalid-subscription');
});
it('should fail on empty params', async () => {
const res = await request.post(api('subscriptions.read')).set(credentials).send({}).expect(400);
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'invalid-params');
});
describe('should handle threads correctly', () => {
let threadId: IThreadMessage['_id'];
let user: TestUser<IUser>;
let threadUserCredentials: Credentials;
before(async () => {
user = await createUser({ username: 'testthread123', password: 'testthread123' });
threadUserCredentials = await login('testthread123', 'testthread123');
const res = await request
.post(api('chat.sendMessage'))
.set(threadUserCredentials)
.send({
message: {
rid: testChannel._id,
msg: 'Starting a Thread',
},
});
threadId = res.body.message._id;
});
after(async () => {
await deleteUser(user);
});
it('should mark threads as read', async () => {
await request
.post(api('chat.sendMessage'))
.set(threadUserCredentials)
.send({
message: {
rid: testChannel._id,
msg: `@${adminUsername} making admin follow this thread`,
tmid: threadId,
},
});
await request
.post(api('subscriptions.read'))
.set(credentials)
.send({
rid: testChannel._id,
readThreads: true,
})
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
});
await request
.get(api('subscriptions.getOne'))
.set(credentials)
.query({
roomId: testChannel._id,
})
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body.subscription).to.not.have.property('tunread');
});
});
it('should not mark threads as read', async () => {
await request
.post(api('chat.sendMessage'))
.set(threadUserCredentials)
.send({
message: {
rid: testChannel._id,
msg: `@${adminUsername} making admin follow this thread`,
tmid: threadId,
},
});
await request
.post(api('subscriptions.read'))
.set(credentials)
.send({
rid: testChannel._id,
readThreads: false,
})
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
});
await request
.get(api('subscriptions.getOne'))
.set(credentials)
.query({
roomId: testChannel._id,
})
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body.subscription).to.have.property('tunread');
expect(res.body.subscription.tunread).to.be.an('array');
expect(res.body.subscription.tunread).to.deep.equal([threadId]);
});
});
});
});
describe('[/subscriptions.unread]', () => {
let testChannel: IRoom;
before(async () => {
testChannel = (await createRoom({ type: 'c', name: `channel.test.${Date.now()}` })).body.channel;
});
after(() => deleteRoom({ type: 'c', roomId: testChannel._id }));
it('should fail when there are no messages on an channel', async () => {
const res = await request
.post(api('subscriptions.unread'))
.set(credentials)
.send({
roomId: testChannel._id,
})
.expect(400);
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error');
expect(res.body).to.have.property('errorType', 'error-no-message-for-unread');
});
it('sending message', async () => {
const res = await request
.post(api('chat.sendMessage'))
.set(credentials)
.send({
message: {
rid: testChannel._id,
msg: 'Sample message',
},
})
.expect('Content-Type', 'application/json')
.expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('message').and.to.be.an('object');
});
it('should return success: true when make as unread successfully', async () => {
const res = await request
.post(api('subscriptions.unread'))
.set(credentials)
.send({
roomId: testChannel._id,
})
.expect(200);
expect(res.body).to.have.property('success', true);
});
it('should fail on invalid params', async () => {
const res = await request
.post(api('subscriptions.unread'))
.set(credentials)
.send({
roomId: 12345,
})
.expect(400);
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error');
});
it('should fail on empty params', async () => {
const res = await request.post(api('subscriptions.unread')).set(credentials).send({}).expect(400);
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error');
});
});
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,39 @@
import { expect } from 'chai';
import { before, describe, it } from 'mocha';
import { getCredentials, api, request, credentials } from '../../data/api-data';
describe('[Webdav]', () => {
before((done) => getCredentials(done));
describe('/webdav.getMyAccounts', () => {
it('should return my webdav accounts', async () => {
const res = await request.get(api('webdav.getMyAccounts')).set(credentials).expect(200);
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('accounts').and.to.be.a('array');
});
});
describe('/webdav.removeWebdavAccount', () => {
it('should return an error when send an invalid request', async () => {
const res = await request.post(api('webdav.removeWebdavAccount')).set(credentials).send({}).expect(400);
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error');
});
it('should return an error when using an invalid account id', async () => {
const res = await request
.post(api('webdav.removeWebdavAccount'))
.set(credentials)
.send({
accountId: {},
})
.expect(400);
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error');
});
});
});

View File

@@ -0,0 +1,33 @@
import type { App } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import { getCredentials, request, credentials } from '../../data/api-data';
import { appAPIParameterTest } from '../../data/apps/app-packages';
import { apps } from '../../data/apps/apps-data';
import { cleanupApps, installLocalTestPackage } from '../../data/apps/helper';
import { IS_EE } from '../../e2e/config/constants';
(IS_EE ? describe : describe.skip)('Apps - API Endpoint Parameter Parsing', () => {
let app: App;
before((done) => getCredentials(done));
before(async () => {
await cleanupApps();
app = await installLocalTestPackage(appAPIParameterTest);
});
after(() => cleanupApps());
it('should correctly parse URL parameters in API endpoint', async () => {
const param1Value = 'test-value-1';
const param2Value = 'test-value-2';
const response = await request.get(apps(`/public/${app.id}/api/${param1Value}/${param2Value}/test`)).set(credentials);
expect(response.status, 'API endpoint request failed').to.equal(200);
expect(response.header['content-type'], 'Content-Type header is incorrect').to.include('text/plain');
expect(response.text, 'Response body does not contain correct parameters').to.equal(`Param1: ${param1Value}, Param2: ${param2Value}`);
});
});

View File

@@ -0,0 +1,109 @@
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import { getCredentials, request, credentials } from '../../data/api-data';
import { apps } from '../../data/apps/apps-data';
import { installTestApp, cleanupApps } from '../../data/apps/helper';
import { updatePermission } from '../../data/permissions.helper';
import { IS_EE } from '../../e2e/config/constants';
(IS_EE ? describe : describe.skip)('Apps - Logs Distinct Values', () => {
let appId: string;
before((done) => getCredentials(done));
before(async () => {
await cleanupApps();
const app = await installTestApp();
appId = app.id;
});
after(() => Promise.all([cleanupApps(), updatePermission('manage-apps', ['admin'])]));
it('should return distinct values successfully', (done) => {
void request
.get(apps(`/${appId}/logs/distinctValues`))
.set(credentials)
.expect(200)
.expect('Content-Type', 'application/json')
.expect((res) => {
expect(res.body).to.have.a.property('success', true);
expect(res.body).to.have.a.property('instanceIds').that.is.an('array');
expect(res.body).to.have.a.property('methods').that.is.an('array');
const { instanceIds, methods } = res.body;
const uniqueInstanceIds = new Set(instanceIds);
const uniqueMethods = new Set(methods);
// All instance IDs should be unique
expect(instanceIds).to.have.lengthOf(uniqueInstanceIds.size);
// All methods should be unique
expect(methods).to.have.lengthOf(uniqueMethods.size);
})
.end(done);
});
it('should require authentication', (done) => {
void request
.get(apps(`/${appId}/logs/distinctValues`))
.expect(401)
.expect('Content-Type', 'application/json')
.expect((res) => {
expect(res.body).to.have.a.property('success', false);
expect(res.body).to.have.a.property('error');
})
.end(done);
});
it('should require manage-apps permission', (done) => {
void updatePermission('manage-apps', []).then(
() =>
void request
.get(apps(`/${appId}/logs/distinctValues`))
.set(credentials)
.expect(403)
.expect('Content-Type', 'application/json')
.expect((res) => {
expect(res.body).to.have.a.property('success', false);
})
// Doing the `updatePermission` call here pollutes the error reporting, but we shouldn't call `done` before doing it.
.end((err) => void updatePermission('manage-apps', ['admin']).then(() => void done(err))),
);
});
it('should return 404 for non-existent app', (done) => {
void request
.get(apps('/non-existent-app-id/logs/distinctValues'))
.set(credentials)
.expect(404)
.expect('Content-Type', 'application/json')
.expect((res) => {
expect(res.body).to.have.a.property('success', false);
expect(res.body).to.have.a.property('error');
})
.end(done);
});
it('should return empty arrays when no logs exist', (done) => {
// Clean up all apps first to ensure no logs
void cleanupApps().then(async () => {
// Install a fresh app
const app = await installTestApp();
void request
.get(apps(`/${app.id}/logs/distinctValues`))
.set(credentials)
.expect(200)
.expect('Content-Type', 'application/json')
.expect((res) => {
expect(res.body).to.have.a.property('success', true);
expect(res.body).to.have.a.property('instanceIds').that.is.an('array');
expect(res.body).to.have.a.property('methods').that.is.an('array');
// Could be empty or have values from other tests
expect(res.body.instanceIds).to.satisfy((arr: any[]) => Array.isArray(arr));
expect(res.body.methods).to.satisfy((arr: any[]) => Array.isArray(arr));
})
.end(done);
});
});
});

View File

@@ -0,0 +1,399 @@
import type { ILoggerStorageEntry } from '@rocket.chat/apps/dist/server/logging/ILoggerStorageEntry';
import type { App } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import { getCredentials, request, credentials, getInstanceId } from '../../data/api-data';
import { apps } from '../../data/apps/apps-data';
import { installTestApp, cleanupApps } from '../../data/apps/helper';
import { IS_EE } from '../../e2e/config/constants';
(IS_EE ? describe : describe.skip)('Apps - Logs Export', () => {
let app: App;
before((done) => getCredentials(done));
before(async () => {
await cleanupApps();
app = await installTestApp();
});
after(() => cleanupApps());
it('should throw an error when trying to export logs for an invalid app', (done) => {
void request
.get(apps('/invalid-id/export-logs'))
.query({ type: 'json' })
.set(credentials)
.expect(404)
.expect((res) => {
expect(res.body).to.have.a.property('success', false);
expect(res.body.error).to.be.equal('No App found by the id of: invalid-id');
})
.end(done);
});
it('should export app logs as JSON successfully', (done) => {
void request
.get(apps(`/${app.id}/export-logs`))
.query({ type: 'json' })
.set(credentials)
.expect('Content-Type', 'text/plain')
.expect(200)
.expect((res) => {
expect(res.headers).to.have.a.property('content-disposition');
expect(res.headers['content-disposition']).to.include('attachment');
expect(res.headers['content-disposition']).to.include(`app-logs-${app.id}`);
expect(res.headers['content-disposition']).to.include('.json');
expect(res.headers).to.have.a.property('content-length');
// Verify the content is valid JSON
expect(() => JSON.parse(res.text)).to.not.throw();
const logs = JSON.parse(res.text);
expect(logs).to.be.an('array');
expect(logs[0]).to.have.a.property('_id');
expect(logs[0]).to.have.a.property('appId', app.id);
})
.end(done);
});
it('should export app logs as CSV successfully', (done) => {
void request
.get(apps(`/${app.id}/export-logs`))
.query({ type: 'csv' })
.set(credentials)
.expect('Content-Type', 'text/plain')
.expect(200)
.expect((res) => {
expect(res.headers).to.have.a.property('content-disposition');
expect(res.headers['content-disposition']).to.include('attachment');
expect(res.headers['content-disposition']).to.include(`app-logs-${app.id}`);
expect(res.headers['content-disposition']).to.include('.csv');
expect(res.headers).to.have.a.property('content-length');
// Verify the content is valid CSV (should have headers and at least one row)
const lines = res.text.split('\n');
expect(lines.length).to.be.at.least(1); // At least header row
if (lines.length > 1 && lines[0]) {
// Check that headers contain expected fields
expect(lines[0]).to.include('_id');
expect(lines[0]).to.include('appId');
}
})
.end(done);
});
it('should throw error when type is not specified', (done) => {
void request
.get(apps(`/${app.id}/export-logs`))
.set(credentials)
.expect(400)
.end(done);
});
it('should export app logs with pagination parameters', (done) => {
void request
.get(apps(`/${app.id}/export-logs`))
.query({ count: 5, type: 'json' })
.set(credentials)
.expect('Content-Type', 'text/plain')
.expect(200)
.expect((res) => {
const logs = JSON.parse(res.text);
expect(logs).to.be.an('array');
expect(logs.length).to.be.at.most(5);
})
.end(done);
});
it('should export app logs with sorting parameters', (done) => {
void request
.get(apps(`/${app.id}/export-logs`))
.query({
sort: JSON.stringify({ _updatedAt: 1 }),
type: 'json',
})
.set(credentials)
.expect('Content-Type', 'text/plain')
.expect(200)
.expect((res) => {
const logs = JSON.parse(res.text);
expect(logs).to.be.an('array');
// Verify sorting by checking if timestamps are in ascending order
if (logs.length > 1) {
for (let i = 1; i < logs.length; i++) {
const prevTime = new Date(logs[i - 1]._updatedAt).getTime();
const currTime = new Date(logs[i]._updatedAt).getTime();
expect(currTime).to.be.at.least(prevTime);
}
}
})
.end(done);
});
it('should export app logs filtered by logLevel', (done) => {
void request
.get(apps(`/${app.id}/export-logs`))
.query({ logLevel: '2', type: 'json' })
.set(credentials)
.expect('Content-Type', 'text/plain')
.expect(200)
.expect((res) => {
const logs = JSON.parse(res.text);
expect(logs).to.be.an('array');
logs.forEach((log: ILoggerStorageEntry) => {
const entry = log.entries.find((entry) => ['error', 'warn', 'info', 'log', 'debug', 'success'].includes(entry.severity));
expect(entry).to.exist;
});
})
.end(done);
});
it('should export app logs filtered by method', (done) => {
void request
.get(apps(`/${app.id}/export-logs`))
.query({ method: 'app:construct', type: 'json' })
.set(credentials)
.expect('Content-Type', 'text/plain')
.expect(200)
.expect((res) => {
const logs = JSON.parse(res.text);
expect(logs).to.be.an('array');
logs.forEach((log: ILoggerStorageEntry) => {
expect(log.method).to.equal('app:construct');
});
})
.end(done);
});
it('should export app logs filtered by instance id', (done) => {
const instanceId = getInstanceId();
void request
.get(apps(`/${app.id}/export-logs`))
.query({ instanceId, type: 'json' })
.set(credentials)
.expect('Content-Type', 'text/plain')
.expect(200)
.expect((res) => {
const logs = JSON.parse(res.text);
expect(logs).to.be.an('array');
logs.forEach((log: ILoggerStorageEntry) => {
expect(log.instanceId).to.equal(instanceId);
});
})
.end(done);
});
it('should export app logs filtered by date range', (done) => {
const startDate = new Date();
startDate.setDate(startDate.getDate() - 1); // 1 day ago
const endDate = new Date();
void request
.get(apps(`/${app.id}/export-logs`))
.query({
startDate: startDate.toISOString(),
endDate: endDate.toISOString(),
type: 'json',
})
.set(credentials)
.expect('Content-Type', 'text/plain')
.expect(200)
.expect((res) => {
const logs = JSON.parse(res.text);
expect(logs).to.be.an('array');
// Verify that all returned logs are within the date range
logs.forEach((log: ILoggerStorageEntry) => {
const logDate = new Date(log._createdAt);
expect(logDate).to.be.above(startDate).and.below(endDate);
});
})
.end(done);
});
it('should export app logs with combined filters', (done) => {
const startDate = new Date();
startDate.setDate(startDate.getDate() - 1); // 1 day ago
const endDate = new Date();
void request
.get(apps(`/${app.id}/export-logs`))
.query({
logLevel: '2',
method: 'app:construct',
startDate: startDate.toISOString(),
endDate: endDate.toISOString(),
type: 'json',
count: 10,
})
.set(credentials)
.expect('Content-Type', 'text/plain')
.expect(200)
.expect((res) => {
const logs = JSON.parse(res.text);
expect(logs).to.be.an('array');
expect(logs.length).to.be.at.most(10);
// Verify that all returned logs match all filter criteria
logs.forEach((log: ILoggerStorageEntry) => {
expect(log.method).to.equal('app:construct');
const logDate = new Date(log._createdAt);
expect(logDate >= startDate && logDate <= endDate).to.be.true;
const entry = log.entries.find((entry) => ['error', 'warn', 'info', 'log', 'debug', 'success'].includes(entry.severity));
expect(entry).to.exist;
});
})
.end(done);
});
it('should return an error when no logs are found for the specified criteria', (done) => {
const futureDate = new Date();
futureDate.setFullYear(futureDate.getFullYear() + 1);
void request
.get(apps(`/${app.id}/export-logs`))
.query({
startDate: futureDate.toISOString(),
type: 'json',
})
.set(credentials)
.expect(400)
.expect((res) => {
expect(res.body).to.have.a.property('success', false);
expect(res.body.error).to.be.equal('No logs found for the specified criteria');
})
.end(done);
});
it('should respect the maximum limit of 2000 logs', (done) => {
void request
.get(apps(`/${app.id}/export-logs`))
.query({ count: 5000, type: 'json' })
.set(credentials)
.expect('Content-Type', 'text/plain')
.expect(200)
.expect((res) => {
const logs = JSON.parse(res.text);
expect(logs).to.be.an('array');
expect(logs.length).to.be.at.most(2000);
})
.end(done);
});
it('should reject invalid logLevel value', (done) => {
void request
.get(apps(`/${app.id}/export-logs`))
.query({ logLevel: 'debug' })
.set(credentials)
.expect(400)
.expect((res) => {
expect(res.body).to.have.a.property('success', false);
expect(res.body).to.have.a.property('error').that.is.not.empty;
})
.end(done);
});
it('should reject invalid date format', (done) => {
void request
.get(apps(`/${app.id}/export-logs`))
.query({ startDate: 'invalid-date' })
.set(credentials)
.expect(400)
.expect((res) => {
expect(res.body).to.have.a.property('success', false);
expect(res.body).to.have.a.property('error').that.is.not.empty;
})
.end(done);
});
it('should reject invalid date range', (done) => {
const startDate = new Date();
const endDate = new Date();
endDate.setDate(endDate.getDate() - 1); // endDate before startDate
void request
.get(apps(`/${app.id}/export-logs`))
.query({
startDate: startDate.toISOString(),
endDate: endDate.toISOString(),
})
.set(credentials)
.expect(400)
.expect((res) => {
expect(res.body).to.have.a.property('success', false);
expect(res.body).to.have.a.property('error').that.is.not.empty;
})
.end(done);
});
it('should reject invalid additional properties', (done) => {
void request
.get(apps(`/${app.id}/export-logs`))
.query({ invalidProperty: 'value' })
.set(credentials)
.expect(400)
.expect((res) => {
expect(res.body).to.have.a.property('success', false);
expect(res.body).to.have.a.property('error').that.is.not.empty;
})
.end(done);
});
it('should handle authentication requirement', (done) => {
void request
.get(apps(`/${app.id}/export-logs`))
.query({ type: 'json' })
.expect(401)
.end(done);
});
it('should handle authentication via cookie', (done) => {
void request
.get(apps(`/${app.id}/export-logs`))
.query({ type: 'json' })
.set({ cookie: `rc_uid=${credentials['X-User-Id']}; rc_token=${credentials['X-Auth-Token']}` })
.expect(200)
.end(done);
});
it('should include proper filename with timestamp in Content-Disposition header', (done) => {
void request
.get(apps(`/${app.id}/export-logs`))
.query({ type: 'json' })
.set(credentials)
.expect(200)
.expect((res) => {
const contentDisposition = res.headers['content-disposition'];
expect(contentDisposition).to.include('attachment');
expect(contentDisposition).to.include(`app-logs-${app.id}`);
expect(contentDisposition).to.include('.json');
// Check that timestamp is included (format: YYYY-MM-DDTHH-MM-SS)
const timestampRegex = /\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}/;
expect(contentDisposition).to.match(timestampRegex);
})
.end(done);
});
it('should include Content-Length header', (done) => {
void request
.get(apps(`/${app.id}/export-logs`))
.query({ type: 'json' })
.set(credentials)
.expect(200)
.expect((res) => {
expect(res.headers).to.have.a.property('content-length');
const contentLength = parseInt(res.headers['content-length'], 10);
expect(contentLength).to.be.a('number');
expect(contentLength).to.be.greaterThan(0);
expect(contentLength).to.equal(Buffer.byteLength(res.text, 'utf8'));
})
.end(done);
});
});

View File

@@ -0,0 +1,240 @@
import type { ILoggerStorageEntry } from '@rocket.chat/apps/dist/server/logging/ILoggerStorageEntry';
import type { App } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import { getCredentials, request, credentials } from '../../data/api-data';
import { apps } from '../../data/apps/apps-data';
import { installTestApp, cleanupApps } from '../../data/apps/helper';
import { updatePermission } from '../../data/permissions.helper';
import { IS_EE } from '../../e2e/config/constants';
(IS_EE ? describe : describe.skip)('Apps - General Logs (GET /api/apps/logs)', () => {
let app: App;
before((done) => getCredentials(done));
before(async () => {
await cleanupApps();
app = await installTestApp();
});
after(() => Promise.all([cleanupApps(), updatePermission('manage-apps', ['admin'])]));
it('should return logs for all apps successfully', (done) => {
void request
.get(apps('/logs'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.a.property('success', true);
expect(res.body).to.have.a.property('logs').that.is.an('array').with.a.lengthOf.at.least(1);
expect(res.body).to.have.a.property('count').that.is.a('number');
expect(res.body).to.have.a.property('total').that.is.a('number');
expect(res.body).to.have.a.property('offset').that.is.a('number');
})
.end(done);
});
it('should require authentication', (done) => {
void request
.get(apps('/logs'))
.expect('Content-Type', 'application/json')
.expect(401)
.expect((res) => {
expect(res.body).to.have.a.property('success', false);
expect(res.body).to.have.a.property('error');
})
.end(done);
});
it('should require manage-apps permission', (done) => {
void updatePermission('manage-apps', []).then(
() =>
void request
.get(apps('/logs'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(403)
.expect((res) => {
expect(res.body).to.have.a.property('success', false);
})
.end((err) => void updatePermission('manage-apps', ['admin']).then(() => void done(err))),
);
});
it('should return logs with pagination', (done) => {
void request
.get(apps('/logs'))
.query({ count: 1, offset: 0 })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.a.property('success', true);
expect(res.body.logs).to.be.an('array').with.a.lengthOf(1);
expect(res.body.count).to.be.equal(1);
expect(res.body.offset).to.be.equal(0);
})
.end(done);
});
it('should return logs with sorting', (done) => {
void request
.get(apps('/logs'))
.query({ sort: JSON.stringify({ _updatedAt: -1 }) })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.a.property('success', true);
expect(res.body.logs).to.be.an('array').with.a.lengthOf.at.least(1);
})
.end(done);
});
it('should return logs filtered by appId', (done) => {
void request
.get(apps('/logs'))
.query({ appId: app.id })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.a.property('success', true);
expect(res.body.logs).to.be.an('array').with.a.lengthOf.at.least(1);
res.body.logs.forEach((log: ILoggerStorageEntry) => {
expect(log.appId).to.equal(app.id);
});
})
.end(done);
});
it('should return logs filtered by method', (done) => {
void request
.get(apps('/logs'))
.query({ method: 'app:construct' })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.a.property('success', true);
expect(res.body.logs).to.be.an('array').with.a.lengthOf.at.least(1);
res.body.logs.forEach((log: ILoggerStorageEntry) => {
expect(log.method).to.equal('app:construct');
});
})
.end(done);
});
it('should return logs filtered by logLevel', (done) => {
void request
.get(apps('/logs'))
.query({ logLevel: '2' })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.a.property('success', true);
expect(res.body.logs).to.be.an('array').with.a.lengthOf.at.least(1);
res.body.logs.forEach((log: ILoggerStorageEntry) => {
const entry = log.entries.find((entry) => ['error', 'warn', 'info', 'log', 'debug', 'success'].includes(entry.severity));
expect(entry).to.exist;
});
})
.end(done);
});
it('should return logs filtered by date range', (done) => {
const startDate = new Date();
startDate.setDate(startDate.getDate() - 1); // 1 day ago
const endDate = new Date();
void request
.get(apps('/logs'))
.query({
startDate: startDate.toISOString(),
endDate: endDate.toISOString(),
})
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.a.property('success', true);
expect(res.body.logs).to.be.an('array').with.a.lengthOf.at.least(1);
res.body.logs.forEach((log: ILoggerStorageEntry) => {
const logDate = new Date(log._createdAt);
expect(logDate).to.be.above(startDate).and.below(endDate);
});
})
.end(done);
});
it('should reject invalid logLevel value', (done) => {
void request
.get(apps('/logs'))
.query({ logLevel: 'debug' })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.a.property('success', false);
expect(res.body).to.have.a.property('error').that.is.not.empty;
})
.end(done);
});
it('should reject invalid date format', (done) => {
void request
.get(apps('/logs'))
.query({ startDate: 'invalid-date' })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.a.property('success', false);
expect(res.body).to.have.a.property('error').that.is.not.empty;
})
.end(done);
});
it('should reject invalid date range', (done) => {
const startDate = new Date();
const endDate = new Date();
endDate.setDate(endDate.getDate() - 1); // endDate before startDate
void request
.get(apps('/logs'))
.query({
startDate: startDate.toISOString(),
endDate: endDate.toISOString(),
})
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.a.property('success', false);
expect(res.body).to.have.a.property('error').that.is.not.empty;
})
.end(done);
});
it('should reject invalid additional properties', (done) => {
void request
.get(apps('/logs'))
.query({ invalidProperty: 'value' })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.a.property('success', false);
expect(res.body).to.have.a.property('error').that.is.not.empty;
})
.end(done);
});
});

View File

@@ -0,0 +1,68 @@
import type { ILoggerStorageEntry } from '@rocket.chat/apps/dist/server/logging/ILoggerStorageEntry';
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import { getCredentials, request, credentials } from '../../data/api-data';
import { appCausingNestedRequests } from '../../data/apps/app-packages';
import { apps } from '../../data/apps/apps-data';
import { cleanupApps, installLocalTestPackage } from '../../data/apps/helper';
import { loadHistory } from '../../data/rooms.helper';
import { executeAppSlashCommand } from '../../data/slashcommands.helpers';
import { IS_EE } from '../../e2e/config/constants';
(IS_EE ? describe : describe.skip)('Apps - Logs Nested Requests', () => {
before((done) => getCredentials(done));
after(() => cleanupApps());
it('should log nested requests without errors', async () => {
const app = await installLocalTestPackage(appCausingNestedRequests);
const roomId = 'GENERAL';
const execId = Math.random().toString(36).substring(2, 15);
const slashcommandRes = await executeAppSlashCommand('nest', roomId, execId);
expect(slashcommandRes.status, 'Slashcommand execution failed').to.equal(200);
const { messages } = await loadHistory(roomId);
const targetMessage = messages.find((msg) => msg.msg === `nested_test_message ${execId}`);
expect(targetMessage, 'Target message from nested request not found').to.exist;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- we're checking existence above
const { _id: messageId } = targetMessage!;
const logsRes = await request.get(apps(`/${app.id}/logs`)).set(credentials);
expect(logsRes.status, 'Fetching app logs failed').to.equal(200);
expect(logsRes.body, 'Apps logs response success is not true').to.have.a.property('success', true);
expect(logsRes.body, 'Apps logs response logs property is empty or missing')
.to.have.a.property('logs')
.that.is.an('array')
.with.lengthOf.greaterThan(0);
let slashcommandExecutionLog: ILoggerStorageEntry | undefined;
let postMessageExecutionLog: ILoggerStorageEntry | undefined;
logsRes.body.logs.forEach((log: ILoggerStorageEntry) => {
if ((log.method as string) === 'slashcommand:nest:executor' && log.entries.find((entry) => entry.args.includes(messageId))) {
slashcommandExecutionLog = log;
return;
}
if ((log.method as string) === 'app:executePostMessageSent' && log.entries.find((entry) => entry.args.includes(messageId))) {
postMessageExecutionLog = log;
}
});
expect(postMessageExecutionLog, 'IPostMessageSent execution log not found').to.exist;
expect(slashcommandExecutionLog, 'Slashcommand execution log not found').to.exist;
expect(
slashcommandExecutionLog?.entries.filter((entry) => entry.args.includes(execId)),
`Slashcommand execution log does not contain a matching execId "${execId}"`,
).to.have.lengthOf(1);
});
});

View File

@@ -0,0 +1,288 @@
import type { ILoggerStorageEntry } from '@rocket.chat/apps/dist/server/logging/ILoggerStorageEntry';
import type { App } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import { getCredentials, request, credentials } from '../../data/api-data';
import { apps } from '../../data/apps/apps-data';
import { installTestApp, cleanupApps } from '../../data/apps/helper';
import { updatePermission } from '../../data/permissions.helper';
import { IS_EE } from '../../e2e/config/constants';
(IS_EE ? describe : describe.skip)('Apps - Logs', () => {
let app: App;
before((done) => getCredentials(done));
before(async () => {
await cleanupApps();
app = await installTestApp();
});
after(() => cleanupApps());
it('should throw an error when trying to get logs for an invalid app', (done) => {
void request
.get(apps('/invalid-id/logs'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(404)
.expect((res) => {
expect(res.body).to.have.a.property('success', false);
expect(res.body).to.not.have.a.property('logs');
expect(res.body.error).to.be.equal('No App found by the id of: invalid-id');
})
.end(done);
});
it('should return app logs successfully', (done) => {
void request
.get(apps(`/${app.id}/logs`))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.a.property('success', true);
expect(res.body).to.have.a.property('logs').that.is.an('array').with.a.lengthOf.at.least(1).and.at.most(50);
expect(res.body).to.have.a.property('count').that.is.a('number');
expect(res.body).to.have.a.property('total').that.is.a('number');
expect(res.body).to.have.a.property('offset').that.is.a('number');
})
.end(done);
});
it('should return app logs with pagination', (done) => {
void request
.get(apps(`/${app.id}/logs`))
.query({ count: 1, offset: 0 })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.a.property('success', true);
expect(res.body.logs).to.be.an('array').with.a.lengthOf(1);
expect(res.body.count).to.be.equal(1);
expect(res.body.offset).to.be.equal(0);
})
.end(done);
});
it('should return app logs with sorting', (done) => {
void request
.get(apps(`/${app.id}/logs`))
.query({ sort: JSON.stringify({ _updatedAt: -1 }) })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.a.property('success', true);
expect(res.body.logs).to.be.an('array').with.a.lengthOf.at.least(1).and.at.most(50);
})
.end(done);
});
it('should return app logs filtered by logLevel', (done) => {
void request
.get(apps(`/${app.id}/logs`))
.query({ logLevel: '2' })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.a.property('success', true);
expect(res.body.logs).to.be.an('array').with.a.lengthOf.at.least(1).and.at.most(50);
res.body.logs.forEach((log: ILoggerStorageEntry) => {
const entry = log.entries.find((entry) => ['error', 'warn', 'info', 'log', 'debug', 'success'].includes(entry.severity));
expect(entry).to.exist;
});
})
.end(done);
});
it('should return app logs filtered by method', (done) => {
void request
.get(apps(`/${app.id}/logs`))
.query({ method: 'app:construct' })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.a.property('success', true);
expect(res.body.logs).to.be.an('array').with.a.lengthOf.at.least(1).and.at.most(50);
res.body.logs.forEach((log: ILoggerStorageEntry) => {
expect(log.method).to.equal('app:construct');
});
})
.end(done);
});
it('should return app logs filtered by date range', (done) => {
const startDate = new Date();
startDate.setDate(startDate.getDate() - 1); // 1 day ago
const endDate = new Date();
void request
.get(apps(`/${app.id}/logs`))
.query({
startDate: startDate.toISOString(),
endDate: endDate.toISOString(),
})
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.a.property('success', true);
expect(res.body.logs).to.be.an('array').with.a.lengthOf.at.least(1).and.at.most(50);
// Verify that all returned logs are within the date range
res.body.logs.forEach((log: ILoggerStorageEntry) => {
const logDate = new Date(log._createdAt);
expect(logDate).to.be.above(startDate).and.below(endDate);
});
})
.end(done);
});
it('should return app logs with combined filters', (done) => {
const startDate = new Date();
startDate.setDate(startDate.getDate() - 1); // 1 day ago
const endDate = new Date();
void request
.get(apps(`/${app.id}/logs`))
.query({
logLevel: '2',
method: 'app:construct',
startDate: startDate.toISOString(),
endDate: endDate.toISOString(),
})
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.a.property('success', true);
expect(res.body.logs).to.be.an('array').with.a.lengthOf.at.least(1).and.at.most(50);
// Verify that all returned logs match all filter criteria
res.body.logs.forEach((log: ILoggerStorageEntry) => {
expect(log.method).to.equal('app:construct');
const logDate = new Date(log._createdAt);
expect(logDate >= startDate && logDate <= endDate).to.be.true;
const entry = log.entries.find((entry) => ['error', 'warn', 'info', 'log', 'debug', 'success'].includes(entry.severity));
expect(entry).to.exist;
});
})
.end(done);
});
it('should reject invalid logLevel value', (done) => {
void request
.get(apps(`/${app.id}/logs`))
.query({ logLevel: 'debug' })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.a.property('success', false);
expect(res.body).to.have.a.property('error').that.is.not.empty;
})
.end(done);
});
it('should reject invalid date format', (done) => {
void request
.get(apps(`/${app.id}/logs`))
.query({ startDate: 'invalid-date' })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.a.property('success', false);
expect(res.body).to.have.a.property('error').that.is.not.empty;
})
.end(done);
});
it('should reject invalid date range', (done) => {
const startDate = new Date();
const endDate = new Date();
endDate.setDate(endDate.getDate() - 1); // endDate before startDate
void request
.get(apps(`/${app.id}/logs`))
.query({
startDate: startDate.toISOString(),
endDate: endDate.toISOString(),
})
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.a.property('success', false);
expect(res.body).to.have.a.property('error').that.is.not.empty;
})
.end(done);
});
it('should reject invalid additional properties', (done) => {
void request
.get(apps(`/${app.id}/logs`))
.query({ invalidProperty: 'value' })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.a.property('success', false);
expect(res.body).to.have.a.property('error').that.is.not.empty;
})
.end(done);
});
it('should reject appId query parameter', (done) => {
void request
.get(apps(`/${app.id}/logs`))
.query({ appId: 'invalid-id' })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(404)
.expect((res) => {
expect(res.body).to.have.a.property('success', false);
expect(res.body).to.have.a.property('error', 'Invalid query parameter "appId": invalid-id');
})
.end(done);
});
it('should require authentication', (done) => {
void request
.get(apps(`/${app.id}/logs`))
.expect('Content-Type', 'application/json')
.expect(401)
.expect((res) => {
expect(res.body).to.have.a.property('success', false);
expect(res.body).to.have.a.property('error');
})
.end(done);
});
it('should require manage-apps permission', (done) => {
void updatePermission('manage-apps', []).then(
() =>
void request
.get(apps(`/${app.id}/logs`))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(403)
.expect((res) => {
expect(res.body).to.have.a.property('success', false);
})
.end((err) => void updatePermission('manage-apps', ['admin']).then(() => void done(err))),
);
});
});

View File

@@ -0,0 +1,49 @@
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import { getCredentials } from '../../data/api-data';
import { messageReactionTest } from '../../data/apps/app-packages';
import { cleanupApps, installLocalTestPackage } from '../../data/apps/helper';
import { sendSimpleMessage, getMessageById } from '../../data/chat.helper';
import { createRoom, deleteRoom } from '../../data/rooms.helper';
import { executeAppSlashCommand } from '../../data/slashcommands.helpers';
import { IS_EE } from '../../e2e/config/constants';
(IS_EE ? describe : describe.skip)('Apps - Message Reactions', () => {
let roomId: string;
before((done) => getCredentials(done));
before(async () => {
await cleanupApps();
await installLocalTestPackage(messageReactionTest);
const res = await createRoom({ type: 'c', name: `apps-reactions-test-${Date.now()}` });
roomId = res.body.channel._id;
});
after(async () => {
await deleteRoom({ type: 'c', roomId });
await cleanupApps();
});
it('should add and remove a thumbs-up reaction to a message', async () => {
const sendRes = await sendSimpleMessage({ roomId, text: 'reaction test message' });
const messageId = sendRes.body.message._id;
const slashRes = await executeAppSlashCommand('msg-update', roomId, `add ${messageId}`);
expect(slashRes.status, 'Slash command to add reaction failed').to.equal(200);
let message = await getMessageById({ msgId: messageId });
expect(message.reactions, 'Message reactions should exist after adding').to.exist;
expect(message.reactions, 'Message should have a thumbs-up reaction').to.have.property(':+1:');
// Now remove the reaction
const removeRes = await executeAppSlashCommand('msg-update', roomId, `remove ${messageId}`);
expect(removeRes.status, 'Slash command to remove reaction failed').to.equal(200);
message = await getMessageById({ msgId: messageId });
const hasThumbsUp = Boolean(message.reactions && ':+1:' in message.reactions);
expect(hasThumbsUp, 'Thumbs-up reaction should have been removed').to.be.false;
});
});

View File

@@ -0,0 +1,239 @@
import type { App, ILivechatVisitor } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import { MongoClient } from 'mongodb';
import { getCredentials, request, credentials } from '../../data/api-data';
import { appExternalIdTest } from '../../data/apps/app-packages';
import { apps } from '../../data/apps/apps-data';
import { cleanupApps } from '../../data/apps/helper';
import { createVisitor, createVisitorWithCustomData, createAgent, makeAgentAvailable } from '../../data/livechat/rooms';
import { updateSetting } from '../../data/permissions.helper';
import { IS_EE, URL_MONGODB } from '../../e2e/config/constants';
(IS_EE ? describe : describe.skip)('Apps - resolveVisitor API', () => {
let app: App;
before((done) => getCredentials(done));
before(async () => {
await updateSetting('Livechat_enabled', true);
await createAgent();
await makeAgentAvailable();
const installResponse = await request.post(apps()).set(credentials).attach('app', appExternalIdTest);
if (!installResponse.body.success) {
throw new Error(`Failed to install test app: ${installResponse.body.error}`);
}
app = installResponse.body.app;
});
after(async () => {
await cleanupApps();
});
const callResolveVisitor = async (
externalId: { entityId: string; metadata?: Record<string, unknown> },
contactData?: { phone?: string; email?: string },
) => {
return request
.post(apps(`/public/${app.id}/resolve-visitor`))
.set(credentials)
.send({ externalId, phone: contactData?.phone, email: contactData?.email });
};
const callUpdateExternalId = async (visitorId: string, externalId: { entityId: string; metadata?: Record<string, unknown> }) => {
return request
.post(apps(`/public/${app.id}/update-external-id`))
.set(credentials)
.send({ visitorId, externalId });
};
describe('externalId lookup', () => {
it('should return null when externalId does not exist', async () => {
const response = await callResolveVisitor({ entityId: 'nonexistent-id' });
expect(response.status).to.equal(200);
expect(response.body.visitor).to.be.null;
});
it('should find visitor directly by externalId', async () => {
const phone = `+1${Date.now()}`;
const visitor = await createVisitor(undefined, undefined, undefined, phone);
const externalId = { entityId: `id-${Date.now()}`, metadata: { username: '@user' } };
await callResolveVisitor(externalId, { phone });
const response = await callResolveVisitor(externalId);
expect(response.status).to.equal(200);
expect(response.body.visitor.id).to.equal(visitor._id);
const appExternalId = response.body.visitor.externalIds.find((e: { appId: string }) => e.appId === app.id);
expect(appExternalId.entityId).to.equal(externalId.entityId);
expect(appExternalId.metadata.username).to.equal(externalId.metadata.username);
});
it('should not update externalId when found by lookup', async () => {
const phone = `+2${Date.now()}`;
await createVisitor(undefined, undefined, undefined, phone);
const externalId = { entityId: `id-${Date.now()}`, metadata: { username: '@original' } };
await callResolveVisitor(externalId, { phone });
// resolveVisitor is for resolving/enriching visitors, not for updating existing data.
// When found by externalId, it returns the visitor as-is without modifications.
// To update visitor data, apps should use other methods like ILivechatUpdater.
const response = await callResolveVisitor({ entityId: externalId.entityId, metadata: { username: '@changed' } });
const appExternalId = response.body.visitor.externalIds.find((e: { appId: string }) => e.appId === app.id);
expect(appExternalId.metadata.username).to.equal('@original');
});
});
describe('phone fallback', () => {
it('should find visitor by phone and save externalId', async () => {
const phone = `+3${Date.now()}`;
const visitor = await createVisitor(undefined, undefined, undefined, phone);
const externalId = { entityId: `id-${Date.now()}`, metadata: { username: '@user' } };
const response = await callResolveVisitor(externalId, { phone });
expect(response.status).to.equal(200);
expect(response.body.visitor.id).to.equal(visitor._id);
const appExternalId = response.body.visitor.externalIds.find((e: { appId: string }) => e.appId === app.id);
expect(appExternalId.entityId).to.equal(externalId.entityId);
expect(appExternalId.metadata.username).to.equal(externalId.metadata.username);
});
it('should overwrite externalId when called with different entityId', async () => {
const phone = `+4${Date.now()}`;
await createVisitor(undefined, undefined, undefined, phone);
await callResolveVisitor({ entityId: `first-${Date.now()}`, metadata: { username: '@first' } }, { phone });
const newExternalId = { entityId: `second-${Date.now()}`, metadata: { username: '@second' } };
const response = await callResolveVisitor(newExternalId, { phone });
const appExternalId = response.body.visitor.externalIds.find((e: { appId: string }) => e.appId === app.id);
expect(appExternalId.entityId).to.equal(newExternalId.entityId);
expect(appExternalId.metadata.username).to.equal('@second');
});
it('should return null when phone not found', async () => {
const response = await callResolveVisitor({ entityId: `id-${Date.now()}` }, { phone: '+0000000000' });
expect(response.status).to.equal(200);
expect(response.body.visitor).to.be.null;
});
it('should not use phone fallback when phone is empty', async () => {
const response = await callResolveVisitor({ entityId: `id-${Date.now()}` }, { phone: '' });
expect(response.status).to.equal(200);
expect(response.body.visitor).to.be.null;
});
});
describe('email fallback', () => {
it('should find visitor by email and save externalId', async () => {
const email = `test-${Date.now()}@example.com`;
const visitor = await createVisitor(undefined, undefined, email);
const externalId = { entityId: `id-email-${Date.now()}`, metadata: { username: '@emailuser' } };
const response = await callResolveVisitor(externalId, { email });
expect(response.status).to.equal(200);
expect(response.body.visitor.id).to.equal(visitor._id);
const appExternalId = response.body.visitor.externalIds.find((e: { appId: string }) => e.appId === app.id);
expect(appExternalId.entityId).to.equal(externalId.entityId);
expect(appExternalId.metadata.username).to.equal(externalId.metadata.username);
});
it('should overwrite externalId when called with different entityId via email', async () => {
const email = `test-${Date.now()}@example.com`;
await createVisitor(undefined, undefined, email);
await callResolveVisitor({ entityId: `first-email-${Date.now()}`, metadata: { username: '@first' } }, { email });
const newExternalId = { entityId: `second-email-${Date.now()}`, metadata: { username: '@second' } };
const response = await callResolveVisitor(newExternalId, { email });
const appExternalId = response.body.visitor.externalIds.find((e: { appId: string }) => e.appId === app.id);
expect(appExternalId.entityId).to.equal(newExternalId.entityId);
expect(appExternalId.metadata.username).to.equal('@second');
});
it('should return null when email not found', async () => {
const response = await callResolveVisitor({ entityId: `id-${Date.now()}` }, { email: 'notfound@example.com' });
expect(response.status).to.equal(200);
expect(response.body.visitor).to.be.null;
});
it('should not use email fallback when email is empty', async () => {
const response = await callResolveVisitor({ entityId: `id-${Date.now()}` }, { email: '' });
expect(response.status).to.equal(200);
expect(response.body.visitor).to.be.null;
});
});
describe('cross-appId lookup', () => {
// Direct MongoDB to inject externalId with a different appId, avoiding the need to install a second test app
const addExternalIdToVisitor = async (visitorId: string, externalId: { appId: string; entityId: string }) => {
const connection = await MongoClient.connect(URL_MONGODB);
try {
await connection
.db()
.collection<ILivechatVisitor>('rocketchat_livechat_visitor')
.updateOne({ _id: visitorId }, { $push: { externalIds: externalId } });
} finally {
await connection.close();
}
};
it('should find visitor by entityId even when externalId was saved by different appId', async () => {
// Create visitor without phone to ensure we're testing entityId lookup only
const visitor = await createVisitorWithCustomData({ ignorePhone: true });
const oldAppId = 'old-app-version-id-12345';
const entityId = `cross-app-${Date.now()}`;
await addExternalIdToVisitor(visitor._id, { appId: oldAppId, entityId });
// App searches by entityId only - should find visitor even though appId is different
const response = await callResolveVisitor({ entityId });
expect(response.status).to.equal(200);
expect(response.body.visitor.id).to.equal(visitor._id);
const oldExternalId = response.body.visitor.externalIds.find((e: { appId: string }) => e.appId === oldAppId);
expect(oldExternalId.entityId).to.equal(entityId);
});
it('should allow app to add its own externalId using updateVisitorExternalId', async () => {
const visitor = await createVisitorWithCustomData({ ignorePhone: true });
const oldAppId = 'old-app-version-id-67890';
const entityId = `cross-app-update-${Date.now()}`;
await addExternalIdToVisitor(visitor._id, { appId: oldAppId, entityId });
const lookupResponse = await callResolveVisitor({ entityId });
expect(lookupResponse.body.visitor.id).to.equal(visitor._id);
const newExternalId = { entityId, metadata: { username: '@newversion' } };
const updateResponse = await callUpdateExternalId(visitor._id, newExternalId);
expect(updateResponse.status).to.equal(200);
expect(updateResponse.body.visitor.id).to.equal(visitor._id);
const oldEntry = updateResponse.body.visitor.externalIds.find((e: { appId: string }) => e.appId === oldAppId);
const newEntry = updateResponse.body.visitor.externalIds.find((e: { appId: string }) => e.appId === app.id);
expect(oldEntry).to.exist;
expect(oldEntry.entityId).to.equal(entityId);
expect(newEntry).to.exist;
expect(newEntry.entityId).to.equal(entityId);
expect(newEntry.metadata.username).to.equal('@newversion');
});
});
});

View File

@@ -0,0 +1,56 @@
import type { IRoom } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import type { Response } from 'supertest';
import { getCredentials, request, credentials, api } from '../../data/api-data';
import { appImplementsIPreFileUpload } from '../../data/apps/app-packages';
import { cleanupApps, installLocalTestPackage } from '../../data/apps/helper';
import { createRoom, deleteRoom } from '../../data/rooms.helper';
import { IS_EE } from '../../e2e/config/constants';
(IS_EE ? describe : describe.skip)('[Apps Hooks - File Upload]', () => {
before((done) => getCredentials(done));
describe('IPreFileUpload', () => {
let room: IRoom;
before(async () => {
await cleanupApps();
await installLocalTestPackage(appImplementsIPreFileUpload);
room = await createRoom({ type: 'c', name: `file-upload-hook-${Date.now()}` }).then((res) => res.body.channel);
});
after(() => Promise.all([deleteRoom({ type: 'c' as const, roomId: room._id }), cleanupApps()]));
it('should be capable of rejecting an upload based on app logic', async () => {
const fileContents = 'I want to be rejected by the app';
await request
.post(api(`rooms.media/${room._id}`))
.set(credentials)
.attach('file', Buffer.from(fileContents), { filename: 'test-should-reject' })
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'error-app-prevented');
expect(res.body).to.have.property('error').that.is.string(fileContents);
});
});
it('should not prevent an unrelated file upload', async () => {
const fileContents = 'I should not be rejected';
await request
.post(api(`rooms.media/${room._id}`))
.set(credentials)
.attach('file', Buffer.from(fileContents), { filename: 'test-file' })
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
});
});
});
});

View File

@@ -0,0 +1,50 @@
import type { App } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import { getCredentials, request, credentials } from '../../data/api-data';
import { apps } from '../../data/apps/apps-data';
import { installTestApp, cleanupApps } from '../../data/apps/helper';
import { IS_EE } from '../../e2e/config/constants';
(IS_EE ? describe : describe.skip)('Apps - Uninstall', () => {
let app: App;
before((done) => getCredentials(done));
before(async () => {
await cleanupApps();
app = await installTestApp();
});
after(() => cleanupApps());
describe('[Uninstall]', () => {
it('should throw an error when trying to uninstall an invalid app', (done) => {
void request
.delete(apps('/invalid-id'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(404)
.expect((res) => {
expect(res.body).to.have.a.property('success', false);
expect(res.body.error).to.be.equal('No App found by the id of: invalid-id');
})
.end(done);
});
it('should remove the app successfully', (done) => {
void request
.delete(apps(`/${app.id}`))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.a.property('success', true);
expect(res.body).to.have.a.property('app');
expect(res.body.app.id).to.be.equal(app.id);
expect(res.body.app.status).to.be.equal('disabled');
})
.end(done);
});
});
});

View File

@@ -0,0 +1,75 @@
import type { App } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import { getCredentials, request, credentials } from '../../data/api-data';
import { appUpdateTest, appUpdateTestBroken, appUpdateTestFaulty } from '../../data/apps/app-packages';
import { apps } from '../../data/apps/apps-data';
import { cleanupApps, installLocalTestPackage } from '../../data/apps/helper';
import { getUserByUsername } from '../../data/users.helper';
import { IS_EE } from '../../e2e/config/constants';
const APP_USERNAME = 'app-update-test.bot';
(IS_EE ? describe : describe.skip)('Apps - Update', () => {
let app: App;
before((done) => getCredentials(done));
before(async () => {
await cleanupApps();
app = await installLocalTestPackage(appUpdateTest);
});
after(() => cleanupApps());
describe('[App user presence]', () => {
it('should keep the app user online after the app is updated', async () => {
const appUserBefore = await getUserByUsername(APP_USERNAME);
expect(appUserBefore.status).to.be.equal('online');
await request
.post(apps(`/${app.id}`))
.set(credentials)
.attach('app', appUpdateTest)
.expect(200);
const appUserAfter = await getUserByUsername(APP_USERNAME);
expect(appUserAfter.status).to.be.equal('online');
});
it('should leave the app user unchanged when the update fails to apply', async () => {
const appUserBefore = await getUserByUsername(APP_USERNAME);
expect(appUserBefore.status).to.be.equal('online');
await request
.post(apps(`/${app.id}`))
.set(credentials)
.attach('app', appUpdateTestBroken)
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
});
const appUserAfter = await getUserByUsername(APP_USERNAME);
expect(appUserAfter.status).to.be.equal('online');
});
it('should take the app user offline when the updated app ends up disabled', async () => {
const appUserBefore = await getUserByUsername(APP_USERNAME);
expect(appUserBefore.status).to.be.equal('online');
await request
.post(apps(`/${app.id}`))
.set(credentials)
.attach('app', appUpdateTestFaulty)
.expect(200)
.expect((res) => {
expect(res.body.app).to.have.property('status', 'disabled');
});
const appUserAfter = await getUserByUsername(APP_USERNAME);
expect(appUserAfter.status).to.be.equal('offline');
});
});
});

View File

@@ -0,0 +1,167 @@
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import { getCredentials, request, credentials, api } from '../../data/api-data';
import { APP_URL, APP_NAME, apps } from '../../data/apps/apps-data';
import { cleanupApps } from '../../data/apps/helper';
import { updatePermission } from '../../data/permissions.helper';
import { getUserByUsername } from '../../data/users.helper';
import { IS_EE } from '../../e2e/config/constants';
const APP_USERNAME = 'appsrocketchattester.bot';
describe('Apps - Installation', () => {
before((done) => getCredentials(done));
before(async () => cleanupApps());
after(() => Promise.all([cleanupApps(), updatePermission('manage-apps', ['admin'])]));
let app: any;
describe('[Installation]', () => {
it('should throw an error when trying to install an app and the apps framework is enabled but the user does not have the permission', (done) => {
void updatePermission('manage-apps', []).then(() => {
void request
.post(apps())
.set(credentials)
.send({
url: APP_URL,
})
.expect('Content-Type', 'application/json')
.expect(403)
.expect((res) => {
expect(res.body).to.have.a.property('success', false);
expect(res.body.error).to.be.equal('User does not have the permissions required for this action [error-unauthorized]');
})
.end(done);
});
});
(IS_EE ? it : it.skip)('should succesfully install an app from a URL in EE, which should be auto-enabled', (done) => {
void updatePermission('manage-apps', ['admin']).then(() => {
void request
.post(apps())
.set(credentials)
.send({
url: APP_URL,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.a.property('success', true);
expect(res.body).to.have.a.property('app');
expect(res.body.app).to.have.a.property('id');
expect(res.body.app).to.have.a.property('version');
expect(res.body.app).to.have.a.property('status').and.to.be.equal('manually_enabled');
app = res.body.app;
})
.end(done);
});
});
(!IS_EE ? it : it.skip)('should succesfully install an app from a URL in CE, which should not be enabled', (done) => {
void updatePermission('manage-apps', ['admin']).then(() => {
void request
.post(apps())
.set(credentials)
.send({
url: APP_URL,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.a.property('success', true);
expect(res.body).to.have.a.property('app');
expect(res.body.app).to.have.a.property('id');
expect(res.body.app).to.have.a.property('version');
expect(res.body.app).to.have.a.property('status').and.to.be.equal('initialized');
app = res.body.app;
})
.end(done);
});
});
it('should have created the app user successfully', (done) => {
void getUserByUsername(APP_USERNAME)
.then((user) => {
expect(user.username).to.be.equal(APP_USERNAME);
})
.then(done);
});
it('should successfully get app details by id', (done) => {
void request
.get(apps(`/${app.id}`))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.a.property('success', true);
expect(res.body).to.have.a.property('app');
expect(res.body.app).to.have.a.property('name', APP_NAME);
expect(res.body.app).to.have.a.property('version');
expect(res.body.app).to.have.a.property('status');
})
.end(done);
});
it('should successfully get app status by id', (done) => {
void request
.get(apps(`/${app.id}/status`))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.a.property('success', true);
expect(res.body).to.have.a.property('status');
expect(res.body.status).to.not.be.empty;
})
.end(done);
});
(IS_EE ? describe : describe.skip)('Slash commands registration', () => {
it('should have created the "test-simple" slash command successfully', (done) => {
void request
.get(api('commands.get'))
.query({ command: 'test-simple' })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.a.property('success', true);
expect(res.body).to.have.a.property('command');
expect(res.body.command.command).to.be.equal('test-simple');
})
.end(done);
});
it('should have created the "test-with-arguments" slash command successfully', (done) => {
void request
.get(api('commands.get'))
.query({ command: 'test-with-arguments' })
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.a.property('success', true);
expect(res.body).to.have.a.property('command');
expect(res.body.command.command).to.be.equal('test-with-arguments');
})
.end(done);
});
});
(IS_EE ? describe : describe.skip)('Video Conf Provider registration', () => {
it('should have created two video conf provider successfully', (done) => {
void request
.get(api('video-conference.providers'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.a.property('success', true);
expect(res.body).to.have.a.property('data').that.is.an('array').with.lengthOf(3);
expect(res.body.data[0]).to.be.an('object').with.a.property('key').equal('test');
expect(res.body.data[1]).to.be.an('object').with.a.property('key').equal('persistentchat');
expect(res.body.data[2]).to.be.an('object').with.a.property('key').equal('unconfigured');
})
.end(done);
});
});
});
});

View File

@@ -0,0 +1,102 @@
import type { App } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { after, afterEach, before, describe, it } from 'mocha';
import { getCredentials, request, credentials } from '../../data/api-data';
import { appPresenceStateTest } from '../../data/apps/app-packages';
import { apps } from '../../data/apps/apps-data';
import { cleanupApps, installLocalTestPackage } from '../../data/apps/helper';
import { adminUsername } from '../../data/user';
import { getUserByUsername } from '../../data/users.helper';
import { IS_EE } from '../../e2e/config/constants';
(IS_EE ? describe : describe.skip)('Apps - Presence State Bridge', () => {
let app: App;
before((done) => getCredentials(done));
before(async () => {
await cleanupApps();
app = await installLocalTestPackage(appPresenceStateTest);
});
after(() => cleanupApps());
afterEach(async () => {
const user = await getUserByUsername(adminUsername);
await request
.post(apps(`/public/${app.id}/end-active-state`))
.set(credentials)
.send({ userId: user._id })
.expect(200);
});
describe('[setActiveState]', () => {
it('should set the user presence with status text and source', async () => {
const user = await getUserByUsername(adminUsername);
await request
.post(apps(`/public/${app.id}/set-active-state`))
.set(credentials)
.send({
userId: user._id,
statusDefault: 'busy',
statusText: 'In a meeting',
statusSource: 'internal',
})
.expect(200);
const updatedUser = await getUserByUsername(adminUsername);
expect(updatedUser.statusText).to.equal('In a meeting');
expect(updatedUser.statusSource).to.equal('internal');
});
});
describe('[endActiveState]', () => {
it('should restore the displaced same-priority state and reset once the queue is exhausted', async () => {
const user = await getUserByUsername(adminUsername);
await request
.post(apps(`/public/${app.id}/set-active-state`))
.set(credentials)
.send({
userId: user._id,
statusDefault: 'busy',
statusText: 'In a meeting',
statusSource: 'internal',
})
.expect(200);
await request
.post(apps(`/public/${app.id}/set-active-state`))
.set(credentials)
.send({
userId: user._id,
statusDefault: 'busy',
statusText: 'On a call',
statusSource: 'internal',
})
.expect(200);
await request
.post(apps(`/public/${app.id}/end-active-state`))
.set(credentials)
.send({ userId: user._id })
.expect(200);
const restoredUser = await getUserByUsername(adminUsername);
expect(restoredUser.statusText).to.equal('In a meeting');
expect(restoredUser.statusSource).to.equal('internal');
await request
.post(apps(`/public/${app.id}/end-active-state`))
.set(credentials)
.send({ userId: user._id })
.expect(200);
const clearedUser = await getUserByUsername(adminUsername);
expect(clearedUser.statusText).to.equal('');
expect(clearedUser.statusSource).to.be.undefined;
});
});
});

View File

@@ -0,0 +1,167 @@
import type { Credentials } from '@rocket.chat/api-client';
import type { App, IMessage, IRoom, IUser } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import { getCredentials, request, credentials } from '../../data/api-data';
import { apps } from '../../data/apps/apps-data';
import { cleanupApps, installTestApp } from '../../data/apps/helper';
import { getMessageById } from '../../data/chat.helper';
import { createRoom, deleteRoom } from '../../data/rooms.helper';
import { adminUsername, password } from '../../data/user';
import type { TestUser } from '../../data/users.helper';
import { createUser, deleteUser, login } from '../../data/users.helper';
import { IS_EE } from '../../e2e/config/constants';
(IS_EE ? describe : describe.skip)('Apps - Send Messages As User', () => {
let app: App;
before((done) => getCredentials(done));
before(async () => {
await cleanupApps();
app = await installTestApp();
});
after(() => cleanupApps());
describe('[Send Message as user]', () => {
it('should return an error when the room is not found', (done) => {
void request
.post(apps(`/public/${app.id}/send-message-as-user`))
.send({
roomId: 'invalid-room',
})
.set(credentials)
.expect(404)
.expect((err: unknown, res: undefined) => {
expect(err).to.have.a.property('error');
expect(res).to.be.equal(undefined);
expect((err as { error?: any }).error).to.have.a.property('text');
expect((err as { error?: any }).error.text).to.be.equal('Room "invalid-room" could not be found');
})
.end(done);
});
it('should return an error when the user is not found', (done) => {
void request
.post(apps(`/public/${app.id}/send-message-as-user?userId=invalid-user`))
.send({
roomId: 'GENERAL',
})
.set(credentials)
.expect(404)
.expect((err: unknown, res: undefined) => {
expect(err).to.have.a.property('error');
expect(res).to.be.equal(undefined);
expect((err as { error?: any }).error).to.have.a.property('text');
expect((err as { error?: any }).error.text).to.be.equal('User with id "invalid-user" could not be found');
})
.end(done);
});
describe('Send to a Public Channel', () => {
let publicMessageId: IMessage['_id'];
it('should send a message as app user', (done) => {
void request
.post(apps(`/public/${app.id}/send-message-as-user?userId=${adminUsername}`))
.set(credentials)
.send({
roomId: 'GENERAL',
})
.expect(200)
.expect((res) => {
const response = JSON.parse(res.text);
expect(response).to.have.a.property('messageId');
publicMessageId = response.messageId;
})
.end(done);
});
it('should be a valid message', async () => {
const message = await getMessageById({ msgId: publicMessageId });
expect(message.msg).to.be.equal('Executing send-message-as-user test endpoint');
});
});
describe('Send to a Private Channel', () => {
let privateMessageId: IMessage['_id'];
let group: IRoom;
let user: TestUser<IUser>;
let userCredentials: Credentials;
before(async () => {
group = (
await createRoom({
type: 'p',
name: `apps-e2etest-room-${Date.now()}`,
})
).body.group;
user = await createUser();
userCredentials = await login(user.username, password);
});
after(() => Promise.all([deleteRoom({ type: 'p', roomId: group._id }), deleteUser(user)]));
it('should return 500 when sending a message as user that has no permissions', (done) => {
void request
.post(apps(`/public/${app.id}/send-message-as-user?userId=${user._id}`))
.set(userCredentials)
.send({
roomId: group._id,
})
.expect(500)
.end(done);
});
it('should send a message as app user', (done) => {
void request
.post(apps(`/public/${app.id}/send-message-as-user?userId=${adminUsername}`))
.set(credentials)
.send({
roomId: group._id,
})
.expect(200)
.expect((res) => {
const response = JSON.parse(res.text);
expect(response).to.have.a.property('messageId');
privateMessageId = response.messageId;
})
.end(done);
});
it('should be a valid message', async () => {
const message = await getMessageById({ msgId: privateMessageId });
expect(message.msg).to.be.equal('Executing send-message-as-user test endpoint');
});
});
describe('Send to a DM Channel', () => {
let DMMessageId: IMessage['_id'];
let dmRoom: IRoom;
before(async () => {
dmRoom = (
await createRoom({
type: 'd',
username: 'rocket.cat',
})
).body.room;
});
after(() => deleteRoom({ type: 'd', roomId: dmRoom._id }));
it('should send a message as app user', (done) => {
void request
.post(apps(`/public/${app.id}/send-message-as-user?userId=${adminUsername}`))
.set(credentials)
.send({
roomId: dmRoom._id,
})
.expect(200)
.expect((res) => {
const response = JSON.parse(res.text);
expect(response).to.have.a.property('messageId');
DMMessageId = response.messageId;
})
.end(done);
});
it('should be a valid message', async () => {
const message = await getMessageById({ msgId: DMMessageId });
expect(message.msg).to.be.equal('Executing send-message-as-user test endpoint');
});
});
});
});

View File

@@ -0,0 +1,133 @@
import type { App, IMessage, IRoom } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import { getCredentials, request, credentials } from '../../data/api-data';
import { apps } from '../../data/apps/apps-data';
import { cleanupApps, installTestApp } from '../../data/apps/helper';
import { getMessageById } from '../../data/chat.helper';
import { createRoom, deleteRoom } from '../../data/rooms.helper';
import { IS_EE } from '../../e2e/config/constants';
(IS_EE ? describe : describe.skip)('Apps - Send Messages As APP User', () => {
let app: App;
before((done) => getCredentials(done));
before(async () => {
await cleanupApps();
app = await installTestApp();
});
after(() => cleanupApps());
describe('[Send Message as app user]', () => {
it('should return an error when the room is not found', (done) => {
void request
.post(apps(`/public/${app.id}/send-message-as-app-user`))
.send({
roomId: 'invalid-room',
})
.set(credentials)
.expect(404)
.expect((err: unknown, res: undefined) => {
expect(err).to.have.a.property('error');
expect(res).to.be.equal(undefined);
expect((err as { error?: any }).error).to.have.a.property('text');
expect((err as { error?: any }).error.text).to.be.equal('Room "invalid-room" could not be found');
})
.end(done);
});
describe('Send to a Public Channel', () => {
let publicMessageId: IMessage['_id'];
it('should send a message as app user', (done) => {
void request
.post(apps(`/public/${app.id}/send-message-as-app-user`))
.set(credentials)
.send({
roomId: 'GENERAL',
})
.expect(200)
.expect((res) => {
const response = JSON.parse(res.text);
expect(response).to.have.a.property('messageId');
publicMessageId = response.messageId;
})
.end(done);
});
it('should be a valid message', async () => {
const message = await getMessageById({ msgId: publicMessageId });
expect(message.msg).to.be.equal('Executing send-message-as-app-user test endpoint');
});
});
describe('Send to a Private Channel', () => {
let privateMessageId: IMessage['_id'];
let group: IRoom;
before(async () => {
group = (
await createRoom({
type: 'p',
name: `apps-e2etest-room-${Date.now()}`,
})
).body.group;
});
after(() => deleteRoom({ type: 'p', roomId: group._id }));
it('should send a message as app user', (done) => {
void request
.post(apps(`/public/${app.id}/send-message-as-app-user`))
.set(credentials)
.send({
roomId: group._id,
})
.expect(200)
.expect((res) => {
const response = JSON.parse(res.text);
expect(response).to.have.a.property('messageId');
privateMessageId = response.messageId;
})
.end(done);
});
it('should be a valid message', async () => {
const message = await getMessageById({ msgId: privateMessageId });
expect(message.msg).to.be.equal('Executing send-message-as-app-user test endpoint');
});
});
describe('Send to a DM Channel', () => {
let DMMessageId: IMessage['_id'];
let dmRoom: IRoom;
before(async () => {
dmRoom = (
await createRoom({
type: 'd',
username: 'rocket.cat',
})
).body.room;
});
after(() => deleteRoom({ type: 'd', roomId: dmRoom._id }));
it('should send a message as app user', (done) => {
void request
.post(apps(`/public/${app.id}/send-message-as-app-user`))
.set(credentials)
.send({
roomId: dmRoom._id,
})
.expect(200)
.expect((res) => {
const response = JSON.parse(res.text);
expect(response).to.have.a.property('messageId');
DMMessageId = response.messageId;
})
.end(done);
});
it('should be a valid message', async () => {
const message = await getMessageById({ msgId: DMMessageId });
expect(message.msg).to.be.equal('Executing send-message-as-app-user test endpoint');
});
});
});
});

View File

@@ -0,0 +1,81 @@
import type { IMessage } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import { getCredentials, request, credentials, api } from '../../data/api-data';
import { cleanupApps, installTestApp } from '../../data/apps/helper';
import { IS_EE } from '../../e2e/config/constants';
(IS_EE ? describe : describe.skip)('Apps - Slash Command "test-simple"', () => {
before((done) => getCredentials(done));
before(async () => {
await cleanupApps();
await installTestApp();
});
after(() => cleanupApps());
describe('[Slash command "test-simple"]', () => {
it('should return an error when no command is provided', (done) => {
void request
.post(api('commands.run'))
.send({
roomId: 'GENERAL',
command: null,
})
.set(credentials)
.expect(400)
.expect((res) => {
expect(res.body).to.have.a.property('success', false);
expect(res.body.error).to.include('must NOT have fewer than 1 characters');
})
.end(done);
});
it('should return an error when the command does not exist', (done) => {
void request
.post(api('commands.run'))
.send({
roomId: 'GENERAL',
command: 'invalid-command',
})
.set(credentials)
.expect(400)
.expect((res) => {
expect(res.body).to.have.a.property('success', false);
expect(res.body.error).to.be.equal('The command provided does not exist (or is disabled).');
})
.end(done);
});
it('should execute the slash command successfully', (done) => {
void request
.post(api('commands.run'))
.send({
roomId: 'GENERAL',
command: 'test-simple',
})
.set(credentials)
.expect(200)
.expect((res) => {
expect(res.body).to.have.a.property('success', true);
})
.end(done);
});
it('should have sent the message correctly', (done) => {
void request
.get(api('chat.search'))
.query({
roomId: 'GENERAL',
searchText: "Slashcommand 'test-simple' successfully executed",
})
.set(credentials)
.expect(200)
.expect((res) => {
const message = (res.body.messages as IMessage[]).find(
(message) => message.msg === "Slashcommand 'test-simple' successfully executed",
);
expect(message).to.not.be.equal(undefined);
})
.end(done);
});
});
});

View File

@@ -0,0 +1,52 @@
import type { IMessage } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import { getCredentials, request, credentials, api } from '../../data/api-data';
import { cleanupApps, installTestApp } from '../../data/apps/helper';
import { IS_EE } from '../../e2e/config/constants';
(IS_EE ? describe : describe.skip)('Apps - Slash Command "test-with-arguments"', () => {
before((done) => getCredentials(done));
before(async () => {
await cleanupApps();
await installTestApp();
});
after(() => cleanupApps());
describe('[Slash command "test-with-arguments"]', () => {
const params = 'argument';
it('should execute the slash command successfully', (done) => {
void request
.post(api('commands.run'))
.send({
roomId: 'GENERAL',
command: 'test-with-arguments',
params,
})
.set(credentials)
.expect(200)
.expect((res) => {
expect(res.body).to.have.a.property('success', true);
})
.end(done);
});
it('should have sent the message correctly', (done) => {
const searchText = `Slashcommand \'test-with-arguments\' successfully executed with arguments: "${params}"`;
void request
.get(api('chat.search'))
.query({
roomId: 'GENERAL',
searchText,
})
.set(credentials)
.expect(200)
.expect((res) => {
const message = (res.body.messages as IMessage[]).find((message) => message.msg === searchText);
expect(message).to.not.be.equal(undefined);
})
.end(done);
});
});
});

Some files were not shown because too many files have changed in this diff Show More