import type { Credentials } from '@rocket.chat/api-client'; import type { IMessage, IRoom, ISubscription, IThreadMessage, IUser } from '@rocket.chat/core-typings'; import { Random } from '@rocket.chat/random'; import { expect } from 'chai'; import { after, before, beforeEach, describe, it } from 'mocha'; import type { Response } from 'supertest'; import { retry } from './helpers/retry'; import { sleep } from '../../../lib/utils/sleep'; import { getCredentials, api, request, credentials, apiUrl } from '../../data/api-data'; import { followMessage, sendMessage, sendSimpleMessage, deleteMessage, updateMessage } from '../../data/chat.helper'; import { imgURL } from '../../data/interactions'; import { mockServerHealthy, mockServerReset, mockServerSet } from '../../data/mock-server.helper'; import { updatePermission, updateSetting } from '../../data/permissions.helper'; import { addUserToRoom, createRoom, deleteRoom, getSubscriptionByRoomId } 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 pinMessage = ({ msgId }: { msgId: IMessage['_id'] }) => { if (!msgId) { throw new Error('"msgId" is required in "pinMessage" test helper'); } return request.post(api('chat.pinMessage')).set(credentials).send({ messageId: msgId, }); }; describe('[Chat]', () => { let testChannel: IRoom; let message: { _id: IMessage['_id'] }; let protectedChannel: IRoom; before((done) => getCredentials(done)); before(async () => { testChannel = (await createRoom({ type: 'c', name: `chat.api-test-${Date.now()}` })).body.channel; protectedChannel = (await createRoom({ type: 'c', name: `chat.api-protected-test-${Date.now()}` })).body.channel; await request .post(api('rooms.saveRoomSettings')) .set(credentials) .send({ rid: protectedChannel._id, joinCode: 'super-secret-password', }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); }); }); after(async () => { await deleteRoom({ type: 'c', roomId: testChannel._id }); await deleteRoom({ type: 'c', roomId: protectedChannel._id }); }); describe('/chat.postMessage', () => { it('should throw an error when at least one of required parameters(channel, roomId) is not sent', async () => { const res = await request .post(api('chat.postMessage')) .set(credentials) .send({ text: 'Sample message', alias: 'Gruggy', emoji: ':smirk:', avatar: 'http://res.guggy.com/logo_128.png', }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); }); it('should throw an error when it has some properties with the wrong type(attachments.title_link_download, attachments.fields, message_link)', async () => { const res = await request .post(api('chat.postMessage')) .set(credentials) .send({ channel: testChannel.name, alias: 'Gruggy', text: 'Sample message', emoji: ':smirk:', avatar: 'http://res.guggy.com/logo_128.png', attachments: [ { color: '#ff0000', text: 'Yay for gruggy!', ts: '2016-12-09T16:53:06.761Z', thumb_url: 'http://res.guggy.com/logo_128.png', message_link: 12, collapsed: false, author_name: 'Bradley Hilton', author_link: 'https://rocket.chat/', author_icon: 'https://avatars.githubusercontent.com/u/850391?v=3', title: 'Attachment Example', title_link: 'https://youtube.com', title_link_download: 'https://youtube.com', image_url: 'http://res.guggy.com/logo_128.png', audio_url: 'http://www.w3schools.com/tags/horse.mp3', video_url: 'http://www.w3schools.com/tags/movie.mp4', fields: '', }, ], }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error'); }); describe('should throw an error when the sensitive properties contain malicious XSS values', () => { it('attachment.message_link', () => void request .post(api('chat.postMessage')) .set(credentials) .send({ channel: testChannel.name, alias: 'Gruggy', text: 'Sample message', avatar: 'http://res.guggy.com/logo_128.png', emoji: ':smirk:', attachments: [ { color: '#ff0000', text: 'Yay for gruggy!', ts: '2016-12-09T16:53:06.761Z', thumb_url: 'http://res.guggy.com/logo_128.png', message_link: 'javascript:alert("xss")', }, ], }) .expect('Content-Type', 'application/json') .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error'); })); it('attachment.author_link', () => void request .post(api('chat.postMessage')) .set(credentials) .send({ channel: testChannel.name, text: 'Sample message', alias: 'Gruggy', emoji: ':smirk:', avatar: 'http://res.guggy.com/logo_128.png', attachments: [ { color: '#ff0000', text: 'Yay for gruggy!', ts: '2016-12-09T16:53:06.761Z', thumb_url: 'http://res.guggy.com/logo_128.png', author_link: 'javascript:alert("xss")', }, ], }) .expect('Content-Type', 'application/json') .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error'); })); it('attachment.title_link', () => void request .post(api('chat.postMessage')) .set(credentials) .send({ channel: testChannel.name, text: 'Sample message', alias: 'Gruggy', emoji: ':smirk:', avatar: 'http://res.guggy.com/logo_128.png', attachments: [ { color: '#ff0000', text: 'Yay for gruggy!', ts: '2016-12-09T16:53:06.761Z', title: 'Attachment Example', title_link: 'javascript:alert("xss")', }, ], }) .expect('Content-Type', 'application/json') .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error'); })); it('attachment.action.url', () => void request .post(api('chat.postMessage')) .set(credentials) .send({ channel: testChannel.name, text: 'Sample message', alias: 'Gruggy', emoji: ':smirk:', avatar: 'http://res.guggy.com/logo_128.png', attachments: [ { color: '#ff0000', text: 'Yay for gruggy!', ts: '2016-12-09T16:53:06.761Z', title: 'Attachment Example', title_link: 'https://youtube.com', actions: [ { type: 'button', text: 'Text', url: 'javascript:alert("xss")', }, ], }, ], }) .expect('Content-Type', 'application/json') .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error'); })); it('message.avatar', () => void request .post(api('chat.postMessage')) .set(credentials) .send({ channel: testChannel.name, text: 'Sample message', emoji: ':smirk:', avatar: 'javascript:alert("xss")', alias: 'Gruggy', attachments: [ { color: '#ff0000', text: 'Yay for gruggy!', ts: '2016-12-09T16:53:06.761Z', title: 'Attachment Example', title_link: 'https://youtube.com', actions: [ { type: 'button', text: 'Text', url: 'https://youtube.com', }, ], }, ], }) .expect('Content-Type', 'application/json') .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error'); })); it('attachment.action.image_url', () => void request .post(api('chat.postMessage')) .set(credentials) .send({ channel: testChannel.name, text: 'Sample message', alias: 'Gruggy', emoji: ':smirk:', avatar: 'http://res.guggy.com/logo_128.png', attachments: [ { color: '#ff0000', text: 'Yay for gruggy!', ts: '2016-12-09T16:53:06.761Z', title: 'Attachment Example', title_link: 'https://youtube.com', actions: [ { type: 'button', text: 'Text', url: 'http://res.guggy.com/logo_128.png', image_url: 'javascript:alert("xss")', }, ], }, ], }) .expect('Content-Type', 'application/json') .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error'); })); it('attachment.thumb_url', () => void request .post(api('chat.postMessage')) .set(credentials) .send({ channel: testChannel.name, text: 'Sample message', emoji: ':smirk:', alias: 'Gruggy', avatar: 'http://res.guggy.com/logo_128.png', attachments: [ { color: '#ff0000', text: 'Yay for gruggy!', ts: '2016-12-09T16:53:06.761Z', title: 'Attachment Example', thumb_url: 'javascript:alert("xss")', title_link: 'http://res.guggy.com/logo_128.png', }, ], }) .expect('Content-Type', 'application/json') .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error'); })); it('attachment.author_icon', () => void request .post(api('chat.postMessage')) .set(credentials) .send({ channel: testChannel.name, text: 'Sample message', emoji: ':smirk:', alias: 'Gruggy', avatar: 'http://res.guggy.com/logo_128.png', attachments: [ { color: '#ff0000', text: 'Yay for gruggy!', ts: '2016-12-09T16:53:06.761Z', title: 'Attachment Example', author_icon: 'javascript:alert("xss")', title_link: 'http://res.guggy.com/logo_128.png', }, ], }) .expect('Content-Type', 'application/json') .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error'); })); it('attachment.image_url', () => void request .post(api('chat.postMessage')) .set(credentials) .send({ channel: testChannel.name, text: 'Sample message', alias: 'Gruggy', emoji: ':smirk:', avatar: 'http://res.guggy.com/logo_128.png', attachments: [ { color: '#ff0000', text: 'Yay for gruggy!', ts: '2016-12-09T16:53:06.761Z', title: 'Attachment Example', image_url: 'javascript:alert("xss")', title_link: 'http://res.guggy.com/logo_128.png', }, ], }) .expect('Content-Type', 'application/json') .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error'); })); it('attachment.audio_url', () => void request .post(api('chat.postMessage')) .set(credentials) .send({ channel: testChannel.name, text: 'Sample message', alias: 'Gruggy', emoji: ':smirk:', avatar: 'http://res.guggy.com/logo_128.png', attachments: [ { color: '#ff0000', text: 'Yay for gruggy!', ts: '2016-12-09T16:53:06.761Z', title: 'Attachment Example', audio_url: 'javascript:alert("xss")', title_link: 'http://res.guggy.com/logo_128.png', }, ], }) .expect('Content-Type', 'application/json') .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error'); })); it('attachment.video_url', () => void request .post(api('chat.postMessage')) .set(credentials) .send({ channel: testChannel.name, alias: 'Gruggy', text: 'Sample message', emoji: ':smirk:', avatar: 'http://res.guggy.com/logo_128.png', attachments: [ { color: '#ff0000', text: 'Yay for gruggy!', ts: '2016-12-09T16:53:06.761Z', title: 'Attachment Example', video_url: 'javascript:alert("xss")', title_link: 'http://res.guggy.com/logo_128.png', }, ], }) .expect('Content-Type', 'application/json') .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error'); })); }); it('should throw an error when the properties (attachments.fields.title, attachments.fields.value) are with the wrong type', async () => { const res = await request .post(api('chat.postMessage')) .set(credentials) .send({ channel: testChannel.name, text: 'Sample message', emoji: ':smirk:', alias: 'Gruggy', avatar: 'http://res.guggy.com/logo_128.png', attachments: [ { color: '#ff0000', text: 'Yay for gruggy!', ts: '2016-12-09T16:53:06.761Z', thumb_url: 'http://res.guggy.com/logo_128.png', message_link: 'https://google.com', collapsed: false, author_name: 'Bradley Hilton', author_link: 'https://rocket.chat/', author_icon: 'https://avatars.githubusercontent.com/u/850391?v=3', title: 'Attachment Example', title_link: 'https://youtube.com', title_link_download: true, image_url: 'http://res.guggy.com/logo_128.png', audio_url: 'http://www.w3schools.com/tags/horse.mp3', video_url: 'http://www.w3schools.com/tags/movie.mp4', fields: [ { short: true, title: 12, value: false, }, ], }, ], }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error'); }); it('should throw an error when the properties (attachments.fields.title) is missing', async () => { const res = await request .post(api('chat.postMessage')) .set(credentials) .send({ channel: testChannel.name, text: 'Sample message', emoji: ':smirk:', alias: 'Gruggy', avatar: 'http://res.guggy.com/logo_128.png', attachments: [ { color: '#ff0000', text: 'Yay for gruggy!', ts: '2016-12-09T16:53:06.761Z', thumb_url: 'http://res.guggy.com/logo_128.png', message_link: 'https://google.com', collapsed: false, author_name: 'Bradley Hilton', author_link: 'https://rocket.chat/', author_icon: 'https://avatars.githubusercontent.com/u/850391?v=3', title: 'Attachment Example', title_link: 'https://youtube.com', title_link_download: true, image_url: 'http://res.guggy.com/logo_128.png', audio_url: 'http://www.w3schools.com/tags/horse.mp3', video_url: 'http://www.w3schools.com/tags/movie.mp4', fields: [ { short: true, value: 'This is attachment field value', }, ], }, ], }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error'); }); it('should throw an error when the properties (attachments.fields.value) is missing', async () => { const res = await request .post(api('chat.postMessage')) .set(credentials) .send({ channel: testChannel.name, text: 'Sample message', emoji: ':smirk:', alias: 'Gruggy', avatar: 'http://res.guggy.com/logo_128.png', attachments: [ { color: '#ff0000', text: 'Yay for gruggy!', ts: '2016-12-09T16:53:06.761Z', thumb_url: 'http://res.guggy.com/logo_128.png', message_link: 'https://google.com', collapsed: false, author_name: 'Bradley Hilton', author_link: 'https://rocket.chat/', author_icon: 'https://avatars.githubusercontent.com/u/850391?v=3', title: 'Attachment Example', title_link: 'https://youtube.com', title_link_download: true, image_url: 'http://res.guggy.com/logo_128.png', audio_url: 'http://www.w3schools.com/tags/horse.mp3', video_url: 'http://www.w3schools.com/tags/movie.mp4', fields: [ { short: true, title: 'This is attachment field title', }, ], }, ], }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error'); }); it('attachment.fields should work fine when value and title are provided', async () => { const res = await request .post(api('chat.postMessage')) .set(credentials) .send({ channel: testChannel.name, text: 'Sample message', attachments: [ { text: 'This is attachment field', color: '#764FA5', fields: [{ short: true, value: 'This is value', title: 'This is title' }], }, ], }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); expect(res.body).to.not.have.property('error'); expect(res.body).to.have.nested.property('message.msg', 'Sample message'); expect(res.body).to.have.nested.property('message.attachments').to.be.an('array'); expect(res.body).to.have.nested.property('message.attachments[0].fields').to.be.an('array'); expect(res.body).to.have.nested.property('message.attachments[0].fields[0].short', true); expect(res.body).to.have.nested.property('message.attachments[0].fields[0].value', 'This is value'); expect(res.body).to.have.nested.property('message.attachments[0].fields[0].title', 'This is title'); }); it('should allow forwarding a message into the same password protected room', async () => { const postResponse = await request .post(api('chat.postMessage')) .set(credentials) .send({ roomId: [protectedChannel._id], text: 'Message to be forwarded', }) .expect('Content-Type', 'application/json') .expect(200); expect(postResponse.body).to.have.property('success', true); const originalMessageId = postResponse.body.message._id as IMessage['_id']; const forwardResponse = await request .post(api('chat.postMessage')) .set(credentials) .send({ roomId: [protectedChannel._id], text: `[](http://localhost:3000/channel/${protectedChannel.name}?msg=${originalMessageId}`, }) .expect('Content-Type', 'application/json') .expect(200); expect(forwardResponse.body).to.have.property('success', true); expect(forwardResponse.body).to.have.nested.property('message.rid', protectedChannel._id); }); it('should return statusCode 200 when postMessage successfully', async () => { const res = await request .post(api('chat.postMessage')) .set(credentials) .send({ channel: testChannel.name, text: 'Sample message', emoji: ':smirk:', attachments: [ { color: '#ff0000', text: 'Yay for gruggy!', ts: '2016-12-09T16:53:06.761Z', thumb_url: 'http://res.guggy.com/logo_128.png', message_link: 'https://google.com', collapsed: false, author_name: 'Bradley Hilton', author_link: 'https://rocket.chat/', author_icon: 'https://avatars.githubusercontent.com/u/850391?v=3', title: 'Attachment Example', title_link: 'https://youtube.com', title_link_download: true, image_url: 'http://res.guggy.com/logo_128.png', audio_url: 'http://www.w3schools.com/tags/horse.mp3', video_url: 'http://www.w3schools.com/tags/movie.mp4', fields: [ { short: true, title: 'Test', value: 'Testing out something or other', }, { short: true, title: 'Another Test', value: '[Link](https://google.com/) something and this and that.', }, ], }, ], }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); expect(res.body).to.have.nested.property('message.msg', 'Sample message'); message = { _id: res.body.message._id }; }); it('should not parse urls when parseUrls=false is provided', async () => { return request .post(api('chat.postMessage')) .set(credentials) .send({ channel: testChannel.name, text: apiUrl, parseUrls: false, }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.nested.property('message.msg', apiUrl); expect(res.body.message).to.not.have.property('urls'); }); }); it('should parse urls when parseUrls=true is provided', async () => { return request .post(api('chat.postMessage')) .set(credentials) .send({ channel: testChannel.name, text: apiUrl, parseUrls: true, }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.nested.property('message.msg', apiUrl); expect(res.body.message).to.have.property('urls'); }); }); it('should parse urls when parseUrls is not provided', async () => { return request .post(api('chat.postMessage')) .set(credentials) .send({ channel: testChannel.name, text: apiUrl, parseUrls: undefined, }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.nested.property('message.msg', apiUrl); expect(res.body.message).to.have.property('urls'); }); }); describe('text message allowed size', () => { before(async () => { await updateSetting('Message_MaxAllowedSize', 10); }); after(async () => { await updateSetting('Message_MaxAllowedSize', 5000); }); it('should return an error if text parameter surpasses the maximum allowed size', async () => { const res = await request .post(api('chat.postMessage')) .set(credentials) .send({ channel: '#general', text: 'Text to test max limit allowed', alias: 'Gruggy', emoji: ':smirk:', avatar: 'http://res.guggy.com/logo_128.png', }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error', 'error-message-size-exceeded'); }); it('should return an error if text parameter in the first attachment surpasses the maximum allowed size', async () => { const res = await request .post(api('chat.postMessage')) .set(credentials) .send({ channel: testChannel.name, text: 'Yay!', emoji: ':smirk:', attachments: [ { color: '#ff0000', text: 'Text to test max limit allowed', ts: '2016-12-09T16:53:06.761Z', thumb_url: 'http://res.guggy.com/logo_128.png', message_link: 'https://google.com', collapsed: false, author_name: 'Bradley Hilton', author_link: 'https://rocket.chat/', author_icon: 'https://avatars.githubusercontent.com/u/850391?v=3', title: 'Attachment Example', title_link: 'https://youtube.com', title_link_download: true, image_url: 'http://res.guggy.com/logo_128.png', audio_url: 'http://www.w3schools.com/tags/horse.mp3', video_url: 'http://www.w3schools.com/tags/movie.mp4', fields: [], }, ], }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error', 'error-message-size-exceeded'); }); it('should return an error if text parameter in any of the attachments surpasses the maximum allowed size', async () => { const res = await request .post(api('chat.postMessage')) .set(credentials) .send({ channel: testChannel.name, text: 'Yay!', emoji: ':smirk:', attachments: [ { color: '#ff0000', text: 'Yay!', ts: '2016-12-09T16:53:06.761Z', thumb_url: 'http://res.guggy.com/logo_128.png', message_link: 'https://google.com', collapsed: false, author_name: 'Bradley Hilton', author_link: 'https://rocket.chat/', author_icon: 'https://avatars.githubusercontent.com/u/850391?v=3', title: 'Attachment Example', title_link: 'https://youtube.com', title_link_download: true, image_url: 'http://res.guggy.com/logo_128.png', audio_url: 'http://www.w3schools.com/tags/horse.mp3', video_url: 'http://www.w3schools.com/tags/movie.mp4', fields: [], }, { color: '#ff0000', text: 'Text to large to test max limit allowed', ts: '2016-12-09T16:53:06.761Z', thumb_url: 'http://res.guggy.com/logo_128.png', message_link: 'https://google.com', collapsed: false, author_name: 'Bradley Hilton', author_link: 'https://rocket.chat/', author_icon: 'https://avatars.githubusercontent.com/u/850391?v=3', title: 'Attachment Example', title_link: 'https://youtube.com', title_link_download: true, image_url: 'http://res.guggy.com/logo_128.png', audio_url: 'http://www.w3schools.com/tags/horse.mp3', video_url: 'http://www.w3schools.com/tags/movie.mp4', fields: [], }, ], }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error', 'error-message-size-exceeded'); }); it('should pass if any text parameter length does not surpasses the maximum allowed size', async () => { const res = await request .post(api('chat.postMessage')) .set(credentials) .send({ channel: testChannel.name, text: 'Sample', emoji: ':smirk:', attachments: [ { color: '#ff0000', text: 'Sample', ts: '2016-12-09T16:53:06.761Z', thumb_url: 'http://res.guggy.com/logo_128.png', message_link: 'https://google.com', collapsed: false, author_name: 'Bradley Hilton', author_link: 'https://rocket.chat/', author_icon: 'https://avatars.githubusercontent.com/u/850391?v=3', title: 'Attachment Example', title_link: 'https://youtube.com', title_link_download: true, image_url: 'http://res.guggy.com/logo_128.png', audio_url: 'http://www.w3schools.com/tags/horse.mp3', video_url: 'http://www.w3schools.com/tags/movie.mp4', fields: [ { short: true, title: 'Test', value: 'Testing out something or other', }, { short: true, title: 'Another Test', value: '[Link](https://google.com/) something and this and that.', }, ], }, ], }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); expect(res.body).to.have.nested.property('message.msg', 'Sample'); }); }); describe('Archived rooms', () => { let archivedChannel: IRoom; before(async () => { archivedChannel = (await createRoom({ type: 'c', name: `chat.api-archived-post-test-${Date.now()}` })).body.channel; await request.post(api('channels.archive')).set(credentials).send({ roomId: archivedChannel._id }); }); after(async () => { await request.post(api('channels.unarchive')).set(credentials).send({ roomId: archivedChannel._id }); await deleteRoom({ type: 'c', roomId: archivedChannel._id }); }); it('should fail to post a message to an archived room', async () => { const res = await request .post(api('chat.postMessage')) .set(credentials) .send({ roomId: archivedChannel._id, text: 'This message should not be posted', }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error', 'room_is_archived'); }); }); }); describe('/chat.getMessage', () => { it('should retrieve the message successfully', async () => { const res = await request .get(api('chat.getMessage')) .set(credentials) .query({ msgId: message._id, }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); expect(res.body).to.have.nested.property('message._id', message._id); }); }); describe('/chat.sendMessage', () => { it("should throw an error when the required param 'rid' is not sent", async () => { const res = await request .post(api('chat.sendMessage')) .set(credentials) .send({ message: { text: 'Sample message', alias: 'Gruggy', emoji: ':smirk:', avatar: 'http://res.guggy.com/logo_128.png', }, }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error', "The 'rid' property on the message object is missing."); }); describe('should throw an error when the sensitive properties contain malicious XSS values', () => { it('attachment.message_link', () => void request .post(api('chat.postMessage')) .set(credentials) .send({ channel: testChannel.name, alias: 'Gruggy', text: 'Sample message', emoji: ':smirk:', avatar: 'http://res.guggy.com/logo_128.png', attachments: [ { color: '#ff0000', text: 'Yay for gruggy!', ts: '2016-12-09T16:53:06.761Z', thumb_url: 'http://res.guggy.com/logo_128.png', message_link: 'javascript:alert("xss")', }, ], }) .expect('Content-Type', 'application/json') .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error'); })); it('attachment.author_link', () => void request .post(api('chat.postMessage')) .set(credentials) .send({ channel: testChannel.name, text: 'Sample message', alias: 'Gruggy', emoji: ':smirk:', avatar: 'http://res.guggy.com/logo_128.png', attachments: [ { color: '#ff0000', text: 'Yay for gruggy!', ts: '2016-12-09T16:53:06.761Z', thumb_url: 'http://res.guggy.com/logo_128.png', author_link: 'javascript:alert("xss")', }, ], }) .expect('Content-Type', 'application/json') .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error'); })); it('attachment.title_link', () => void request .post(api('chat.postMessage')) .set(credentials) .send({ channel: testChannel.name, text: 'Sample message', alias: 'Gruggy', emoji: ':smirk:', avatar: 'http://res.guggy.com/logo_128.png', attachments: [ { color: '#ff0000', text: 'Yay for gruggy!', ts: '2016-12-09T16:53:06.761Z', title: 'Attachment Example', title_link: 'javascript:alert("xss")', }, ], }) .expect('Content-Type', 'application/json') .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error'); })); it('attachment.action.url', () => void request .post(api('chat.postMessage')) .set(credentials) .send({ channel: testChannel.name, text: 'Sample message', alias: 'Gruggy', emoji: ':smirk:', avatar: 'http://res.guggy.com/logo_128.png', attachments: [ { color: '#ff0000', text: 'Yay for gruggy!', ts: '2016-12-09T16:53:06.761Z', title: 'Attachment Example', title_link: 'https://youtube.com', actions: [ { type: 'button', text: 'Text', url: 'javascript:alert("xss")', }, ], }, ], }) .expect('Content-Type', 'application/json') .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error'); })); }); it('should throw an error when it has some properties with the wrong type(attachments.title_link_download, attachments.fields, message_link)', async () => { const res = await request .post(api('chat.sendMessage')) .set(credentials) .send({ message: { channel: testChannel.name, text: 'Sample message', alias: 'Gruggy', emoji: ':smirk:', avatar: 'http://res.guggy.com/logo_128.png', attachments: [ { color: '#ff0000', text: 'Yay for gruggy!', ts: '2016-12-09T16:53:06.761Z', thumb_url: 'http://res.guggy.com/logo_128.png', message_link: 12, collapsed: false, author_name: 'Bradley Hilton', author_link: 'https://rocket.chat/', author_icon: 'https://avatars.githubusercontent.com/u/850391?v=3', title: 'Attachment Example', title_link: 'https://youtube.com', title_link_download: 'https://youtube.com', image_url: 'http://res.guggy.com/logo_128.png', audio_url: 'http://www.w3schools.com/tags/horse.mp3', video_url: 'http://www.w3schools.com/tags/movie.mp4', fields: '', }, ], }, }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error'); }); it('should send a message successfully', (done) => { message._id = `id-${Date.now()}`; void request .post(api('chat.sendMessage')) .set(credentials) .send({ message: { _id: message._id, rid: testChannel._id, msg: 'Sample message', emoji: ':smirk:', attachments: [ { color: '#ff0000', text: 'Yay for gruggy!', ts: '2016-12-09T16:53:06.761Z', thumb_url: 'http://res.guggy.com/logo_128.png', message_link: 'https://google.com', collapsed: false, author_name: 'Bradley Hilton', author_link: 'https://rocket.chat/', author_icon: 'https://avatars.githubusercontent.com/u/850391?v=3', title: 'Attachment Example', title_link: 'https://youtube.com', title_link_download: true, image_url: 'http://res.guggy.com/logo_128.png', audio_url: 'http://www.w3schools.com/tags/horse.mp3', video_url: 'http://www.w3schools.com/tags/movie.mp4', fields: [ { short: true, title: 'Test', value: 'Testing out something or other', }, { short: true, title: 'Another Test', value: '[Link](https://google.com/) something and this and that.', }, ], }, ], }, }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.nested.property('message.msg', 'Sample message'); }) .end(done); }); describe('Bad words filter', () => { before(() => Promise.all([updateSetting('Message_AllowBadWordsFilter', true), updateSetting('Message_BadWordsFilterList', 'badword,badword2')]), ); after(() => Promise.all([updateSetting('Message_AllowBadWordsFilter', false), updateSetting('Message_BadWordsFilterList', '')])); it('should censor bad words on send', async () => { const badMessage = { _id: Random.id(), rid: testChannel._id, msg: 'This message has badword badword2', }; await request .post(api('chat.sendMessage')) .set(credentials) .send({ message: badMessage }) .expect(200) .expect('Content-Type', 'application/json') .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('message'); const { message } = res.body; expect(message).to.have.property('msg', 'This message has ******* ********'); expect(message).to.have.property('md').to.be.an('array').that.has.lengthOf(1); const para = message.md[0]; expect(para).to.have.property('value').to.be.an('array').that.has.lengthOf(1); const text = para.value[0]; expect(text).to.have.property('value', 'This message has ******* ********'); }); }); }); describe('Archived rooms', () => { let archivedChannel: IRoom; before(async () => { archivedChannel = (await createRoom({ type: 'c', name: `chat.api-archived-test-${Date.now()}` })).body.channel; await request.post(api('channels.archive')).set(credentials).send({ roomId: archivedChannel._id }); }); after(async () => { await request.post(api('channels.unarchive')).set(credentials).send({ roomId: archivedChannel._id }); await deleteRoom({ type: 'c', roomId: archivedChannel._id }); }); it('should fail to send a message to an archived room', async () => { const res = await request .post(api('chat.sendMessage')) .set(credentials) .send({ message: { rid: archivedChannel._id, msg: 'This message should not be sent', }, }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error', 'room_is_archived'); }); }); describe('oembed', () => { // the TEST_MODE oembed provider (server/services/messages/lib/oembed/providers.ts) resolves /video/ URLs through GET /oembed on the mock-server const mockVideoUrl = (id: string) => `http://mock-server.dev:8080/video/${id}`; const mockPageUrl = (id: string) => `http://mock-server.dev:8080/page/${id}`; const mockOembedEndpoint = { method: 'GET', path: '/oembed' }; // the whole oembed pipeline runs against the local mock-server, so a fixed wait suffices — exceeding it means a CI perf problem worth surfacing const oembedProcessingMs = 500; const youtubeOembedResponse = { title: 'Rocket.Chat Demo', author_name: 'Rocket.Chat', author_url: 'https://www.youtube.com/@RocketChatApp', type: 'video', height: 150, width: 200, version: '1.0', provider_name: 'YouTube', provider_url: 'https://www.youtube.com/', thumbnail_height: 360, thumbnail_width: 480, thumbnail_url: 'https://i.ytimg.com/vi/T2v29gK8fP4/hqdefault.jpg', html: '', }; const spotifyOembedResponse = { html: '', iframe_url: 'https://open.spotify.com/embed/track/4cOdK2wGLETKBW3PvgPWqT?utm_source=oembed', width: 456, height: 152, version: '1.0', provider_name: 'Spotify', provider_url: 'https://spotify.com', type: 'rich', title: 'Never Gonna Give You Up', thumbnail_url: 'https://image-cdn-ak.spotifycdn.com/image/ab67616d00001e0215ebbedaacef61af244262a8', thumbnail_width: 300, thumbnail_height: 300, }; let ytEmbedMsgId: IMessage['_id']; let imgUrlMsgId: IMessage['_id']; before(async () => { const healthy = await mockServerHealthy(); expect(healthy, 'mock-server is not reachable — ensure it is running').to.be.true; await mockServerReset(); }); // programmed responses queue per key and an infinite (times: 0) entry never expires, so each test starts from a clean mock afterEach(() => mockServerReset()); before(() => Promise.all([ updateSetting('API_EmbedIgnoredHosts', ''), updateSetting('API_EmbedSafePorts', '80, 443, 3000, 8080'), updateSetting('SSRF_Allowlist', '127.0.0.1:3000, mock-server.dev'), ]), ); after(() => Promise.all([ updateSetting('API_EmbedIgnoredHosts', 'localhost, 127.0.0.1, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16'), updateSetting('API_EmbedSafePorts', '80, 443'), updateSetting('SSRF_Allowlist', ''), ]), ); before(async () => { await mockServerSet(mockOembedEndpoint.method, mockOembedEndpoint.path, youtubeOembedResponse); const ytEmbedMsgPayload = { _id: `id-${Date.now()}`, rid: testChannel._id, msg: mockVideoUrl('embed-max-width'), emoji: ':smirk:', }; const ytPostResponse = await request.post(api('chat.sendMessage')).set(credentials).send({ message: ytEmbedMsgPayload }); ytEmbedMsgId = ytPostResponse.body.message._id; }); before(async () => { const imgUrlMsgPayload = { _id: `id-${Date.now()}1`, rid: testChannel._id, msg: 'http://127.0.0.1:3000/images/logo/logo.png', emoji: ':smirk:', }; const imgUrlResponse = await request.post(api('chat.sendMessage')).set(credentials).send({ message: imgUrlMsgPayload }); imgUrlMsgId = imgUrlResponse.body.message._id; }); it('should have an iframe oembed with style max-width', async () => { await retry( 'Oembed is generated async thats why the retry is required', async () => { await request .get(api('chat.getMessage')) .set(credentials) .query({ msgId: ytEmbedMsgId, }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('message').to.have.property('urls').to.be.an('array').that.is.not.empty; expect(res.body.message.urls[0]) .to.have.property('meta') .to.have.property('oembedHtml') .to.have.string('', }, }) .expect('Content-Type', 'application/json') .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); }); }); describe('customFields', () => { async function testMessageSending({ customFields, testCb, statusCode, }: { customFields?: Record; testCb: (res: Response) => any; statusCode: number; }) { await request .post(api('chat.sendMessage')) .set(credentials) .send({ message: { rid: testChannel._id, msg: 'Sample message', customFields, }, }) .expect('Content-Type', 'application/json') .expect(statusCode) .expect(testCb); await request .post(api('chat.postMessage')) .set(credentials) .send({ roomId: testChannel._id, text: 'Sample message', customFields, }) .expect('Content-Type', 'application/json') .expect(statusCode) .expect(testCb); } describe('when disabled', () => { it('should not allow sending custom fields', async () => { await testMessageSending({ customFields: { field1: 'value1', }, statusCode: 400, testCb: (res) => { expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error', 'Custom fields not enabled'); }, }); }); it('should not allow update custom fields', async () => { const res = await sendSimpleMessage({ roomId: testChannel._id }); const msgId = res.body.message._id; await request .post(api('chat.update')) .set(credentials) .send({ roomId: testChannel._id, msgId, text: 'Sample message Updated', customFields: { field1: 'value1', }, }) .expect('Content-Type', 'application/json') .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error', 'Custom fields not enabled'); }); }); }); describe('when enabled', () => { before(async () => { await updateSetting('Message_CustomFields_Enabled', true); await updateSetting( 'Message_CustomFields', JSON.stringify({ properties: { priority: { type: 'string', nullable: false, enum: ['low', 'medium', 'high'], }, }, required: ['priority'], }), ); }); after(async () => { await updateSetting('Message_CustomFields_Enabled', false); }); it('should allow not sending custom fields', async () => { await testMessageSending({ statusCode: 200, testCb: (res) => { expect(res.body).to.have.property('success', true); }, }); }); it('should not allow sending empty custom fields', async () => { await testMessageSending({ customFields: {}, statusCode: 400, testCb: (res) => { expect(res.body).to.have.property('success', false); }, }); }); it('should not allow sending wrong custom fields', async () => { await testMessageSending({ customFields: { field1: 'value1', }, statusCode: 400, testCb: (res) => { expect(res.body).to.have.property('success', false); }, }); }); it('should allow sending correct custom fields', async () => { await testMessageSending({ customFields: { priority: 'low', }, statusCode: 200, testCb: (res) => { expect(res.body).to.have.property('success', true); expect(res.body.message).to.have.property('customFields').to.deep.equal({ priority: 'low' }); }, }); }); it('should allow not sending custom fields on update', async () => { const res = await sendSimpleMessage({ roomId: testChannel._id }); const msgId = res.body.message._id; await request .post(api('chat.update')) .set(credentials) .send({ roomId: testChannel._id, msgId, text: 'Sample message Updated', }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); }); }); it('should not allow update empty custom fields', async () => { const res = await sendSimpleMessage({ roomId: testChannel._id }); const msgId = res.body.message._id; await request .post(api('chat.update')) .set(credentials) .send({ roomId: testChannel._id, msgId, text: 'Sample message Updated', customFields: {}, }) .expect('Content-Type', 'application/json') .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); }); }); it('should not allow update wrong custom fields', async () => { const res = await sendSimpleMessage({ roomId: testChannel._id }); const msgId = res.body.message._id; await request .post(api('chat.update')) .set(credentials) .send({ roomId: testChannel._id, msgId, text: 'Sample message Updated', customFields: { field1: 'value1', }, }) .expect('Content-Type', 'application/json') .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); }); }); it('should allow update correct custom fields', async () => { const res = await sendSimpleMessage({ roomId: testChannel._id }); const msgId = res.body.message._id; await request .post(api('chat.update')) .set(credentials) .send({ roomId: testChannel._id, msgId, text: 'Sample message Updated', customFields: { priority: 'low', }, }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body.message).to.have.property('customFields').to.deep.equal({ priority: 'low' }); }); }); }); }); // TODO: Auto-close unclosed markdown code blocks on backend - Remove in 9.0.0 // In 9.0.0, this behavior is handled entirely on the client side and should no longer be done on the backend. it('should auto-close an unclosed code block when sending a message', async () => { const unclosedMsg = '```\nsome code'; const expectedMsg = '```\nsome code\n```'; await request .post(api('chat.sendMessage')) .set(credentials) .send({ message: { rid: testChannel._id, msg: unclosedMsg, }, }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.nested.property('message.msg', expectedMsg); }); }); }); describe('/chat.update', () => { const siteUrl = process.env.SITE_URL || process.env.TEST_API_URL || 'http://localhost:3000'; let simpleMessageId: IMessage['_id']; before('should send simple message in room', async () => { await updateSetting('Message_CustomFields_Enabled', true); await updateSetting('Message_CustomFields', JSON.stringify({ properties: { test: { type: 'string' } } })); const res = await sendSimpleMessage({ roomId: 'GENERAL' }); simpleMessageId = res.body.message._id; }); after(async () => { await updateSetting('Message_CustomFields_Enabled', false); await updateSetting('Message_CustomFields', ''); }); it('should fail updating a message if no room id is provided', () => { return request .post(api('chat.update')) .set(credentials) .send({ msgId: message._id, text: 'This message was edited via API', }) .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 updating a message if no message id is provided', () => { return request .post(api('chat.update')) .set(credentials) .send({ roomId: testChannel._id, text: 'This message was edited via API', }) .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 updating a message if no text is provided', () => { return request .post(api('chat.update')) .set(credentials) .send({ roomId: testChannel._id, msgId: message._id, }) .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 updating a message if an invalid message id is provided', () => { return request .post(api('chat.update')) .set(credentials) .send({ roomId: testChannel._id, msgId: 'invalid-id', text: 'This message was edited via API', }) .expect('Content-Type', 'application/json') .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error', 'No message found with the id of "invalid-id".'); }); }); it('should fail updating a message if it is not in the provided room', () => { return request .post(api('chat.update')) .set(credentials) .send({ roomId: 'invalid-room', msgId: message._id, text: 'This message was edited via API', }) .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 room id provided does not match where the message is from.'); }); }); it('should fail updating a message with "content" if it is not encrypted', async () => { const res = await request .post(api('chat.update')) .set(credentials) .send({ roomId: testChannel._id, msgId: message._id, content: { algorithm: 'rc.v1.aes-sha2', ciphertext: 'U2FsdGVkX1+u3j0u2+oXg4o3kw5y4t7D9sdfsdff==', }, }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error', 'Only encrypted messages can have content updated.'); }); it('should update a message successfully', async () => { const res = await request .post(api('chat.update')) .set(credentials) .send({ roomId: testChannel._id, msgId: message._id, text: 'This message was edited via API', }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); expect(res.body).to.have.nested.property('message.msg', 'This message was edited via API'); }); it('should add quote attachments to a message', async () => { const quotedMsgLink = `${siteUrl}/channel/general?msg=${message._id}`; void request .post(api('chat.update')) .set(credentials) .send({ roomId: testChannel._id, msgId: message._id, text: `Testing quotes ${quotedMsgLink}`, }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.nested.property('message.msg', `Testing quotes ${quotedMsgLink}`); expect(res.body.message).to.have.property('attachments').that.is.an('array').that.has.lengthOf(1); expect(res.body.message.attachments[0]).to.have.property('message_link', quotedMsgLink); }); }); it('should replace a quote attachment in a message', async () => { const quotedMsgLink = `${siteUrl}/channel/general?msg=${simpleMessageId}`; void request .post(api('chat.update')) .set(credentials) .send({ roomId: testChannel._id, msgId: message._id, text: `Testing quotes ${quotedMsgLink}`, }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.nested.property('message.msg', `Testing quotes ${quotedMsgLink}`); expect(res.body.message).to.have.property('attachments').that.is.an('array').that.has.lengthOf(1); expect(res.body.message.attachments[0]).to.have.property('message_link', quotedMsgLink); }); }); it('should add multiple quote attachments in a single message', async () => { const quotedMsgLink = `${siteUrl}/channel/general?msg=${simpleMessageId}`; const newQuotedMsgLink = `${siteUrl}/channel/general?msg=${message._id}`; void request .post(api('chat.update')) .set(credentials) .send({ roomId: testChannel._id, msgId: message._id, text: `${newQuotedMsgLink} Testing quotes ${quotedMsgLink}`, }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.nested.property('message.msg', `Testing quotes ${quotedMsgLink}`); expect(res.body.message).to.have.property('attachments').that.is.an('array').that.has.lengthOf(2); expect(res.body.message.attachments[0]).to.have.property('message_link', newQuotedMsgLink); expect(res.body.message.attachments[1]).to.have.property('message_link', quotedMsgLink); }); }); it('should erase old quote attachments when updating a message', async () => { await request .post(api('chat.update')) .set(credentials) .send({ roomId: testChannel._id, msgId: message._id, text: 'This message was edited via API', }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.nested.property('message.msg', 'This message was edited via API'); expect(res.body.message).to.have.property('attachments').that.is.an('array').that.has.lengthOf(0); }); }); it('should do nothing if the message text hasnt changed and theres no custom fields', async () => { await request .post(api('chat.update')) .set(credentials) .send({ roomId: testChannel._id, msgId: message._id, text: 'This message was edited via API', }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.nested.property('message.msg', 'This message was edited via API'); expect(res.body).to.not.have.nested.property('message.customFields'); }); }); it('should update message custom fields along with msg', async () => { await request .post(api('chat.update')) .set(credentials) .send({ roomId: testChannel._id, msgId: message._id, text: 'This message was edited via API 2', customFields: { test: 'test' }, }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.nested.property('message.msg', 'This message was edited via API 2'); expect(res.body.message).to.have.property('customFields').that.is.an('object').that.deep.equals({ test: 'test' }); }); }); it('should update message custom fields without changes to msg', async () => { await request .post(api('chat.update')) .set(credentials) .send({ roomId: testChannel._id, msgId: message._id, text: 'This message was edited via API 2', customFields: { test: 'test 2' }, }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.nested.property('message.msg', 'This message was edited via API 2'); expect(res.body.message).to.have.property('customFields').that.is.an('object').that.deep.equals({ test: 'test 2' }); }); }); describe('Bad words filter', () => { before(() => Promise.all([updateSetting('Message_AllowBadWordsFilter', true), updateSetting('Message_BadWordsFilterList', 'badword,badword2')]), ); after(() => Promise.all([updateSetting('Message_AllowBadWordsFilter', false), updateSetting('Message_BadWordsFilterList', '')])); it('should censor bad words on update', async () => { await request .post(api('chat.update')) .set(credentials) .send({ roomId: testChannel._id, msgId: message._id, text: 'This message has badword badword2', }) .expect(200) .expect('Content-Type', 'application/json') .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('message'); const { message } = res.body; expect(message).to.have.property('msg', 'This message has ******* ********'); expect(message).to.have.property('md').to.be.an('array').that.has.lengthOf(1); const para = message.md[0]; expect(para).to.have.property('value').to.be.an('array').that.has.lengthOf(1); const text = para.value[0]; expect(text).to.have.property('value', 'This message has ******* ********'); }); }); }); }); describe('[/chat.delete]', () => { let msgId: IMessage['_id']; let user: TestUser; let userCredentials: Credentials; before(async () => { user = await createUser(); userCredentials = await login(user.username, password); }); after(() => deleteUser(user)); beforeEach((done) => { void 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); msgId = res.body.message._id; }) .end(done); }); it('should fail deleting a message if no message id is provided', async () => { return request .post(api('chat.delete')) .set(credentials) .send({ roomId: testChannel._id, }) .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 deleting a message if no room id is provided', async () => { return request .post(api('chat.delete')) .set(credentials) .send({ msgId, }) .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 deleting a message if it is not in the provided room', async () => { return request .post(api('chat.delete')) .set(credentials) .send({ roomId: 'invalid-room', msgId, }) .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 room id provided does not match where the message is from.'); }); }); it('should fail deleting a message if an invalid id is provided', async () => { return request .post(api('chat.delete')) .set(credentials) .send({ roomId: testChannel._id, msgId: 'invalid-id', }) .expect('Content-Type', 'application/json') .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error', `No message found with the id of "invalid-id".`); }); }); it('should delete a message successfully', async () => { const res = await request .post(api('chat.delete')) .set(credentials) .send({ roomId: testChannel._id, msgId, }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); }); it('sending message as another user...', async () => { const res = await request .post(api('chat.sendMessage')) .set(userCredentials) .send({ message: { rid: testChannel._id, msg: 'Sample message', }, }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); msgId = res.body.message._id; }); it('should delete a message successfully when the user deletes a message send by another user', async () => { const res = await request .post(api('chat.delete')) .set(credentials) .send({ roomId: testChannel._id, msgId, asUser: true, }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); }); describe('when deleting by fileId', () => { const uploadFile = async (): Promise => { const { body } = await request .post(api(`rooms.media/${testChannel._id}`)) .set(credentials) .attach('file', imgURL) .expect(200); expect(body).to.have.property('success', true); return body.file._id; }; it('should delete the message associated with the provided fileId', async () => { const fileId = await uploadFile(); let fileMsgId: string | undefined; await request .post(api(`rooms.mediaConfirm/${testChannel._id}/${fileId}`)) .set(credentials) .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); fileMsgId = res.body.message._id; }); await request .post(api('chat.delete')) .set(credentials) .send({ fileId }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); }); await request .get(api('chat.getMessage')) .set(credentials) .query({ msgId: fileMsgId }) .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); }); }); it('should fail when the uploaded file has no associated message', async () => { const fileId = await uploadFile(); await request .post(api('chat.delete')) .set(credentials) .send({ fileId }) .expect('Content-Type', 'application/json') .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error', `No message found with the file id: "${fileId}".`); }); }); it('should fail when neither msgId/roomId nor fileId is provided', async () => { await request .post(api('chat.delete')) .set(credentials) .send({}) .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'); }); }); }); describe('when deleting a thread message', () => { let otherUser: TestUser; let otherUserCredentials: Credentials; let parentThreadId: IMessage['_id']; before(async () => { const username = `user${+new Date()}`; otherUser = await createUser({ username }); otherUserCredentials = await login(otherUser.username, password); parentThreadId = (await sendSimpleMessage({ roomId: testChannel._id })).body.message._id; await addUserToRoom({ rid: testChannel._id, usernames: [otherUser.username] }); }); after(() => Promise.all([deleteUser(otherUser), deleteMessage({ msgId: parentThreadId, roomId: testChannel._id })])); const expectNoUnreadThreadMessages = (s: ISubscription) => { expect(s).to.have.property('tunread'); expect(s.tunread).to.be.an('array'); expect(s.tunread).to.deep.equal([]); }; it('should reset the unread counter if the message was removed', async () => { const { body } = await sendSimpleMessage({ roomId: testChannel._id, tmid: parentThreadId, userCredentials: otherUserCredentials }); const childrenMessageId = body.message._id; await followMessage({ msgId: parentThreadId, requestCredentials: otherUserCredentials }); await deleteMessage({ msgId: childrenMessageId, roomId: testChannel._id }); const userWhoCreatedTheThreadSubscription = await getSubscriptionByRoomId(testChannel._id); expectNoUnreadThreadMessages(userWhoCreatedTheThreadSubscription); }); it('should reset the unread counter of users who followed the thread', async () => { const { body } = await sendSimpleMessage({ roomId: testChannel._id, tmid: parentThreadId }); const childrenMessageId = body.message._id; await followMessage({ msgId: parentThreadId, requestCredentials: otherUserCredentials }); await deleteMessage({ msgId: childrenMessageId, roomId: testChannel._id }); const userWhoWasFollowingTheThreadSubscription = await getSubscriptionByRoomId(testChannel._id, otherUserCredentials); expectNoUnreadThreadMessages(userWhoWasFollowingTheThreadSubscription); }); }); describe('in read-only rooms with unmuted users', () => { let readOnlyChannel: IRoom; let unmutedUser: TestUser; let unmutedUserCredentials: Credentials; let notUnmutedUser: TestUser; let notUnmutedUserCredentials: Credentials; before(async () => { unmutedUser = await createUser(); unmutedUserCredentials = await login(unmutedUser.username, password); notUnmutedUser = await createUser(); notUnmutedUserCredentials = await login(notUnmutedUser.username, password); const channelResult = await request .post(api('channels.create')) .set(credentials) .send({ name: `readonly-delete-test-${Date.now()}`, readOnly: true, }); readOnlyChannel = channelResult.body.channel; await request.post(api('channels.invite')).set(credentials).send({ roomId: readOnlyChannel._id, userId: unmutedUser._id, }); await request.post(api('channels.invite')).set(credentials).send({ roomId: readOnlyChannel._id, userId: notUnmutedUser._id, }); await request.post(api('rooms.unmuteUser')).set(credentials).send({ roomId: readOnlyChannel._id, username: unmutedUser.username, }); await updatePermission('delete-message', ['user']); }); after(async () => { await Promise.all([ readOnlyChannel && deleteRoom({ type: 'c', roomId: readOnlyChannel._id }), unmutedUser && deleteUser(unmutedUser), notUnmutedUser && deleteUser(notUnmutedUser), updatePermission('delete-message', ['admin', 'owner', 'moderator']), ]); }); it('should allow unmuted user to delete message from another user in read-only room', async () => { const messageResult = await request .post(api('chat.sendMessage')) .set(credentials) .send({ message: { rid: readOnlyChannel._id, msg: 'Message to be deleted by unmuted user', }, }); const deleteMsgId = messageResult.body.message._id; await request .post(api('chat.delete')) .set(unmutedUserCredentials) .send({ roomId: readOnlyChannel._id, msgId: deleteMsgId, }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); }); }); it('should NOT allow non-unmuted user to delete message in read-only room (regression test)', async () => { const messageResult = await request .post(api('chat.sendMessage')) .set(unmutedUserCredentials) .send({ message: { rid: readOnlyChannel._id, msg: 'Message from unmuted user', }, }); const deleteMsgId = messageResult.body.message._id; await request .post(api('chat.delete')) .set(notUnmutedUserCredentials) .send({ roomId: readOnlyChannel._id, msgId: deleteMsgId, }) .expect('Content-Type', 'application/json') .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error', "You can't delete messages because the room is readonly."); }); }); it('should NOT allow unmuted user without delete-message permission to delete message', async () => { await updatePermission('delete-message', []); const messageResult = await request .post(api('chat.sendMessage')) .set(credentials) .send({ message: { rid: readOnlyChannel._id, msg: 'Message that unmuted user without permission cannot delete', }, }); const deleteMsgId = messageResult.body.message._id; await request .post(api('chat.delete')) .set(unmutedUserCredentials) .send({ roomId: readOnlyChannel._id, msgId: deleteMsgId, }) .expect('Content-Type', 'application/json') .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); }); await updatePermission('delete-message', ['user']); }); }); }); describe('/chat.search', () => { before(async () => { const sendMessage = (text: string) => request .post(api('chat.sendMessage')) .set(credentials) .send({ message: { rid: testChannel._id, msg: text, }, }); await sendMessage('msg1'); await sendMessage('msg1'); await sendMessage('msg1'); await sendMessage('msg1'); await sendMessage('msg1'); await sendMessage('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!'); }); it('should return a list of messages when execute successfully', async () => { const res = await request .get(api('chat.search')) .set(credentials) .query({ roomId: testChannel._id, searchText: 'msg1', }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('messages'); }); it('should return a list of messages(length=1) when is provided "count" query parameter execute successfully', async () => { const res = await request .get(api('chat.search')) .set(credentials) .query({ roomId: testChannel._id, searchText: 'msg1', count: 1, }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('messages'); expect(res.body.messages.length).to.equal(1); }); it('should return a list of messages(length=3) when is provided "count" and "offset" query parameters are executed successfully', async () => { const res = await request .get(api('chat.search')) .set(credentials) .query({ roomId: testChannel._id, searchText: 'msg1', offset: 1, count: 3, }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('messages'); expect(res.body.messages.length).to.equal(3); }); it('should return a empty list of messages when is provided a huge offset value', async () => { const res = await request .get(api('chat.search')) .set(credentials) .query({ roomId: testChannel._id, searchText: 'msg1', offset: 9999, count: 3, }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('messages'); expect(res.body.messages.length).to.equal(0); }); it('should return an empty array of messages with status 200 if the regexp starts with an invalid quantifier', async () => { await request .get(api('chat.search')) .set(credentials) .query({ roomId: testChannel._id, searchText: '/*test/', }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('messages').and.to.be.deep.equal([]); }); }); it('should return an empty array of messages with status 200 if the regexp has an unclosed parenthesis', async () => { await request .get(api('chat.search')) .set(credentials) .query({ roomId: testChannel._id, searchText: '/(test/', }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('messages').and.to.be.deep.equal([]); }); }); it('should return an empty array of messages with status 200 if the regexp has an unclosed character class', async () => { await request .get(api('chat.search')) .set(credentials) .query({ roomId: testChannel._id, searchText: '/[a-z/', }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('messages').and.to.be.deep.equal([]); }); }); it('should not cause catastrophic backtracking when using a malicious regexp', async () => { await request .get(api('chat.search')) .set(credentials) .query({ roomId: testChannel._id, searchText: '/(a+)+$/', }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('messages').and.to.be.deep.equal([]); }); }); }); describe('[/chat.react]', () => { it("should return statusCode: 200 and success when try unreact a message that's no reacted yet", async () => { const res = await request .post(api('chat.react')) .set(credentials) .send({ emoji: ':squid:', messageId: message._id, shouldReact: false, }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); }); it('should react a message successfully', async () => { const res = await request .post(api('chat.react')) .set(credentials) .send({ emoji: 'smile', messageId: message._id, }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); }); it('should return statusCode: 200 when the emoji is valid', async () => { const res = await request .post(api('chat.react')) .set(credentials) .send({ emoji: ':squid:', messageId: message._id, }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); }); it("should return statusCode: 200 and success when try react a message that's already reacted", async () => { const res = await request .post(api('chat.react')) .set(credentials) .send({ emoji: ':squid:', messageId: message._id, shouldReact: true, }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); }); it('should return statusCode: 200 when unreact a message with flag, shouldReact: false', async () => { const res = await request .post(api('chat.react')) .set(credentials) .send({ emoji: ':squid:', messageId: message._id, shouldReact: false, }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); }); it('should return statusCode: 200 when react a message with flag, shouldReact: true', async () => { const res = await request .post(api('chat.react')) .set(credentials) .send({ emoji: ':squid:', messageId: message._id, shouldReact: true, }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); }); it('should return statusCode: 200 when the emoji is valid and has no colons', async () => { const res = await request .post(api('chat.react')) .set(credentials) .send({ emoji: 'bee', messageId: message._id, }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); }); it('should return statusCode: 200 for reaction property when the emoji is valid', async () => { const res = await request .post(api('chat.react')) .set(credentials) .send({ reaction: 'ant', messageId: message._id, }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); }); }); describe('[/chat.getMessageReadReceipts]', () => { const isEnterprise = typeof process.env.IS_EE === 'string' ? process.env.IS_EE === 'true' : !!process.env.IS_EE; describe('when execute successfully', () => { it('should return statusCode: 200 and an array of receipts when running EE', function (done) { if (!isEnterprise) { this.skip(); } void request .get(api(`chat.getMessageReadReceipts`)) .set(credentials) .query({ messageId: message._id, }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('receipts').and.to.be.an('array'); expect(res.body).to.have.property('success', true); }) .end(done); }); }); describe('when an error occurs', () => { it('should throw error-action-not-allowed error when not running EE', function (done) { // 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(); } void request .get(api(`chat.getMessageReadReceipts`)) .set(credentials) .query({ messageId: message._id, }) .expect('Content-Type', 'application/json') .expect(400) .expect((res) => { 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'); }) .end(done); }); it('should return statusCode: 400 and an error when no messageId is provided', function (done) { if (!isEnterprise) { this.skip(); } void request .get(api('chat.getMessageReadReceipts')) .set(credentials) .expect('Content-Type', 'application/json') .expect(400) .expect((res) => { expect(res.body).not.have.property('receipts'); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error'); }) .end(done); }); }); }); describe('[/chat.reportMessage]', () => { describe('when execute successfully', () => { it('should return the statusCode 200', async () => { const res = await request .post(api('chat.reportMessage')) .set(credentials) .send({ messageId: message._id, description: 'test', }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); }); }); describe('when an error occurs', () => { it('should return statusCode 400 and an error', async () => { const res = await request .post(api('chat.reportMessage')) .set(credentials) .send({ messageId: message._id, }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error'); }); }); }); describe('[/chat.getDeletedMessages]', () => { let roomId: IRoom['_id']; before(async () => { roomId = ( await createRoom({ type: 'c', name: `channel.test.${Date.now()}`, }) ).body.channel._id; const msgId = (await sendSimpleMessage({ roomId })).body.message._id; await deleteMessage({ roomId, msgId }); }); after(() => deleteRoom({ type: 'c', roomId })); describe('when execute successfully', () => { it('should return a list of deleted messages', async () => { const res = await request .get(api('chat.getDeletedMessages')) .set(credentials) .query({ roomId, since: new Date('20 December 2018 17:51 UTC').toISOString(), }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('messages').and.to.be.an('array'); expect(res.body.messages.length).to.be.equal(1); }); it('should return a list of deleted messages when the user sets count query parameter', async () => { const res = await request .get(api('chat.getDeletedMessages')) .set(credentials) .query({ roomId, since: new Date('20 December 2018 17:51 UTC').toISOString(), count: 1, }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('messages').and.to.be.an('array'); expect(res.body.messages.length).to.be.equal(1); }); it('should return a list of deleted messages when the user sets count and offset query parameters', async () => { const res = await request .get(api('chat.getDeletedMessages')) .set(credentials) .query({ roomId, since: new Date('20 December 2018 17:51 UTC').toISOString(), count: 1, offset: 0, }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('messages').and.to.be.an('array'); expect(res.body.messages.length).to.be.equal(1); }); }); describe('when an error occurs', () => { it('should return statusCode 400', async () => { const res = await request .get(api('chat.getDeletedMessages')) .set(credentials) .query({ since: new Date('20 December 2018 17:51 UTC').toISOString(), count: 1, offset: 0, }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); }); it('should return statusCode 400 and an error when "since" is not provided', async () => { const res = await request .get(api('chat.getDeletedMessages')) .set(credentials) .query({ roomId, count: 1, offset: 0, }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); }); it('should return statusCode 400 and an error when "since" is provided but it is invalid ISODate', async () => { const res = await request .get(api('chat.getDeletedMessages')) .set(credentials) .query({ roomId, since: 'InvalidaDate', count: 1, offset: 0, }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); }); }); }); describe('[/chat.pinMessage]', () => { after(() => Promise.all([updateSetting('Message_AllowPinning', true), updatePermission('pin-message', ['owner', 'moderator', 'admin'])]), ); it('should return an error when pinMessage is not allowed in this server', async () => { await updateSetting('Message_AllowPinning', false); const res = await request .post(api('chat.pinMessage')) .set(credentials) .send({ messageId: message._id, }) .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 an error when pinMessage is allowed in server but user dont have permission', async () => { await updateSetting('Message_AllowPinning', true); await updatePermission('pin-message', []); const res = await request .post(api('chat.pinMessage')) .set(credentials) .send({ messageId: message._id, }) .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 an error when messageId does not exist', async () => { await request .post(api('chat.pinMessage')) .set(credentials) .send({ messageId: 'test', }) .expect('Content-Type', 'application/json') .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error'); }); }); it('should pin Message successfully', async () => { await updatePermission('pin-message', ['admin']); const res = await request .post(api('chat.pinMessage')) .set(credentials) .send({ messageId: message._id, }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); expect(res.body).to.not.have.property('error'); }); it('should return message when its already pinned', async () => { await request .post(api('chat.pinMessage')) .set(credentials) .send({ messageId: message._id, }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.not.have.property('error'); }); }); }); describe('[/chat.unPinMessage]', () => { after(() => Promise.all([updateSetting('Message_AllowPinning', true), updatePermission('pin-message', ['owner', 'moderator', 'admin'])]), ); it('should return an error when pinMessage is not allowed in this server', async () => { await updateSetting('Message_AllowPinning', false); const res = await request .post(api('chat.unPinMessage')) .set(credentials) .send({ messageId: message._id, }) .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 an error when pinMessage is allowed in server but users dont have permission', async () => { await updateSetting('Message_AllowPinning', true); await updatePermission('pin-message', []); const res = await request .post(api('chat.unPinMessage')) .set(credentials) .send({ messageId: message._id, }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error'); }); it('should unpin Message successfully', async () => { await updatePermission('pin-message', ['admin']); const res = await request .post(api('chat.unPinMessage')) .set(credentials) .send({ messageId: message._id, }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); expect(res.body).to.not.have.property('error'); }); }); describe('[/chat.unStarMessage]', () => { after(() => updateSetting('Message_AllowStarring', true)); it('should return an error when starMessage is not allowed in this server', async () => { await updateSetting('Message_AllowStarring', false); const res = await request .post(api('chat.unStarMessage')) .set(credentials) .send({ messageId: message._id, }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error'); }); it('should unstar Message successfully', async () => { await updateSetting('Message_AllowStarring', true); const res = await request .post(api('chat.unStarMessage')) .set(credentials) .send({ messageId: message._id, }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); expect(res.body).to.not.have.property('error'); }); }); describe('[/chat.starMessage]', () => { after(() => updateSetting('Message_AllowStarring', true)); it('should return an error when starMessage is not allowed in this server', async () => { await updateSetting('Message_AllowStarring', false); const res = await request .post(api('chat.starMessage')) .set(credentials) .send({ messageId: message._id, }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error'); }); it('should star Message successfully', async () => { await updateSetting('Message_AllowStarring', true); const res = await request .post(api('chat.starMessage')) .set(credentials) .send({ messageId: message._id, }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); expect(res.body).to.not.have.property('error'); }); }); describe('[/chat.ignoreUser]', () => { let user: IUser; let testDM: IRoom & { rid: IRoom['_id'] }; before(async () => { user = await createUser(); await request .post(api('im.create')) .set(credentials) .send({ username: user.username, }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { testDM = res.body.room; }); }); after(() => Promise.all([deleteRoom({ type: 'd', roomId: testDM.rid }), deleteUser(user)])); it('should fail if invalid roomId', (done) => { void request .get(api('chat.ignoreUser')) .set(credentials) .query({ rid: 'invalid', userId: 'rocket.cat', }) .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-subscription'); }) .end(() => { done(); }); }); it('should fail if invalid userId', (done) => { void request .get(api('chat.ignoreUser')) .set(credentials) .query({ rid: testDM.rid, userId: 'invalid', }) .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-subscription'); }) .end(() => { done(); }); }); it('should successfully ignore user', (done) => { void request .get(api('chat.ignoreUser')) .set(credentials) .query({ rid: testDM.rid, userId: user._id, }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); }) .end(() => { done(); }); }); it('should successfully unignore user', (done) => { void request .get(api('chat.ignoreUser')) .set(credentials) .query({ rid: testDM.rid, userId: user._id, ignore: false, }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); }) .end(() => { done(); }); }); }); describe('[/chat.getPinnedMessages]', () => { let roomId: IRoom['_id']; before(async () => { roomId = ( await createRoom({ type: 'c', name: `channel.test.${Date.now()}`, }) ).body.channel._id; const msgId = (await sendSimpleMessage({ roomId })).body.message._id; await pinMessage({ msgId }); }); after(() => deleteRoom({ type: 'c', roomId })); describe('when execute successfully', () => { it('should return a list of pinned messages', async () => { const res = await request .get(api('chat.getPinnedMessages')) .set(credentials) .query({ roomId, }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('messages').and.to.be.an('array'); expect(res.body.messages.length).to.be.equal(1); }); it('should return a list of pinned messages when the user sets count query parameter', async () => { const res = await request .get(api('chat.getPinnedMessages')) .set(credentials) .query({ roomId, count: 1, }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('messages').and.to.be.an('array'); expect(res.body.messages.length).to.be.equal(1); }); it('should return a list of pinned messages when the user sets count and offset query parameters', async () => { const res = await request .get(api('chat.getPinnedMessages')) .set(credentials) .query({ roomId, count: 1, offset: 0, }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('messages').and.to.be.an('array'); expect(res.body.messages.length).to.be.equal(1); }); }); describe('when an error occurs', () => { it('should return statusCode 400 and an error when "roomId" is not provided', async () => { const res = await request .get(api('chat.getPinnedMessages')) .set(credentials) .query({ count: 1, offset: 0, }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); }); }); }); describe('[/chat.getMentionedMessages]', () => { 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 return an error when the required "roomId" parameter is not sent', async () => { const res = await request .get(api('chat.getMentionedMessages')) .set(credentials) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body.errorType).to.be.equal('error-invalid-params'); }); it('should return an error when the roomId is invalid', async () => { const res = await request .get(api('chat.getMentionedMessages')) .query({ roomId: 'invalid-room' }) .set(credentials) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body.error).to.be.equal('error-not-allowed'); }); it('should return the mentioned messages', async () => { const res = await request .get(api('chat.getMentionedMessages')) .query({ roomId: testChannel._id }) .set(credentials) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); expect(res.body.messages).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('[/chat.getStarredMessages]', () => { 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 return an error when the required "roomId" parameter is not sent', async () => { const res = await request.get(api('chat.getStarredMessages')).set(credentials).expect('Content-Type', 'application/json').expect(400); expect(res.body).to.have.property('success', false); expect(res.body.errorType).to.be.equal('error-invalid-params'); }); it('should return an error when the roomId is invalid', async () => { const res = await request .get(api('chat.getStarredMessages')) .query({ roomId: 'invalid-room' }) .set(credentials) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body.error).to.be.equal('error-not-allowed'); }); it('should return the starred messages', async () => { const res = await request .get(api('chat.getStarredMessages')) .query({ roomId: testChannel._id }) .set(credentials) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); expect(res.body.messages).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('[/chat.getDiscussions]', () => { const messageText = 'Message to create discussion'; let testChannel: IRoom; let discussionRoom: IRoom & { rid: IRoom['_id'] }; const messageWords = [ ...messageText.split(' '), ...messageText.toUpperCase().split(' '), ...messageText.toLowerCase().split(' '), messageText, messageText.charAt(0), ' ', ]; before(async () => { testChannel = (await createRoom({ type: 'c', name: `channel.test.getDiscussions.${Date.now()}` })).body.channel; discussionRoom = ( await request .post(api('rooms.createDiscussion')) .set(credentials) .send({ prid: testChannel._id, t_name: 'Message to create discussion', }) .expect('Content-Type', 'application/json') .expect(200) ).body.discussion; }); after(() => deleteRoom({ type: 'c', roomId: testChannel._id })); it('should return an error when the required "roomId" parameter is not sent', async () => { const res = await request.get(api('chat.getDiscussions')).set(credentials).expect('Content-Type', 'application/json').expect(400); expect(res.body).to.have.property('success', false); }); it('should return an error when the roomId is invalid', async () => { const res = await request .get(api('chat.getDiscussions')) .query({ roomId: 'invalid-room' }) .set(credentials) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body.error).to.be.equal('error-not-allowed'); }); it('should return the discussions of a room', async () => { const res = await request .get(api('chat.getDiscussions')) .query({ roomId: testChannel._id }) .set(credentials) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); expect(res.body.messages).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 the discussions of a room even requested with count and offset params', async () => { const res = await request .get(api('chat.getDiscussions')) .set(credentials) .query({ roomId: testChannel._id, count: 5, offset: 0, }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); expect(res.body.messages).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'); }); function filterDiscussionsByText(text: string) { it(`should return the room's discussion list filtered by the text '${text}'`, async () => { const res = await request .get(api('chat.getDiscussions')) .set(credentials) .query({ roomId: testChannel._id, text, }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('messages').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'); expect(res.body.messages).to.have.lengthOf(1); expect(res.body.messages[0].drid).to.be.equal(discussionRoom.rid); }); it(`should return the room's discussion list filtered by the text '${text}' even requested with count and offset params`, async () => { const res = await request .get(api('chat.getDiscussions')) .set(credentials) .query({ roomId: testChannel._id, text, count: 5, offset: 0, }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('messages').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'); expect(res.body.messages).to.have.lengthOf(1); expect(res.body.messages[0].drid).to.be.equal(discussionRoom.rid); }); } messageWords.forEach((text) => { filterDiscussionsByText(text); }); }); describe('[/chat.syncMessages]', () => { let testChannel: IRoom; before(async () => { testChannel = (await createRoom({ type: 'c', name: `channel.test.syncMessages.${Date.now()}` })).body.channel; }); after(() => deleteRoom({ type: 'c', roomId: testChannel._id })); it('should return an error when the required "roomId" parameter is not sent', async () => { const res = await request.get(api('chat.syncMessages')).set(credentials).expect('Content-Type', 'application/json').expect(400); expect(res.body).to.have.property('success', false); expect(res.body.errorType).to.be.equal('error-invalid-params'); expect(res.body.error).to.include(`must have required property 'roomId'`); }); it('should return an error when the neither "lastUpdate" or "type" parameter is sent', async () => { const res = await request .get(api('chat.syncMessages')) .set(credentials) .query({ roomId: testChannel._id }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body.errorType).to.be.equal('error-param-required'); expect(res.body.error).to.include('The "type" or "lastUpdate" parameters must be provided'); }); it('should return an error when the "lastUpdate" parameter is invalid', async () => { const res = await request .get(api('chat.syncMessages')) .set(credentials) .query({ roomId: 'invalid-room', lastUpdate: 'invalid-date' }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body.errorType).to.be.equal('error-lastUpdate-param-invalid'); expect(res.body.error).to.include('The "lastUpdate" query parameter must be a valid date'); }); it('should return an error when user provides an invalid roomId', async () => { const res = await request .get(api('chat.syncMessages')) .set(credentials) .query({ roomId: 'invalid-room', lastUpdate: new Date().toISOString() }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body.errorType).to.be.equal('error-not-allowed'); expect(res.body.error).to.include('Not allowed'); }); it('should return an error when the "type" parameter is not supported', async () => { const res = await request .get(api('chat.syncMessages')) .set(credentials) .query({ roomId: testChannel._id, type: 'invalid-type', next: new Date().toISOString() }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body.errorType).to.be.equal('error-invalid-params'); expect(res.body.error).to.include('must be equal to one of the allowed values'); }); it('should return an error when the "next" or "previous" parameter is sent without the "type" parameter', async () => { const nextResponse = await request .get(api('chat.syncMessages')) .set(credentials) .query({ roomId: testChannel._id, next: new Date().toISOString() }); const previousResponse = await request .get(api('chat.syncMessages')) .set(credentials) .query({ roomId: testChannel._id, previous: new Date().toISOString() }); expect(nextResponse.statusCode).to.equal(400); expect(nextResponse.body).to.have.property('success', false); expect(nextResponse.body.errorType).to.be.equal('error-param-required'); expect(nextResponse.body.error).to.include('The "type" or "lastUpdate" parameters must be provided'); expect(previousResponse.statusCode).to.equal(400); expect(previousResponse.body).to.have.property('success', false); expect(previousResponse.body.errorType).to.be.equal('error-param-required'); expect(previousResponse.body.error).to.include('The "type" or "lastUpdate" parameters must be provided'); }); it('should return an error when both "next" and "previous" are sent', async () => { const res = await request .get(api('chat.syncMessages')) .set(credentials) .query({ roomId: testChannel._id, type: 'UPDATED', next: new Date().toISOString(), previous: new Date().toISOString() }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body.errorType).to.be.equal('error-cursor-conflict'); expect(res.body.error).to.include('You cannot provide both "next" and "previous" parameters'); }); it('should return an error when both "next" or "previous" and "lastUpdate" are sent', async () => { const res = await request .get(api('chat.syncMessages')) .set(credentials) .query({ roomId: testChannel._id, type: 'UPDATED', next: new Date().toISOString(), lastUpdate: new Date().toISOString() }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body.errorType).to.be.equal('error-cursor-and-lastUpdate-conflict'); expect(res.body.error).to.include('The attributes "next", "previous" and "lastUpdate" cannot be used together'); }); it('should return an error when neither "type" or "lastUpdate" are sent', async () => { const res = await request .get(api('chat.syncMessages')) .set(credentials) .query({ roomId: testChannel._id }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body.errorType).to.be.equal('error-param-required'); expect(res.body.error).to.include('The "type" or "lastUpdate" parameters must be provided'); }); it('should return an empty response when there are no messages to sync', async () => { const res = await request .get(api('chat.syncMessages')) .set(credentials) .query({ roomId: testChannel._id, lastUpdate: new Date().toISOString() }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); expect(res.body.result).to.have.property('updated').and.to.be.an('array'); expect(res.body.result).to.have.property('deleted').and.to.be.an('array'); expect(res.body.result.updated).to.have.lengthOf(0); expect(res.body.result.deleted).to.have.lengthOf(0); }); it('should return all updated and deleted messages since "lastUpdate" parameter date', async () => { const lastUpdate = new Date().toISOString(); // Create two messages isolated to avoid ts conflict const firstMessage = await sendSimpleMessage({ roomId: testChannel._id, text: 'First Message' }); const secondMessage = await sendSimpleMessage({ roomId: testChannel._id, text: 'Second Message' }); const response = await request.get(api('chat.syncMessages')).set(credentials).query({ roomId: testChannel._id, lastUpdate }); expect(response.body.result.updated).to.have.lengthOf(2); expect(response.body.result.updated[0]._id).to.be.equal(secondMessage.body.message._id); expect(response.body.result.updated[1]._id).to.be.equal(firstMessage.body.message._id); expect(response.body.result.deleted).to.have.lengthOf(0); await deleteMessage({ roomId: testChannel._id, msgId: firstMessage.body.message._id }); const responseAfterDelete = await request .get(api('chat.syncMessages')) .set(credentials) .query({ roomId: testChannel._id, lastUpdate }); expect(responseAfterDelete.body.result.updated).to.have.lengthOf(1); expect(responseAfterDelete.body.result.updated[0]._id).to.be.equal(secondMessage.body.message._id); expect(responseAfterDelete.body.result.deleted).to.have.lengthOf(1); expect(responseAfterDelete.body.result.deleted[0]._id).to.be.equal(firstMessage.body.message._id); await deleteMessage({ roomId: testChannel._id, msgId: secondMessage.body.message._id }); }); it('should return an error when the "fromTs" parameter is invalid', async () => { const res = await request .get(api('chat.syncMessages')) .set(credentials) .query({ roomId: testChannel._id, lastUpdate: new Date().toISOString(), fromTs: 'invalid-date' }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body.errorType).to.be.equal('error-invalid-params'); }); it('should return an error when "fromTs" is sent along with cursor pagination', async () => { const res = await request .get(api('chat.syncMessages')) .set(credentials) .query({ roomId: testChannel._id, type: 'UPDATED', next: `${Date.now()}`, fromTs: new Date().toISOString() }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body.errorType).to.be.equal('error-fromTs-requires-lastUpdate'); expect(res.body.error).to.include('The "fromTs" parameter can only be used together with "lastUpdate"'); }); it('should return an error when "fromTs" is sent without "lastUpdate"', async () => { const res = await request .get(api('chat.syncMessages')) .set(credentials) .query({ roomId: testChannel._id, type: 'UPDATED', fromTs: new Date().toISOString() }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body.errorType).to.be.equal('error-fromTs-requires-lastUpdate'); }); describe('"fromTs" parameter', () => { let olderMessage: IMessage; let newerMessage: IMessage; let lastUpdate: string; // the server filters with `ts: { $gte: fromTs }`, so a boundary taken from the newer message only // excludes the older one when the two land on distinct milliseconds const sendBoundaryMessages = async (olderText: string, newerText: string): Promise<[IMessage, IMessage]> => { const older: IMessage = (await sendSimpleMessage({ roomId: testChannel._id, text: olderText })).body.message; await sleep(5); const newer: IMessage = (await sendSimpleMessage({ roomId: testChannel._id, text: newerText })).body.message; expect(new Date(newer.ts).getTime()).to.be.greaterThan(new Date(older.ts).getTime()); return [older, newer]; }; before(async () => { [olderMessage, newerMessage] = await sendBoundaryMessages('Older Message', 'Newer Message'); // every edit below happens after this point, so both messages are candidates on `_updatedAt` lastUpdate = new Date().toISOString(); await updateMessage({ roomId: testChannel._id, msgId: olderMessage._id, updatedMessage: 'Older Message edited' }); await updateMessage({ roomId: testChannel._id, msgId: newerMessage._id, updatedMessage: 'Newer Message edited' }); }); after(() => Promise.all([ deleteMessage({ roomId: testChannel._id, msgId: olderMessage._id }), deleteMessage({ roomId: testChannel._id, msgId: newerMessage._id }), ]), ); it('should return every edited message when "fromTs" is not provided', async () => { const response = await request .get(api('chat.syncMessages')) .set(credentials) .query({ roomId: testChannel._id, lastUpdate }) .expect(200); expect(response.body.result.updated).to.have.lengthOf(2); expect(response.body.result.updated.map((msg: IMessage) => msg._id)).to.have.members([olderMessage._id, newerMessage._id]); }); it('should omit messages older than "fromTs" while still returning the ones inside the window', async () => { const response = await request .get(api('chat.syncMessages')) .set(credentials) .query({ roomId: testChannel._id, lastUpdate, fromTs: newerMessage.ts }) .expect(200); expect(response.body.result.updated).to.have.lengthOf(1); expect(response.body.result.updated[0]._id).to.be.equal(newerMessage._id); expect(response.body.result.updated[0].msg).to.be.equal('Newer Message edited'); }); it('should omit deletions of messages older than "fromTs"', async () => { const [droppedMessage, windowStart] = await sendBoundaryMessages('To Be Deleted', 'Window Start'); const deletionLastUpdate = new Date().toISOString(); await deleteMessage({ roomId: testChannel._id, msgId: droppedMessage._id }); const scoped = await request .get(api('chat.syncMessages')) .set(credentials) .query({ roomId: testChannel._id, lastUpdate: deletionLastUpdate, fromTs: windowStart.ts }) .expect(200); expect(scoped.body.result.deleted).to.have.lengthOf(0); const unscoped = await request .get(api('chat.syncMessages')) .set(credentials) .query({ roomId: testChannel._id, lastUpdate: deletionLastUpdate }) .expect(200); expect(unscoped.body.result.deleted).to.have.lengthOf(1); expect(unscoped.body.result.deleted[0]._id).to.be.equal(droppedMessage._id); await deleteMessage({ roomId: testChannel._id, msgId: windowStart._id }); }); }); it('should return all updated messages with a cursor when "type" parameter is "UPDATED"', async () => { const lastUpdate = new Date(); const firstMessage = await sendSimpleMessage({ roomId: testChannel._id, text: 'First Message' }); const secondMessage = await sendSimpleMessage({ roomId: testChannel._id, text: 'Second Message' }); const thirdMessage = await sendSimpleMessage({ roomId: testChannel._id, text: 'Third Message' }); const response = await request .get(api('chat.syncMessages')) .set(credentials) .query({ roomId: testChannel._id, next: new Date(lastUpdate).getTime().toString(), type: 'UPDATED', count: 2, }); expect(response.body.result.updated).to.have.lengthOf(2); expect(response.body.result.updated[0]._id).to.be.equal(firstMessage.body.message._id); expect(response.body.result.updated[1]._id).to.be.equal(secondMessage.body.message._id); expect(response.body.result.cursor) .to.have.property('next') .and.to.equal(new Date(response.body.result.updated[response.body.result.updated.length - 1]._updatedAt).getTime().toString()); expect(response.body.result.cursor) .to.have.property('previous') .and.to.equal(new Date(response.body.result.updated[0]._updatedAt).getTime().toString()); expect(response.body.result.cursor) .to.have.property('previous') .and.to.equal(new Date(firstMessage.body.message._updatedAt).getTime().toString()); const responseWithNext = await request .get(api('chat.syncMessages')) .set(credentials) .query({ roomId: testChannel._id, next: response.body.result.cursor.next, type: 'UPDATED', count: 2 }); expect(responseWithNext.body.result.updated).to.have.lengthOf(1); expect(responseWithNext.body.result.updated[0]._id).to.be.equal(thirdMessage.body.message._id); expect(responseWithNext.body.result.cursor).to.have.property('next').and.to.be.null; expect(responseWithNext.body.result.cursor) .to.have.property('previous') .and.to.equal(new Date(thirdMessage.body.message._updatedAt).getTime().toString()); await Promise.all([ deleteMessage({ roomId: testChannel._id, msgId: firstMessage.body.message._id }), deleteMessage({ roomId: testChannel._id, msgId: secondMessage.body.message._id }), deleteMessage({ roomId: testChannel._id, msgId: thirdMessage.body.message._id }), ]); }); it('should return all deleted messages with a cursor when "type" parameter is "DELETED"', async () => { const newChannel = (await createRoom({ type: 'c', name: `channel.test.syncMessages.${Date.now()}` })).body.channel; const lastUpdate = new Date(); const firstMessage = (await sendSimpleMessage({ roomId: newChannel._id, text: 'First Message' })).body.message; const secondMessage = (await sendSimpleMessage({ roomId: newChannel._id, text: 'Second Message' })).body.message; const thirdMessage = (await sendSimpleMessage({ roomId: newChannel._id, text: 'Third Message' })).body.message; const response = await request .get(api('chat.syncMessages')) .set(credentials) .query({ roomId: newChannel._id, next: lastUpdate.getTime().toString(), type: 'DELETED', count: 2 }); expect(response.body.result.deleted).to.have.lengthOf(0); expect(response.body.result.cursor).to.have.property('next').and.to.be.null; expect(response.body.result.cursor).to.have.property('previous').and.to.be.null; const firstDeletedMessage = (await deleteMessage({ roomId: newChannel._id, msgId: firstMessage._id })).body.message; const secondDeletedMessage = (await deleteMessage({ roomId: newChannel._id, msgId: secondMessage._id })).body.message; const thirdDeletedMessage = (await deleteMessage({ roomId: newChannel._id, msgId: thirdMessage._id })).body.message; const responseAfterDelete = await request .get(api('chat.syncMessages')) .set(credentials) .query({ roomId: newChannel._id, next: lastUpdate.getTime().toString(), type: 'DELETED', count: 2 }); expect(responseAfterDelete.body.result.deleted).to.have.lengthOf(2); expect(responseAfterDelete.body.result.deleted[0]._id).to.be.equal(firstDeletedMessage._id); expect(responseAfterDelete.body.result.deleted[1]._id).to.be.equal(secondDeletedMessage._id); expect(responseAfterDelete.body.result.cursor) .to.have.property('next') .and.to.equal( new Date(responseAfterDelete.body.result.deleted[responseAfterDelete.body.result.deleted.length - 1]._deletedAt) .getTime() .toString(), ); expect(responseAfterDelete.body.result.cursor) .to.have.property('previous') .and.to.equal(new Date(responseAfterDelete.body.result.deleted[0]._deletedAt).getTime().toString()); const responseAfterDeleteWithPrevious = await request .get(api('chat.syncMessages')) .set(credentials) .query({ roomId: newChannel._id, next: responseAfterDelete.body.result.cursor.next, type: 'DELETED', count: 2 }); expect(responseAfterDeleteWithPrevious.body.result.deleted).to.have.lengthOf(1); expect(responseAfterDeleteWithPrevious.body.result.deleted[0]._id).to.be.equal(thirdDeletedMessage._id); expect(responseAfterDeleteWithPrevious.body.result.cursor).to.have.property('next').and.to.be.null; expect(responseAfterDeleteWithPrevious.body.result.cursor) .to.have.property('previous') .and.to.equal(new Date(responseAfterDeleteWithPrevious.body.result.deleted[0]._deletedAt).getTime().toString()); await deleteRoom({ type: 'c', roomId: newChannel._id }); }); }); }); describe('Threads', () => { let testThreadChannel: IRoom; before((done) => getCredentials(done)); before(async () => { testThreadChannel = (await createRoom({ type: 'c', name: `chat.api-test-${Date.now()}` })).body.channel; await updatePermission('view-c-room', ['admin', 'user', 'bot', 'app', 'anonymous']); }); after(() => Promise.all([ updateSetting('Threads_enabled', true), updatePermission('view-c-room', ['admin', 'user', 'bot', 'app', 'anonymous']), deleteRoom({ type: 'c', roomId: testThreadChannel._id }), ]), ); describe('[/chat.getThreadsList]', () => { const messageText = 'Message to create thread'; let testChannel: IRoom; let threadMessage: IThreadMessage; let user: TestUser; const messageWords = [ ...messageText.split(' '), ...messageText.toUpperCase().split(' '), ...messageText.toLowerCase().split(' '), messageText, messageText.charAt(0), ' ', ]; before(async () => { testChannel = (await createRoom({ type: 'c', name: `channel.test.threads.${Date.now()}` })).body.channel; const { message } = ( await request .post(api('chat.sendMessage')) .set(credentials) .send({ message: { rid: testChannel._id, msg: 'Message to create thread', }, }) .expect('Content-Type', 'application/json') .expect(200) ).body; threadMessage = ( await request .post(api('chat.sendMessage')) .set(credentials) .send({ message: { rid: testChannel._id, msg: 'Thread message', tmid: message._id, }, }) .expect('Content-Type', 'application/json') .expect(200) ).body.message; }); after(() => Promise.all([ updateSetting('Threads_enabled', true), deleteRoom({ type: 'c', roomId: testChannel._id }), updatePermission('view-c-room', ['admin', 'user', 'bot', 'app', 'anonymous']), deleteUser(user), ]), ); it('should return an error for chat.getThreadsList when threads are not allowed in this server', async () => { await updateSetting('Threads_enabled', false); const res = await request .get(api('chat.getThreadsList')) .set(credentials) .query({ rid: testChannel._id, }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('errorType', 'error-not-allowed'); expect(res.body).to.have.property('error', 'Threads Disabled [error-not-allowed]'); }); it('should return an error when the user is not allowed access the room', (done) => { void createUser().then((createdUser) => { user = createdUser; void login(createdUser.username, password).then((userCredentials) => { void updateSetting('Threads_enabled', true).then(() => { void updatePermission('view-c-room', []).then(() => { void request .get(api('chat.getThreadsList')) .set(userCredentials) .query({ rid: testChannel._id, }) .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-allowed'); expect(res.body).to.have.property('error', 'Not Allowed [error-not-allowed]'); }) .end(done); }); }); }); }); }); it("should return the room's thread list", async () => { await updatePermission('view-c-room', ['admin', 'user']); const res = await request .get(api('chat.getThreadsList')) .set(credentials) .query({ rid: testChannel._id, }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('threads').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'); expect(res.body.threads).to.have.lengthOf(1); expect(res.body.threads[0]._id).to.be.equal(threadMessage.tmid); }); it("should fail returning a room's thread list if no roomId is provided", async () => { await updatePermission('view-c-room', ['admin', 'user']); return request .get(api('chat.getThreadsList')) .set(credentials) .query({}) .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-params'); }); }); it("should fail returning a room's thread list if an invalid type is provided", async () => { return request .get(api('chat.getThreadsList')) .set(credentials) .query({ rid: testChannel._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('errorType', 'error-invalid-params'); }); }); it("should return the room's thread list", async () => { return request .get(api('chat.getThreadsList')) .set(credentials) .query({ rid: 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('threads').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'); expect(res.body.threads).to.have.lengthOf(1); expect(res.body.threads[0]._id).to.be.equal(threadMessage.tmid); }); }); it("should return the room's thread list even requested with count and offset params", async () => { await updatePermission('view-c-room', ['admin', 'user']); const res = await request .get(api('chat.getThreadsList')) .set(credentials) .query({ rid: testChannel._id, count: 5, offset: 0, }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('threads').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'); expect(res.body.threads).to.have.lengthOf(1); expect(res.body.threads[0]._id).to.be.equal(threadMessage.tmid); }); function filterThreadsByText(text: string) { it(`should return the room's thread list filtered by the text '${text}'`, async () => { const res = await request .get(api('chat.getThreadsList')) .set(credentials) .query({ rid: testChannel._id, text, }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('threads').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'); expect(res.body.threads).to.have.lengthOf(1); expect(res.body.threads[0]._id).to.be.equal(threadMessage.tmid); }); it(`should return the room's thread list filtered by the text '${text}' even requested with count and offset params`, async () => { const res = await request .get(api('chat.getThreadsList')) .set(credentials) .query({ rid: testChannel._id, text, count: 5, offset: 0, }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('threads').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'); expect(res.body.threads).to.have.lengthOf(1); expect(res.body.threads[0]._id).to.be.equal(threadMessage.tmid); }); } messageWords.forEach((text) => { filterThreadsByText(text); }); it('should return an empty thread list', async () => { await updatePermission('view-c-room', ['admin', 'user']); const res = await request .get(api('chat.getThreadsList')) .set(credentials) .query({ rid: testChannel._id, text: 'missing', }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('threads').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'); expect(res.body.threads).to.have.lengthOf(0); }); }); describe('[/chat.syncThreadsList]', () => { let testChannel: IRoom; let threadMessage: IThreadMessage; let user: TestUser; before(async () => { testChannel = (await createRoom({ type: 'c', name: `.threads.sync.${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(() => Promise.all([ updateSetting('Threads_enabled', true), deleteRoom({ type: 'c', roomId: testChannel._id }), updatePermission('view-c-room', ['admin', 'user', 'bot', 'app', 'anonymous']), deleteUser(user), ]), ); it('should return an error for chat.getThreadsList when threads are not allowed in this server', async () => { await updateSetting('Threads_enabled', false); const res = await request .get(api('chat.getThreadsList')) .set(credentials) .query({ rid: testChannel._id, }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('errorType', 'error-not-allowed'); expect(res.body).to.have.property('error', 'Threads Disabled [error-not-allowed]'); }); it('should return an error when the required param "rid" is missing', async () => { await updateSetting('Threads_enabled', true); const res = await request .get(api('chat.syncThreadsList')) .set(credentials) .query({}) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('errorType', 'error-invalid-params'); }); it('should return an error when the required param "updatedSince" is missing', async () => { await updateSetting('Threads_enabled', true); const res = await request .get(api('chat.syncThreadsList')) .set(credentials) .query({ rid: testChannel._id, }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('errorType', 'error-invalid-params'); }); it('should return an error when the param "updatedSince" is an invalid date', async () => { await updateSetting('Threads_enabled', true); const res = await request .get(api('chat.syncThreadsList')) .set(credentials) .query({ rid: testChannel._id, updatedSince: 'invalid-date', }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('errorType', 'error-invalid-params'); }); it('should return an error when the user is not allowed access the room', (done) => { void createUser().then((createdUser) => { user = createdUser; void login(createdUser.username, password).then((userCredentials) => { void updatePermission('view-c-room', []).then(() => { void request .get(api('chat.syncThreadsList')) .set(userCredentials) .query({ rid: testChannel._id, updatedSince: new Date().toISOString(), }) .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-allowed'); expect(res.body).to.have.property('error', 'Not Allowed [error-not-allowed]'); }) .end(done); }); }); }); }); it("should return the room's thread synced list", async () => { await updatePermission('view-c-room', ['admin', 'user']); const res = await request .get(api('chat.syncThreadsList')) .set(credentials) .query({ rid: testChannel._id, updatedSince: new Date('2019-04-01').toISOString(), }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('threads').and.to.be.an('object'); expect(res.body.threads).to.have.property('update').and.to.be.an('array'); expect(res.body.threads).to.have.property('remove').and.to.be.an('array'); expect(res.body.threads.update).to.have.lengthOf(1); expect(res.body.threads.remove).to.have.lengthOf(0); expect(res.body.threads.update[0]._id).to.be.equal(threadMessage.tmid); }); }); describe('[/chat.getThreadMessages]', () => { let testChannel: IRoom; let threadMessage: IThreadMessage; let createdThreadMessage: IThreadMessage; let user: TestUser; before(async () => { testChannel = (await createRoom({ type: 'c', name: `channel.test.threads.${Date.now()}` })).body.channel; createdThreadMessage = ( await sendSimpleMessage({ roomId: testChannel._id, text: 'Message to create thread', }) ).body.message; threadMessage = ( await sendSimpleMessage({ roomId: testChannel._id, text: 'Thread Message', tmid: createdThreadMessage._id, }) ).body.message; }); after(() => Promise.all([ updateSetting('Threads_enabled', true), deleteRoom({ type: 'c', roomId: testChannel._id }), updatePermission('view-c-room', ['admin', 'user', 'bot', 'app', 'anonymous']), deleteUser(user), ]), ); it('should return an error for chat.getThreadMessages when threads are not allowed in this server', async () => { await updateSetting('Threads_enabled', false); const res = await request .get(api('chat.getThreadMessages')) .set(credentials) .query({ tmid: threadMessage.tmid, }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('errorType', 'error-not-allowed'); expect(res.body).to.have.property('error', 'Threads Disabled [error-not-allowed]'); }); it('should return an error when the user is not allowed access the room', (done) => { void createUser().then((createdUser) => { user = createdUser; void login(createdUser.username, password).then((userCredentials) => { void updateSetting('Threads_enabled', true).then(() => { void updatePermission('view-c-room', []).then(() => { void request .get(api('chat.getThreadMessages')) .set(userCredentials) .query({ tmid: threadMessage.tmid, }) .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-allowed'); expect(res.body).to.have.property('error', 'Not Allowed [error-not-allowed]'); }) .end(done); }); }); }); }); }); it("should return the thread's message list", async () => { await updatePermission('view-c-room', ['admin', 'user']); const res = await request .get(api('chat.getThreadMessages')) .set(credentials) .query({ tmid: threadMessage.tmid, }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('messages').and.to.be.an('array'); expect(res.body).to.have.property('total').and.to.be.equal(1); expect(res.body).to.have.property('offset'); expect(res.body).to.have.property('count'); expect(res.body.messages).to.have.lengthOf(1); expect(res.body.messages[0].tmid).to.be.equal(createdThreadMessage._id); }); describe('Message_KeepHistory', () => { describe('when enabled', () => { let secondThreadMessage: IThreadMessage; before(async () => { await updateSetting('Message_KeepHistory', true); await updateMessage({ msgId: threadMessage._id, updatedMessage: 'Edited thread message', roomId: testChannel._id, }); secondThreadMessage = ( await sendSimpleMessage({ roomId: testChannel._id, text: 'Second thread message', tmid: createdThreadMessage._id, }) ).body.message; }); after(() => updateSetting('Message_KeepHistory', false)); it('should exclude hidden message edit history from offset pagination', async () => { const res = await request .get(api('chat.getThreadMessages')) .set(credentials) .query({ tmid: threadMessage.tmid, count: 1, offset: 1, }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.include({ success: true, total: 2, count: 1, offset: 1 }); expect(res.body.messages).to.have.lengthOf(1); expect(res.body.messages[0]).to.have.property('_id', secondThreadMessage._id); }); it('should exclude hidden message edit history when resolving an aroundId offset', async () => { const res = await request .get(api('chat.getThreadMessages')) .set(credentials) .query({ tmid: threadMessage.tmid, aroundId: secondThreadMessage._id, count: 2, }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.include({ success: true, total: 2, count: 2, offset: 0 }); expect(res.body.messages.map(({ _id }: IMessage) => _id)).to.deep.equal([threadMessage._id, secondThreadMessage._id]); }); }); }); }); describe('[/chat.readThread]', () => { let testChannel: IRoom; let parentMessage: IMessage; let replier: TestUser; let replierCredentials: Credentials; before(async () => { testChannel = (await createRoom({ type: 'c', name: `channel.test.readThread.${Date.now()}` })).body.channel; parentMessage = (await sendSimpleMessage({ roomId: testChannel._id, text: 'Message to create thread' })).body.message; replier = await createUser(); replierCredentials = await login(replier.username, password); await addUserToRoom({ usernames: [replier.username], rid: testChannel._id }); }); after(() => Promise.all([updateSetting('Threads_enabled', true), deleteRoom({ type: 'c', roomId: testChannel._id }), deleteUser(replier)]), ); it('should fail when threads are disabled', async () => { await updateSetting('Threads_enabled', false); try { await request .post(api('chat.readThread')) .set(credentials) .send({ tmid: parentMessage._id }) .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-allowed'); }); } finally { await updateSetting('Threads_enabled', true); } }); it('should fail when tmid does not match any message', async () => { await request .post(api('chat.readThread')) .set(credentials) .send({ tmid: 'invalid-message-id' }) .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-message'); }); }); it('should fail when tmid is missing', async () => { await request .post(api('chat.readThread')) .set(credentials) .send({}) .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 clear the unread thread flag of the caller subscription', async () => { await sendSimpleMessage({ roomId: testChannel._id, text: 'Thread reply', tmid: parentMessage._id, userCredentials: replierCredentials, }); await retry('caller subscription has the thread as unread', async () => { const subscription = await getSubscriptionByRoomId(testChannel._id); expect(subscription.tunread).to.include(parentMessage._id); }); await request .post(api('chat.readThread')) .set(credentials) .send({ tmid: parentMessage._id }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); }); const subscription = await getSubscriptionByRoomId(testChannel._id); expect(subscription.tunread ?? []).to.not.include(parentMessage._id); }); }); describe('[/chat.syncThreadMessages]', () => { let testChannel: IRoom; let threadMessage: IThreadMessage; let createdThreadMessage: IThreadMessage; let user: TestUser; before(async () => { testChannel = (await createRoom({ type: 'c', name: `message.threads.${Date.now()}` })).body.channel; createdThreadMessage = ( await sendSimpleMessage({ roomId: testChannel._id, text: 'Message to create thread', }) ).body.message; threadMessage = ( await sendSimpleMessage({ roomId: testChannel._id, text: 'Thread Message', tmid: createdThreadMessage._id, }) ).body.message; }); after(() => Promise.all([ updateSetting('Threads_enabled', true), deleteRoom({ type: 'c', roomId: testChannel._id }), updatePermission('view-c-room', ['admin', 'user', 'bot', 'app', 'anonymous']), deleteUser(user), ]), ); it('should return an error for chat.syncThreadMessages when threads are not allowed in this server', async () => { await updateSetting('Threads_enabled', false); const res = await request .get(api('chat.syncThreadMessages')) .set(credentials) .query({ tmid: threadMessage.tmid, updatedSince: new Date().toISOString(), }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('errorType', 'error-not-allowed'); expect(res.body).to.have.property('error', 'Threads Disabled [error-not-allowed]'); }); it('should return an error when the required param "tmid" is missing', async () => { await updateSetting('Threads_enabled', true); const res = await request .get(api('chat.syncThreadMessages')) .set(credentials) .query({}) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('errorType', 'error-invalid-params'); }); it('should return an error when the required param "updatedSince" is missing', async () => { await updateSetting('Threads_enabled', true); const res = await request .get(api('chat.syncThreadMessages')) .set(credentials) .query({ tmid: threadMessage.tmid, }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('errorType', 'error-invalid-params'); }); it('should return an error when the param "updatedSince" is an invalid date', async () => { await updateSetting('Threads_enabled', true); const res = await request .get(api('chat.syncThreadMessages')) .set(credentials) .query({ tmid: threadMessage.tmid, updatedSince: 'invalid-date', }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('errorType', 'error-invalid-params'); }); it('should return an error when the user is not allowed access the room', (done) => { void createUser().then((createdUser) => { user = createdUser; void login(createdUser.username, password).then((userCredentials) => { void updatePermission('view-c-room', []).then(() => { void request .get(api('chat.syncThreadMessages')) .set(userCredentials) .query({ tmid: threadMessage.tmid, updatedSince: new Date().toISOString(), }) .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-allowed'); expect(res.body).to.have.property('error', 'Not Allowed [error-not-allowed]'); }) .end(done); }); }); }); }); it("should return the thread's message list", async () => { await updatePermission('view-c-room', ['admin', 'user']); const res = await request .get(api('chat.syncThreadMessages')) .set(credentials) .query({ tmid: threadMessage.tmid, updatedSince: new Date('2019-04-01').toISOString(), }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('messages').and.to.be.an('object'); expect(res.body.messages).to.have.property('update').and.to.be.an('array'); expect(res.body.messages).to.have.property('remove').and.to.be.an('array'); expect(res.body.messages.update).to.have.lengthOf(1); expect(res.body.messages.remove).to.have.lengthOf(0); expect(res.body.messages.update[0].id).to.be.equal(createdThreadMessage.tmid); }); }); describe('[/chat.followMessage]', () => { let testChannel: IRoom; let threadMessage: IThreadMessage; let user: TestUser; before(async () => { testChannel = (await createRoom({ type: 'c', name: `channel.test.threads.follow${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(() => Promise.all([ updateSetting('Threads_enabled', true), deleteRoom({ type: 'c', roomId: testChannel._id }), updatePermission('view-c-room', ['admin', 'user', 'bot', 'app', 'anonymous']), deleteUser(user), ]), ); it('should return an error for chat.followMessage when threads are not allowed in this server', async () => { await updateSetting('Threads_enabled', false); const res = await request .post(api('chat.followMessage')) .set(credentials) .send({ mid: threadMessage.tmid, }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('errorType', 'error-not-allowed'); expect(res.body).to.have.property('error', 'not-allowed [error-not-allowed]'); }); it('should return an error when the message does not exist', async () => { await updateSetting('Threads_enabled', true); const res = await request .post(api('chat.followMessage')) .set(credentials) .send({ mid: 'invalid-message-id', }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('errorType', 'error-invalid-message'); expect(res.body).to.have.property('error', 'Invalid message [error-invalid-message]'); }); it('should return an error when the user is not allowed access the room', (done) => { void createUser().then((createdUser) => { user = createdUser; void login(createdUser.username, password).then((userCredentials) => { void updatePermission('view-c-room', []).then(() => { void request .post(api('chat.followMessage')) .set(userCredentials) .send({ mid: threadMessage.tmid, }) .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-allowed'); expect(res.body).to.have.property('error', 'not-allowed [error-not-allowed]'); }) .end(done); }); }); }); }); it('should return success: true when it execute successfully', async () => { await updatePermission('view-c-room', ['admin', 'user']); const res = await request .post(api('chat.followMessage')) .set(credentials) .send({ mid: threadMessage.tmid, }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); }); }); describe('[/chat.unfollowMessage]', () => { let testChannel: IRoom; let threadMessage: IThreadMessage; let user: TestUser; before(async () => { testChannel = (await createRoom({ type: 'c', name: `channel.test.threads.unfollow.${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(() => Promise.all([ updateSetting('Threads_enabled', true), deleteRoom({ type: 'c', roomId: testChannel._id }), updatePermission('view-c-room', ['admin', 'user', 'bot', 'app', 'anonymous']), deleteUser(user), ]), ); it('should return an error for chat.unfollowMessage when threads are not allowed in this server', async () => { await updateSetting('Threads_enabled', false); const res = await request .post(api('chat.unfollowMessage')) .set(credentials) .send({ mid: threadMessage.tmid, }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('errorType', 'error-not-allowed'); expect(res.body).to.have.property('error', 'not-allowed [error-not-allowed]'); }); it('should return an error when the message does not exist', async () => { await updateSetting('Threads_enabled', true); const res = await request .post(api('chat.unfollowMessage')) .set(credentials) .send({ mid: 'invalid-message-id', }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('errorType', 'error-invalid-message'); expect(res.body).to.have.property('error', 'Invalid message [error-invalid-message]'); }); it('should return an error when the user is not allowed access the room', (done) => { void createUser().then((createdUser) => { user = createdUser; void login(createdUser.username, password).then((userCredentials) => { void updatePermission('view-c-room', []).then(() => { void request .post(api('chat.unfollowMessage')) .set(userCredentials) .send({ mid: threadMessage.tmid, }) .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-allowed'); expect(res.body).to.have.property('error', 'not-allowed [error-not-allowed]'); }) .end(done); }); }); }); }); it('should return success: true when it execute successfully', async () => { await updatePermission('view-c-room', ['admin', 'user']); const res = await request .post(api('chat.unfollowMessage')) .set(credentials) .send({ mid: threadMessage.tmid, }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); }); }); describe('[/chat.getURLPreview]', () => { const url = 'https://www.youtube.com/watch?v=no050HN4ojo'; it('should return the URL preview with metadata and headers', async () => { await request .get(api('chat.getURLPreview')) .set(credentials) .query({ roomId: testThreadChannel._id, url, }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('urlPreview').and.to.be.an('object').that.is.not.empty; expect(res.body.urlPreview).to.have.property('url', url); expect(res.body.urlPreview).to.have.property('headers').and.to.be.an('object').that.is.not.empty; }); }); describe('when an error occurs', () => { it('should return statusCode 400 and an error when "roomId" is not provided', async () => { await request .get(api('chat.getURLPreview')) .set(credentials) .query({ url, }) .expect('Content-Type', 'application/json') .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); }); }); it('should return statusCode 400 and an error when "url" is not provided', async () => { await request .get(api('chat.getURLPreview')) .set(credentials) .query({ roomId: testThreadChannel._id, }) .expect('Content-Type', 'application/json') .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); }); }); it('should return statusCode 400 and an error when "roomId" is provided but user is not in the room', async () => { await request .get(api('chat.getURLPreview')) .set(credentials) .query({ roomId: 'undefined', url, }) .expect('Content-Type', 'application/json') .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); expect(res.body.errorType).to.be.equal('error-not-allowed'); }); }); }); }); describe('[/chat.getMessages]', () => { let firstRoom: IRoom; let secondRoom: IRoom; let privateRoom: IRoom; let firstMessageId: IMessage['_id']; let secondMessageId: IMessage['_id']; let privateMessageId: IMessage['_id']; let outsider: TestUser; let outsiderCredentials: Credentials; before(async () => { firstRoom = (await createRoom({ type: 'c', name: `chat.getMessages-first-${Date.now()}` })).body.channel; secondRoom = (await createRoom({ type: 'c', name: `chat.getMessages-second-${Date.now()}` })).body.channel; privateRoom = (await createRoom({ type: 'p', name: `chat.getMessages-private-${Date.now()}` })).body.group; const [first, second, priv] = await Promise.all([ sendMessage({ message: { rid: firstRoom._id, msg: 'first room message' } }), sendMessage({ message: { rid: secondRoom._id, msg: 'second room message' } }), sendMessage({ message: { rid: privateRoom._id, msg: 'private room message' } }), ]); firstMessageId = first.body.message._id; secondMessageId = second.body.message._id; privateMessageId = priv.body.message._id; outsider = await createUser(); outsiderCredentials = await login(outsider.username, password); }); after(async () => { await Promise.all([ deleteRoom({ type: 'c', roomId: firstRoom._id }), deleteRoom({ type: 'c', roomId: secondRoom._id }), deleteRoom({ type: 'p', roomId: privateRoom._id }), deleteUser(outsider), ]); }); it('should return the requested messages', async () => { const res = await request .post(api('chat.getMessages')) .set(credentials) .send({ messageIds: [firstMessageId] }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); expect(res.body.messages).to.have.lengthOf(1); expect(res.body.messages[0]).to.have.property('_id', firstMessageId); expect(res.body.messages[0]).to.have.property('msg', 'first room message'); }); it('should resolve a batch spanning several rooms in a single request', async () => { const res = await request .post(api('chat.getMessages')) .set(credentials) .send({ messageIds: [firstMessageId, secondMessageId] }) .expect(200); expect(res.body.messages.map((message: IMessage) => message._id)).to.have.members([firstMessageId, secondMessageId]); }); it('should omit ids that do not resolve to a message', async () => { const res = await request .post(api('chat.getMessages')) .set(credentials) .send({ messageIds: [firstMessageId, 'does-not-exist'] }) .expect(200); expect(res.body.messages.map((message: IMessage) => message._id)).to.deep.equal([firstMessageId]); }); it('should reject the whole batch when any message belongs to an unreadable room', async () => { const res = await request .post(api('chat.getMessages')) .set(outsiderCredentials) .send({ messageIds: [firstMessageId, privateMessageId] }) .expect(403); expect(res.body).to.have.property('success', false); }); it('should resolve the readable subset for the same user when the unreadable id is dropped', async () => { const res = await request .post(api('chat.getMessages')) .set(outsiderCredentials) .send({ messageIds: [firstMessageId] }) .expect(200); expect(res.body.messages).to.have.lengthOf(1); }); it('should fail when messageIds is empty', async () => { const res = await request.post(api('chat.getMessages')).set(credentials).send({ messageIds: [] }).expect(400); expect(res.body).to.have.property('success', false); }); it('should fail when unauthenticated', async () => { await request .post(api('chat.getMessages')) .send({ messageIds: [firstMessageId] }) .expect(401); }); }); });