diff --git a/src/tests/backend/common.js b/src/tests/backend/common.js index 793828ac7..89f635012 100644 --- a/src/tests/backend/common.js +++ b/src/tests/backend/common.js @@ -184,3 +184,18 @@ exports.handshake = async (socket, padId) => { logger.debug('received CLIENT_VARS message'); return msg; }; + +const alphabet = 'abcdefghijklmnopqrstuvwxyz'; + +/** + * Generates a random string. + * + * @param {number} [len] - The desired length of the generated string. + * @param {string} [charset] - Characters to pick from. + * @returns {string} + */ +exports.randomString = (len = 10, charset = `${alphabet}${alphabet.toUpperCase()}0123456789`) => { + let ret = ''; + while (ret.length < len) ret += charset[Math.floor(Math.random() * charset.length)]; + return ret; +}; diff --git a/src/tests/backend/specs/messages.js b/src/tests/backend/specs/messages.js new file mode 100644 index 000000000..7e58ca5e7 --- /dev/null +++ b/src/tests/backend/specs/messages.js @@ -0,0 +1,103 @@ +'use strict'; + +const AttributePool = require('../../../static/js/AttributePool'); +const assert = require('assert').strict; +const common = require('../common'); +const padManager = require('../../../node/db/PadManager'); + +describe(__filename, function () { + let agent; + let pad; + let padId; + let rev; + let socket; + + before(async function () { + agent = await common.init(); + }); + + beforeEach(async function () { + padId = common.randomString(); + assert(!await padManager.doesPadExist(padId)); + pad = await padManager.getPad(padId, ''); + assert.equal(pad.text(), '\n'); + const res = await agent.get(`/p/${padId}`).expect(200); + socket = await common.connect(res); + const {type, data: clientVars} = await common.handshake(socket, padId); + assert.equal(type, 'CLIENT_VARS'); + rev = clientVars.collab_client_vars.rev; + }); + + afterEach(async function () { + if (socket != null) socket.close(); + socket = null; + if (pad != null) await pad.remove(); + pad = null; + }); + + describe('USER_CHANGES', function () { + const sendUserChanges = (changeset, apool = new AttributePool()) => { + socket.json.send({ + type: 'COLLABROOM', + component: 'pad', + data: { + type: 'USER_CHANGES', + baseRev: rev, + changeset, + apool: new AttributePool(), + }, + }); + }; + const assertAccepted = async (wantRev) => { + const msg = await common.waitForSocketEvent(socket, 'message'); + assert.deepEqual(msg, { + type: 'COLLABROOM', + data: { + type: 'ACCEPT_COMMIT', + newRev: wantRev, + }, + }); + rev = wantRev; + }; + const assertRejected = async () => { + const msg = await common.waitForSocketEvent(socket, 'message'); + assert.deepEqual(msg, {disconnect: 'badChangeset'}); + }; + + it('changes are applied', async function () { + sendUserChanges('Z:1>5+5$hello'); + await assertAccepted(rev + 1); + assert.equal(pad.text(), 'hello\n'); + }); + + it('bad changeset is rejected', async function () { + sendUserChanges('this is not a valid changeset'); + await assertRejected(); + }); + + it('retransmission is rejected', async function () { + sendUserChanges('Z:1>5+5$hello'); + await assertAccepted(rev + 1); + --rev; + sendUserChanges('Z:1>5+5$hello'); + await assertRejected(); + assert.equal(pad.text(), 'hello\n'); + }); + + it('identity changeset is accepted', async function () { + sendUserChanges('Z:1>5+5$hello'); + await assertAccepted(rev + 1); + sendUserChanges('Z:6>0$'); + await assertAccepted(rev + 1); + assert.equal(pad.text(), 'hello\n'); + }); + + it('non-identity changeset with no net change is accepted', async function () { + sendUserChanges('Z:1>5+5$hello'); + await assertAccepted(rev + 1); + sendUserChanges('Z:6>0-5+5$hello'); + await assertAccepted(rev + 1); + assert.equal(pad.text(), 'hello\n'); + }); + }); +});