From 20230472a70a8d2f06fdb2b4c31ea907a013aab0 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 3 Dec 2021 00:00:35 -0500 Subject: [PATCH] chat: New option to completely disable chat --- CHANGELOG.md | 2 + doc/docker.md | 19 +-- settings.json.docker | 8 + settings.json.template | 8 + src/ep.json | 7 +- src/node/chat.js | 25 +++ src/node/hooks/express/static.js | 8 +- src/node/hooks/express/tests.js | 6 +- src/node/utils/Minify.js | 2 +- src/node/utils/Settings.js | 6 + src/static/js/pluginfw/plugins.js | 4 + src/tests/backend/specs/api/chat.js | 52 +++++- src/tests/backend/specs/chat.js | 239 ++++++++++++++++------------ 13 files changed, 267 insertions(+), 119 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a82afd0136d7..e64c5a839443 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ### Notable enhancements and fixes +* New `enableIntegratedChat` setting makes it possible to completely disable the + built-in chat feature (not just hide it). * Improvements to login session management: * `express_sid` cookies and `sessionstorage:*` database records are no longer created unless `requireAuthentication` is `true` (or a plugin causes them to diff --git a/doc/docker.md b/doc/docker.md index f72c4dd666b8..093a260768f9 100644 --- a/doc/docker.md +++ b/doc/docker.md @@ -80,15 +80,16 @@ The `settings.json.docker` available by default allows to control almost every s ### General -| Variable | Description | Default | -| ------------------ | ------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `TITLE` | The name of the instance | `Etherpad` | -| `FAVICON` | favicon default name, or a fully specified URL to your own favicon | `favicon.ico` | -| `DEFAULT_PAD_TEXT` | The default text of a pad | `Welcome to Etherpad! This pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents! Get involved with Etherpad at https://etherpad.org` | -| `IP` | IP which etherpad should bind at. Change to `::` for IPv6 | `0.0.0.0` | -| `PORT` | port which etherpad should bind at | `9001` | -| `ADMIN_PASSWORD` | the password for the `admin` user (leave unspecified if you do not want to create it) | | -| `USER_PASSWORD` | the password for the first user `user` (leave unspecified if you do not want to create it) | | +| Variable | Description | Default | +| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `TITLE` | The name of the instance | `Etherpad` | +| `FAVICON` | favicon default name, or a fully specified URL to your own favicon | `favicon.ico` | +| `DEFAULT_PAD_TEXT` | The default text of a pad | `Welcome to Etherpad! This pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents! Get involved with Etherpad at https://etherpad.org` | +| `ENABLE_INTEGRATED_CHAT` | Whether to enable the built-in chat feature. Set this to false if you prefer to use a plugin to provide chat functionality or simply do not want the feature. | true | +| `IP` | IP which etherpad should bind at. Change to `::` for IPv6 | `0.0.0.0` | +| `PORT` | port which etherpad should bind at | `9001` | +| `ADMIN_PASSWORD` | the password for the `admin` user (leave unspecified if you do not want to create it) | | +| `USER_PASSWORD` | the password for the first user `user` (leave unspecified if you do not want to create it) | | ### Database diff --git a/settings.json.docker b/settings.json.docker index 725af9f314c5..d1679d7da73d 100644 --- a/settings.json.docker +++ b/settings.json.docker @@ -223,6 +223,13 @@ */ "defaultPadText" : "${DEFAULT_PAD_TEXT:Welcome to Etherpad!\n\nThis pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents!\n\nGet involved with Etherpad at https:\/\/etherpad.org\n}", + /* + * Whether to enable the built-in chat feature. Set this to false if you + * prefer to use a plugin to provide chat functionality or simply do not want + * the feature. + */ + "enableIntegratedChat": "${ENABLE_INTEGRATED_CHAT:true}", + /* * Default Pad behavior. * @@ -231,6 +238,7 @@ "padOptions": { "noColors": "${PAD_OPTIONS_NO_COLORS:false}", "showControls": "${PAD_OPTIONS_SHOW_CONTROLS:true}", + // To completely disable chat, set enableIntegratedChat to false. "showChat": "${PAD_OPTIONS_SHOW_CHAT:true}", "showLineNumbers": "${PAD_OPTIONS_SHOW_LINE_NUMBERS:true}", "useMonospaceFont": "${PAD_OPTIONS_USE_MONOSPACE_FONT:false}", diff --git a/settings.json.template b/settings.json.template index b2cb9555a6ce..446ddb882a80 100644 --- a/settings.json.template +++ b/settings.json.template @@ -224,6 +224,13 @@ */ "defaultPadText" : "Welcome to Etherpad!\n\nThis pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents!\n\nGet involved with Etherpad at https:\/\/etherpad.org\n", + /* + * Whether to enable the built-in chat feature. Set this to false if you + * prefer to use a plugin to provide chat functionality or simply do not want + * the feature. + */ + "enableIntegratedChat": true, + /* * Default Pad behavior. * @@ -232,6 +239,7 @@ "padOptions": { "noColors": false, "showControls": true, + // To completely disable chat, set enableIntegratedChat to false. "showChat": true, "showLineNumbers": true, "useMonospaceFont": false, diff --git a/src/ep.json b/src/ep.json index 7c76b4dd7664..6fb110f5e6bd 100644 --- a/src/ep.json +++ b/src/ep.json @@ -28,7 +28,12 @@ "padCheck": "ep_etherpad-lite/node/chat", "padCopy": "ep_etherpad-lite/node/chat", "padLoad": "ep_etherpad-lite/node/chat", - "padRemove": "ep_etherpad-lite/node/chat", + "padRemove": "ep_etherpad-lite/node/chat" + } + }, + { + "name": "chatAlwaysLoaded", + "hooks": { "socketio": "ep_etherpad-lite/node/chat" } }, diff --git a/src/node/chat.js b/src/node/chat.js index dc9f0ef17442..768e74b2e2b6 100644 --- a/src/node/chat.js +++ b/src/node/chat.js @@ -10,10 +10,14 @@ const pad = require('./db/Pad'); const padManager = require('./db/PadManager'); const padMessageHandler = require('./handler/PadMessageHandler'); const promises = require('./utils/promises'); +const settings = require('./utils/Settings'); let socketio; const appendChatMessage = async (pad, msg) => { + if (!settings.enableIntegratedChat) { + throw new Error('integrated chat is disabled (see enableIntegratedChat in settings.json)'); + } pad.chatHead++; await Promise.all([ // Don't save the display name in the database because the user can change it at any time. The @@ -25,6 +29,9 @@ const appendChatMessage = async (pad, msg) => { }; const getChatMessage = async (pad, entryNum) => { + if (!settings.enableIntegratedChat) { + throw new Error('integrated chat is disabled (see enableIntegratedChat in settings.json)'); + } const entry = await pad.db.get(`pad:${pad.id}:chat:${entryNum}`); if (entry == null) return null; const message = ChatMessage.fromObject(entry); @@ -33,6 +40,9 @@ const getChatMessage = async (pad, entryNum) => { }; const getChatMessages = async (pad, start, end) => { + if (!settings.enableIntegratedChat) { + throw new Error('integrated chat is disabled (see enableIntegratedChat in settings.json)'); + } const entries = await Promise.all( [...Array(end + 1 - start).keys()].map((i) => getChatMessage(pad, start + i))); @@ -49,6 +59,9 @@ const getChatMessages = async (pad, start, end) => { }; const sendChatMessageToPadClients = async (message, padId) => { + if (!settings.enableIntegratedChat) { + throw new Error('integrated chat is disabled (see enableIntegratedChat in settings.json)'); + } const pad = await padManager.getPad(padId, null, message.authorId); await hooks.aCallAll('chatNewMessage', {message, pad, padId}); // appendChatMessage() ignores the displayName property so we don't need to wait for @@ -65,6 +78,7 @@ const sendChatMessageToPadClients = async (message, padId) => { exports.clientVars = (hookName, {pad: {chatHead}}) => ({chatHead}); exports.eejsBlock_mySettings = (hookName, context) => { + if (!settings.enableIntegratedChat) return; context.content += `

@@ -78,6 +92,7 @@ exports.eejsBlock_mySettings = (hookName, context) => { }; exports.eejsBlock_stickyContainer = (hookName, context) => { + if (!settings.enableIntegratedChat) return; /* eslint-disable max-len */ context.content += `

@@ -108,6 +123,7 @@ exports.eejsBlock_stickyContainer = (hookName, context) => { }; exports.handleMessage = async (hookName, {message, sessionInfo, socket}) => { + if (!settings.enableIntegratedChat) return; const {authorId, padId, readOnly} = sessionInfo; if (message.type !== 'COLLABROOM' || readOnly) return; switch (message.data.type) { @@ -201,6 +217,9 @@ api.registerChatHandlers({ * {code: 1, message:"padID does not exist", data: null} */ appendChatMessage: async (padID, text, authorID, time) => { + if (!settings.enableIntegratedChat) { + throw new Error('integrated chat is disabled (see enableIntegratedChat in settings.json)'); + } if (typeof text !== 'string') throw new CustomError('text is not a string', 'apierror'); if (time === undefined || !Number.isInteger(Number.parseInt(time))) time = Date.now(); await sendChatMessageToPadClients(new ChatMessage(text, authorID, time), padID); @@ -215,6 +234,9 @@ api.registerChatHandlers({ * {code: 1, message:"padID does not exist", data: null} */ getChatHead: async (padID) => { + if (!settings.enableIntegratedChat) { + throw new Error('integrated chat is disabled (see enableIntegratedChat in settings.json)'); + } const pad = await getPadSafe(padID); return {chatHead: pad.chatHead}; }, @@ -234,6 +256,9 @@ api.registerChatHandlers({ * {code: 1, message:"padID does not exist", data: null} */ getChatHistory: async (padID, start, end) => { + if (!settings.enableIntegratedChat) { + throw new Error('integrated chat is disabled (see enableIntegratedChat in settings.json)'); + } if (start && end) { if (start < 0) throw new CustomError('start is below zero', 'apierror'); if (end < 0) throw new CustomError('end is below zero', 'apierror'); diff --git a/src/node/hooks/express/static.js b/src/node/hooks/express/static.js index d33e10ad908c..62f409a5e726 100644 --- a/src/node/hooks/express/static.js +++ b/src/node/hooks/express/static.js @@ -28,11 +28,13 @@ const tar = (() => { 'pad_impexp.js', 'pad_savedrevs.js', 'pad_connectionstatus.js', - 'ChatMessage.js', - 'chat.js', + ...settings.enableIntegratedChat ? [ + 'ChatMessage.js', + 'chat.js', + '$tinycon/tinycon.js', + ] : [], 'vendors/gritter.js', '$js-cookie/dist/js.cookie.js', - '$tinycon/tinycon.js', 'vendors/farbtastic.js', 'skin_variants.js', 'socketio.js', diff --git a/src/node/hooks/express/tests.js b/src/node/hooks/express/tests.js index 66b47d2af958..e59d018095a4 100644 --- a/src/node/hooks/express/tests.js +++ b/src/node/hooks/express/tests.js @@ -38,8 +38,10 @@ exports.expressPreSession = async (hookName, {app}) => { if (!pluginPath.endsWith(path.sep)) pluginPath += path.sep; const specDir = `${plugin === 'ep_etherpad-lite' ? '' : 'static/'}tests/frontend/specs`; for (const spec of await findSpecs(path.join(pluginPath, specDir))) { - if (plugin === 'ep_etherpad-lite' && !settings.enableAdminUITests && - spec.startsWith('admin')) continue; + if (plugin === 'ep_etherpad-lite') { + if (!settings.enableAdminUITests && spec.startsWith('admin')) continue; + if (!settings.enableIntegratedChat && spec.startsWith('chat')) continue; + } modules.push(`${plugin}/${specDir}/${spec.replace(/\.js$/, '')}`); } })); diff --git a/src/node/utils/Minify.js b/src/node/utils/Minify.js index 2e8a2d960fc1..4109786546a1 100644 --- a/src/node/utils/Minify.js +++ b/src/node/utils/Minify.js @@ -42,7 +42,7 @@ const LIBRARY_WHITELIST = [ 'js-cookie', 'security', 'split-grid', - 'tinycon', + ...settings.enableIntegratedChat ? ['tinycon'] : [], 'underscore', 'unorm', ]; diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index 51f48237aa8f..ac89153e8e12 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -156,6 +156,12 @@ exports.defaultPadText = [ 'Etherpad on Github: https://github.com/ether/etherpad-lite', ].join('\n'); +/** + * Whether to enable the built-in chat feature. Set this to false if you prefer to use a plugin to + * provide chat functionality or simply do not want the feature. + */ +exports.enableIntegratedChat = true; + /** * The default Pad Settings for a user (Can be overridden by changing the setting */ diff --git a/src/static/js/pluginfw/plugins.js b/src/static/js/pluginfw/plugins.js index 2317168f76cc..89df1b5c9c23 100644 --- a/src/static/js/pluginfw/plugins.js +++ b/src/static/js/pluginfw/plugins.js @@ -5,6 +5,7 @@ const hooks = require('./hooks'); const log4js = require('log4js'); const path = require('path'); const runCmd = require('../../../node/utils/run_cmd'); +const settings = require('../../../node/utils/Settings'); const tsort = require('./tsort'); const pluginUtils = require('./shared'); const defs = require('./plugin_defs'); @@ -136,6 +137,9 @@ const loadPlugin = async (packages, pluginName, plugins, parts) => { const data = await fs.readFile(pluginPath); try { const plugin = JSON.parse(data); + if (pluginName === 'ep_etherpad-lite' && !settings.enableIntegratedChat) { + plugin.parts = plugin.parts.filter((part) => part.name !== 'chat'); + } plugin.package = packages[pluginName]; plugins[pluginName] = plugin; for (const part of plugin.parts) { diff --git a/src/tests/backend/specs/api/chat.js b/src/tests/backend/specs/api/chat.js index eaa11d2e0efb..346127aa0b8e 100644 --- a/src/tests/backend/specs/api/chat.js +++ b/src/tests/backend/specs/api/chat.js @@ -2,6 +2,8 @@ const assert = require('assert').strict; const common = require('../../common'); +const plugins = require('../../../../static/js/pluginfw/plugins'); +const settings = require('../../../../node/utils/Settings'); let agent; const apiKey = common.apiKey; @@ -13,7 +15,12 @@ const timestamp = Date.now(); const endPoint = (point) => `/api/${apiVersion}/${point}?apikey=${apiKey}`; describe(__filename, function () { + const backups = {settings: {}}; + before(async function () { + backups.settings.enableIntegratedChat = settings.enableIntegratedChat; + settings.enableIntegratedChat = true; + await plugins.update(); agent = await common.init(); await agent.get('/api/') .expect(200) @@ -37,7 +44,16 @@ describe(__filename, function () { }); }); - describe('message sequence', function () { + after(async function () { + Object.assign(settings, backups.settings); + await plugins.update(); + }); + + describe('settings.enableIntegratedChat = true', function () { + beforeEach(async function () { + settings.enableIntegratedChat = true; + }); + it('appendChatMessage', async function () { await agent.get(`${endPoint('appendChatMessage')}&padID=${padID}&text=blalblalbha` + `&authorID=${authorID}&time=${timestamp}`) @@ -68,6 +84,40 @@ describe(__filename, function () { }); }); }); + + describe('settings.enableIntegratedChat = false', function () { + beforeEach(async function () { + settings.enableIntegratedChat = false; + }); + + it('appendChatMessage returns an error', async function () { + await agent.get(`${endPoint('appendChatMessage')}&padID=${padID}&text=blalblalbha` + + `&authorID=${authorID}&time=${timestamp}`) + .expect(500) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 2); + }); + }); + + it('getChatHead returns an error', async function () { + await agent.get(`${endPoint('getChatHead')}&padID=${padID}`) + .expect(500) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 2); + }); + }); + + it('getChatHistory returns an error', async function () { + await agent.get(`${endPoint('getChatHistory')}&padID=${padID}`) + .expect(500) + .expect('Content-Type', /json/) + .expect((res) => { + assert.equal(res.body.code, 2); + }); + }); + }); }); function makeid() { diff --git a/src/tests/backend/specs/chat.js b/src/tests/backend/specs/chat.js index 5bc421765d1a..936209cc59a8 100644 --- a/src/tests/backend/specs/chat.js +++ b/src/tests/backend/specs/chat.js @@ -6,29 +6,37 @@ const assert = require('assert').strict; const common = require('../common'); const padManager = require('../../../node/db/PadManager'); const pluginDefs = require('../../../static/js/pluginfw/plugin_defs'); +const plugins = require('../../../static/js/pluginfw/plugins'); +const settings = require('../../../node/utils/Settings'); const logger = common.logger; const checkHook = async (hookName, checkFn) => { if (pluginDefs.hooks[hookName] == null) pluginDefs.hooks[hookName] = []; - await new Promise((resolve, reject) => { - pluginDefs.hooks[hookName].push({ - hook_fn: async (hookName, context) => { - if (checkFn == null) return; - logger.debug(`hook ${hookName} invoked`); - try { - // Make sure checkFn is called only once. - const _checkFn = checkFn; - checkFn = null; - await _checkFn(context); - } catch (err) { - reject(err); - return; - } - resolve(); - }, + let hook; + try { + await new Promise((resolve, reject) => { + hook = { + hook_fn: async (hookName, context) => { + if (checkFn == null) return; + logger.debug(`hook ${hookName} invoked`); + try { + // Make sure checkFn is called only once. + const _checkFn = checkFn; + checkFn = null; + await _checkFn(context); + } catch (err) { + reject(err); + return; + } + resolve(); + }, + }; + pluginDefs.hooks[hookName].push(hook); }); - }); + } finally { + pluginDefs.hooks[hookName] = pluginDefs.hooks[hookName].filter((h) => h !== hook); + } }; const sendMessage = async (socket, data) => ( @@ -37,119 +45,146 @@ const sendChat = async (socket, message) => ( await sendMessage(socket, {type: 'CHAT_MESSAGE', message})); describe(__filename, function () { + const backups = {settings: {}}; + let clientVars; const padId = 'testChatPad'; - const hooksBackup = {}; + let socket; + + const connect = async () => { + socket = await common.connect(); + ({data: clientVars} = await common.handshake(socket, padId)); + }; before(async function () { - for (const [name, defs] of Object.entries(pluginDefs.hooks)) { - if (defs == null) continue; - hooksBackup[name] = defs; - } + backups.settings.enableIntegratedChat = settings.enableIntegratedChat; }); beforeEach(async function () { - for (const [name, defs] of Object.entries(hooksBackup)) pluginDefs.hooks[name] = [...defs]; - for (const name of Object.keys(pluginDefs.hooks)) { - if (hooksBackup[name] == null) delete pluginDefs.hooks[name]; - } if (await padManager.doesPadExist(padId)) { const pad = await padManager.getPad(padId); await pad.remove(); } }); - after(async function () { - Object.assign(pluginDefs.hooks, hooksBackup); - for (const name of Object.keys(pluginDefs.hooks)) { - if (hooksBackup[name] == null) delete pluginDefs.hooks[name]; + afterEach(async function () { + if (socket) { + socket.close(); + socket = null; } }); - describe('chatNewMessage hook', function () { - let authorId; - let socket; + after(async function () { + Object.assign(settings, backups.settings); + await plugins.update(); + }); + + describe('settings.enableIntegratedChat = true', function () { + before(async function () { + settings.enableIntegratedChat = true; + await plugins.update(); + }); beforeEach(async function () { - socket = await common.connect(); - const {data: clientVars} = await common.handshake(socket, padId); - authorId = clientVars.userId; + await connect(); }); - afterEach(async function () { - socket.close(); + describe('chatNewMessage hook', function () { + it('message', async function () { + const start = Date.now(); + await Promise.all([ + checkHook('chatNewMessage', ({message}) => { + assert(message != null); + assert(message instanceof ChatMessage); + assert.equal(message.authorId, clientVars.userId); + assert.equal(message.text, this.test.title); + assert(message.time >= start); + assert(message.time <= Date.now()); + }), + sendChat(socket, {text: this.test.title}), + ]); + }); + + it('pad', async function () { + await Promise.all([ + checkHook('chatNewMessage', ({pad}) => { + assert(pad != null); + assert(pad instanceof Pad); + assert.equal(pad.id, padId); + }), + sendChat(socket, {text: this.test.title}), + ]); + }); + + it('padId', async function () { + await Promise.all([ + checkHook('chatNewMessage', (context) => { + assert.equal(context.padId, padId); + }), + sendChat(socket, {text: this.test.title}), + ]); + }); + + it('mutations propagate', async function () { + const listen = async (type) => await new Promise((resolve) => { + const handler = (msg) => { + if (msg.type !== 'COLLABROOM') return; + if (msg.data == null || msg.data.type !== type) return; + resolve(msg.data); + socket.off('message', handler); + }; + socket.on('message', handler); + }); + + const modifiedText = `${this.test.title} `; + const customMetadata = {foo: this.test.title}; + await Promise.all([ + checkHook('chatNewMessage', ({message}) => { + message.text = modifiedText; + message.customMetadata = customMetadata; + }), + (async () => { + const {message} = await listen('CHAT_MESSAGE'); + assert(message != null); + assert.equal(message.text, modifiedText); + assert.deepEqual(message.customMetadata, customMetadata); + })(), + sendChat(socket, {text: this.test.title}), + ]); + // Simulate fetch of historical chat messages when a pad is first loaded. + await Promise.all([ + (async () => { + const {messages: [message]} = await listen('CHAT_MESSAGES'); + assert(message != null); + assert.equal(message.text, modifiedText); + assert.deepEqual(message.customMetadata, customMetadata); + })(), + sendMessage(socket, {type: 'GET_CHAT_MESSAGES', start: 0, end: 0}), + ]); + }); }); + }); - it('message', async function () { - const start = Date.now(); - await Promise.all([ - checkHook('chatNewMessage', ({message}) => { - assert(message != null); - assert(message instanceof ChatMessage); - assert.equal(message.authorId, authorId); - assert.equal(message.text, this.test.title); - assert(message.time >= start); - assert(message.time <= Date.now()); - }), - sendChat(socket, {text: this.test.title}), - ]); + describe('settings.enableIntegratedChat = false', function () { + before(async function () { + settings.enableIntegratedChat = false; + await plugins.update(); }); - it('pad', async function () { - await Promise.all([ - checkHook('chatNewMessage', ({pad}) => { - assert(pad != null); - assert(pad instanceof Pad); - assert.equal(pad.id, padId); - }), - sendChat(socket, {text: this.test.title}), - ]); + beforeEach(async function () { + await connect(); }); - it('padId', async function () { - await Promise.all([ - checkHook('chatNewMessage', (context) => { - assert.equal(context.padId, padId); - }), - sendChat(socket, {text: this.test.title}), - ]); + it('clientVars.chatHead is unset', async function () { + assert(!('chatHead' in clientVars), `chatHead should be unset, is ${clientVars.chatHead}`); }); - it('mutations propagate', async function () { - const listen = async (type) => await new Promise((resolve) => { - const handler = (msg) => { - if (msg.type !== 'COLLABROOM') return; - if (msg.data == null || msg.data.type !== type) return; - resolve(msg.data); - socket.off('message', handler); - }; - socket.on('message', handler); - }); + it('rejects CHAT_MESSAGE messages', async function () { + await assert.rejects(sendChat(socket, {text: 'this is a test'}), /unknown message type/); + }); - const modifiedText = `${this.test.title} `; - const customMetadata = {foo: this.test.title}; - await Promise.all([ - checkHook('chatNewMessage', ({message}) => { - message.text = modifiedText; - message.customMetadata = customMetadata; - }), - (async () => { - const {message} = await listen('CHAT_MESSAGE'); - assert(message != null); - assert.equal(message.text, modifiedText); - assert.deepEqual(message.customMetadata, customMetadata); - })(), - sendChat(socket, {text: this.test.title}), - ]); - // Simulate fetch of historical chat messages when a pad is first loaded. - await Promise.all([ - (async () => { - const {messages: [message]} = await listen('CHAT_MESSAGES'); - assert(message != null); - assert.equal(message.text, modifiedText); - assert.deepEqual(message.customMetadata, customMetadata); - })(), - sendMessage(socket, {type: 'GET_CHAT_MESSAGES', start: 0, end: 0}), - ]); + it('rejects GET_CHAT_MESSAGES messages', async function () { + const msg = {type: 'GET_CHAT_MESSAGES', start: 0, end: 0}; + await assert.rejects(sendMessage(socket, msg), /unknown message type/); }); }); });