2021-12-12 03:46:58 -05:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
const assert = require('assert').strict;
|
|
|
|
const common = require('../common');
|
|
|
|
const padManager = require('../../../node/db/PadManager');
|
2021-12-20 17:55:00 -05:00
|
|
|
const plugins = require('../../../static/js/pluginfw/plugin_defs');
|
|
|
|
const readOnlyManager = require('../../../node/db/ReadOnlyManager');
|
2021-12-12 03:46:58 -05:00
|
|
|
|
|
|
|
describe(__filename, function () {
|
|
|
|
let agent;
|
|
|
|
let pad;
|
|
|
|
let padId;
|
2021-12-20 17:55:00 -05:00
|
|
|
let roPadId;
|
2021-12-12 03:46:58 -05:00
|
|
|
let rev;
|
|
|
|
let socket;
|
2021-12-20 17:55:00 -05:00
|
|
|
let roSocket;
|
|
|
|
const backups = {};
|
2021-12-12 03:46:58 -05:00
|
|
|
|
|
|
|
before(async function () {
|
|
|
|
agent = await common.init();
|
|
|
|
});
|
|
|
|
|
|
|
|
beforeEach(async function () {
|
2021-12-20 17:55:00 -05:00
|
|
|
backups.hooks = {handleMessageSecurity: plugins.hooks.handleMessageSecurity};
|
|
|
|
plugins.hooks.handleMessageSecurity = [];
|
2021-12-12 03:46:58 -05:00
|
|
|
padId = common.randomString();
|
|
|
|
assert(!await padManager.doesPadExist(padId));
|
2022-02-23 01:03:46 -05:00
|
|
|
pad = await padManager.getPad(padId, 'dummy text\n');
|
2021-12-20 17:55:00 -05:00
|
|
|
await pad.setText('\n'); // Make sure the pad is created.
|
2021-12-12 03:46:58 -05:00
|
|
|
assert.equal(pad.text(), '\n');
|
2021-12-20 17:55:00 -05:00
|
|
|
let res = await agent.get(`/p/${padId}`).expect(200);
|
2021-12-12 03:46:58 -05:00
|
|
|
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;
|
2021-12-20 17:55:00 -05:00
|
|
|
|
|
|
|
roPadId = await readOnlyManager.getReadOnlyId(padId);
|
|
|
|
res = await agent.get(`/p/${roPadId}`).expect(200);
|
|
|
|
roSocket = await common.connect(res);
|
2022-02-28 19:20:55 -05:00
|
|
|
await common.handshake(roSocket, roPadId);
|
2024-02-18 00:06:26 +03:30
|
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
2021-12-12 03:46:58 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
afterEach(async function () {
|
2021-12-20 17:55:00 -05:00
|
|
|
Object.assign(plugins.hooks, backups.hooks);
|
2021-12-12 03:46:58 -05:00
|
|
|
if (socket != null) socket.close();
|
|
|
|
socket = null;
|
2021-12-20 17:55:00 -05:00
|
|
|
if (roSocket != null) roSocket.close();
|
|
|
|
roSocket = null;
|
2021-12-12 03:46:58 -05:00
|
|
|
if (pad != null) await pad.remove();
|
|
|
|
pad = null;
|
|
|
|
});
|
|
|
|
|
2022-02-23 01:03:46 -05:00
|
|
|
describe('CHANGESET_REQ', function () {
|
|
|
|
it('users are unable to read changesets from other pads', async function () {
|
|
|
|
const otherPadId = `${padId}other`;
|
|
|
|
assert(!await padManager.doesPadExist(otherPadId));
|
|
|
|
const otherPad = await padManager.getPad(otherPadId, 'other text\n');
|
|
|
|
try {
|
|
|
|
await otherPad.setText('other text\n');
|
|
|
|
const resP = common.waitForSocketEvent(roSocket, 'message');
|
|
|
|
await common.sendMessage(roSocket, {
|
|
|
|
component: 'pad',
|
|
|
|
padId: otherPadId, // The server should ignore this.
|
|
|
|
type: 'CHANGESET_REQ',
|
|
|
|
data: {
|
|
|
|
granularity: 1,
|
|
|
|
start: 0,
|
|
|
|
requestID: 'requestId',
|
|
|
|
},
|
|
|
|
});
|
|
|
|
const res = await resP;
|
|
|
|
assert.equal(res.type, 'CHANGESET_REQ');
|
|
|
|
assert.equal(res.data.requestID, 'requestId');
|
|
|
|
// Should match padId's text, not otherPadId's text.
|
|
|
|
assert.match(res.data.forwardsChangesets[0], /^[^$]*\$dummy text\n/);
|
|
|
|
} finally {
|
|
|
|
await otherPad.remove();
|
|
|
|
}
|
|
|
|
});
|
2023-06-26 18:17:06 +01:00
|
|
|
|
|
|
|
it('CHANGESET_REQ: verify revNum is a number (regression)', async function () {
|
|
|
|
const otherPadId = `${padId}other`;
|
|
|
|
assert(!await padManager.doesPadExist(otherPadId));
|
|
|
|
const otherPad = await padManager.getPad(otherPadId, 'other text\n');
|
|
|
|
let errorCatched = 0;
|
|
|
|
try {
|
|
|
|
await otherPad.setText('other text\n');
|
|
|
|
await common.sendMessage(roSocket, {
|
|
|
|
component: 'pad',
|
|
|
|
padId: otherPadId, // The server should ignore this.
|
|
|
|
type: 'CHANGESET_REQ',
|
|
|
|
data: {
|
|
|
|
granularity: 1,
|
|
|
|
start: 'test123',
|
|
|
|
requestID: 'requestId',
|
|
|
|
},
|
|
|
|
});
|
|
|
|
assert.equal('This code should never run', 1);
|
|
|
|
}
|
|
|
|
catch(e) {
|
|
|
|
assert.match(e.message, /rev is not a number/);
|
|
|
|
errorCatched = 1;
|
|
|
|
}
|
|
|
|
finally {
|
|
|
|
await otherPad.remove();
|
|
|
|
assert.equal(errorCatched, 1);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
it('CHANGESET_REQ: revNum is converted to number if possible (regression)', async function () {
|
|
|
|
const otherPadId = `${padId}other`;
|
|
|
|
assert(!await padManager.doesPadExist(otherPadId));
|
|
|
|
const otherPad = await padManager.getPad(otherPadId, 'other text\n');
|
|
|
|
try {
|
|
|
|
await otherPad.setText('other text\n');
|
|
|
|
const resP = common.waitForSocketEvent(roSocket, 'message');
|
|
|
|
await common.sendMessage(roSocket, {
|
|
|
|
component: 'pad',
|
|
|
|
padId: otherPadId, // The server should ignore this.
|
|
|
|
type: 'CHANGESET_REQ',
|
|
|
|
data: {
|
|
|
|
granularity: 1,
|
|
|
|
start: '1test123',
|
|
|
|
requestID: 'requestId',
|
|
|
|
},
|
|
|
|
});
|
|
|
|
const res = await resP;
|
|
|
|
assert.equal(res.type, 'CHANGESET_REQ');
|
|
|
|
assert.equal(res.data.requestID, 'requestId');
|
|
|
|
assert.equal(res.data.start, 1);
|
|
|
|
}
|
|
|
|
finally {
|
|
|
|
await otherPad.remove();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
it('CHANGESET_REQ: revNum 2 is converted to head rev 1 (regression)', async function () {
|
|
|
|
const otherPadId = `${padId}other`;
|
|
|
|
assert(!await padManager.doesPadExist(otherPadId));
|
|
|
|
const otherPad = await padManager.getPad(otherPadId, 'other text\n');
|
|
|
|
try {
|
|
|
|
await otherPad.setText('other text\n');
|
|
|
|
const resP = common.waitForSocketEvent(roSocket, 'message');
|
|
|
|
await common.sendMessage(roSocket, {
|
|
|
|
component: 'pad',
|
|
|
|
padId: otherPadId, // The server should ignore this.
|
|
|
|
type: 'CHANGESET_REQ',
|
|
|
|
data: {
|
|
|
|
granularity: 1,
|
|
|
|
start: '2',
|
|
|
|
requestID: 'requestId',
|
|
|
|
},
|
|
|
|
});
|
|
|
|
const res = await resP;
|
|
|
|
assert.equal(res.type, 'CHANGESET_REQ');
|
|
|
|
assert.equal(res.data.requestID, 'requestId');
|
|
|
|
assert.equal(res.data.start, 1);
|
|
|
|
}
|
|
|
|
finally {
|
|
|
|
await otherPad.remove();
|
|
|
|
}
|
|
|
|
});
|
2022-02-23 01:03:46 -05:00
|
|
|
});
|
2022-02-23 17:09:41 -05:00
|
|
|
|
2021-12-12 03:46:58 -05:00
|
|
|
describe('USER_CHANGES', function () {
|
2021-12-19 16:47:45 -05:00
|
|
|
const sendUserChanges =
|
2021-12-20 17:55:00 -05:00
|
|
|
async (socket, cs) => await common.sendUserChanges(socket, {baseRev: rev, changeset: cs});
|
|
|
|
const assertAccepted = async (socket, wantRev) => {
|
2021-12-19 16:47:45 -05:00
|
|
|
await common.waitForAcceptCommit(socket, wantRev);
|
2021-12-12 03:46:58 -05:00
|
|
|
rev = wantRev;
|
|
|
|
};
|
2021-12-20 17:55:00 -05:00
|
|
|
const assertRejected = async (socket) => {
|
2021-12-12 03:46:58 -05:00
|
|
|
const msg = await common.waitForSocketEvent(socket, 'message');
|
|
|
|
assert.deepEqual(msg, {disconnect: 'badChangeset'});
|
|
|
|
};
|
|
|
|
|
|
|
|
it('changes are applied', async function () {
|
2021-12-19 16:47:45 -05:00
|
|
|
await Promise.all([
|
2021-12-20 17:55:00 -05:00
|
|
|
assertAccepted(socket, rev + 1),
|
|
|
|
sendUserChanges(socket, 'Z:1>5+5$hello'),
|
2021-12-19 16:47:45 -05:00
|
|
|
]);
|
2021-12-12 03:46:58 -05:00
|
|
|
assert.equal(pad.text(), 'hello\n');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('bad changeset is rejected', async function () {
|
2021-12-19 16:47:45 -05:00
|
|
|
await Promise.all([
|
2021-12-20 17:55:00 -05:00
|
|
|
assertRejected(socket),
|
|
|
|
sendUserChanges(socket, 'this is not a valid changeset'),
|
2021-12-19 16:47:45 -05:00
|
|
|
]);
|
2021-12-12 03:46:58 -05:00
|
|
|
});
|
|
|
|
|
2021-12-12 18:19:36 -05:00
|
|
|
it('retransmission is accepted, has no effect', async function () {
|
2021-12-19 16:47:45 -05:00
|
|
|
const cs = 'Z:1>5+5$hello';
|
|
|
|
await Promise.all([
|
2021-12-20 17:55:00 -05:00
|
|
|
assertAccepted(socket, rev + 1),
|
|
|
|
sendUserChanges(socket, cs),
|
2021-12-19 16:47:45 -05:00
|
|
|
]);
|
2021-12-12 03:46:58 -05:00
|
|
|
--rev;
|
2021-12-19 16:47:45 -05:00
|
|
|
await Promise.all([
|
2021-12-20 17:55:00 -05:00
|
|
|
assertAccepted(socket, rev + 1),
|
|
|
|
sendUserChanges(socket, cs),
|
2021-12-19 16:47:45 -05:00
|
|
|
]);
|
2021-12-12 03:46:58 -05:00
|
|
|
assert.equal(pad.text(), 'hello\n');
|
|
|
|
});
|
|
|
|
|
2021-12-11 20:03:35 -05:00
|
|
|
it('identity changeset is accepted, has no effect', async function () {
|
2021-12-19 16:47:45 -05:00
|
|
|
await Promise.all([
|
2021-12-20 17:55:00 -05:00
|
|
|
assertAccepted(socket, rev + 1),
|
|
|
|
sendUserChanges(socket, 'Z:1>5+5$hello'),
|
2021-12-19 16:47:45 -05:00
|
|
|
]);
|
|
|
|
await Promise.all([
|
2021-12-20 17:55:00 -05:00
|
|
|
assertAccepted(socket, rev),
|
|
|
|
sendUserChanges(socket, 'Z:6>0$'),
|
2021-12-19 16:47:45 -05:00
|
|
|
]);
|
2021-12-12 03:46:58 -05:00
|
|
|
assert.equal(pad.text(), 'hello\n');
|
|
|
|
});
|
|
|
|
|
2021-12-11 20:03:35 -05:00
|
|
|
it('non-identity changeset with no net change is accepted, has no effect', async function () {
|
2021-12-19 16:47:45 -05:00
|
|
|
await Promise.all([
|
2021-12-20 17:55:00 -05:00
|
|
|
assertAccepted(socket, rev + 1),
|
|
|
|
sendUserChanges(socket, 'Z:1>5+5$hello'),
|
|
|
|
]);
|
|
|
|
await Promise.all([
|
|
|
|
assertAccepted(socket, rev),
|
|
|
|
sendUserChanges(socket, 'Z:6>0-5+5$hello'),
|
2021-12-19 16:47:45 -05:00
|
|
|
]);
|
2021-12-20 17:55:00 -05:00
|
|
|
assert.equal(pad.text(), 'hello\n');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('handleMessageSecurity can grant one-time write access', async function () {
|
|
|
|
const cs = 'Z:1>5+5$hello';
|
2022-02-22 23:33:03 -05:00
|
|
|
const errRegEx = /write attempt on read-only pad/;
|
2021-12-20 17:55:00 -05:00
|
|
|
// First try to send a change and verify that it was dropped.
|
2022-02-22 23:33:03 -05:00
|
|
|
await assert.rejects(sendUserChanges(roSocket, cs), errRegEx);
|
2021-12-20 17:55:00 -05:00
|
|
|
// sendUserChanges() waits for message ack, so if the message was accepted then head should
|
|
|
|
// have already incremented by the time we get here.
|
|
|
|
assert.equal(pad.head, rev); // Not incremented.
|
|
|
|
|
|
|
|
// Now allow the change.
|
|
|
|
plugins.hooks.handleMessageSecurity.push({hook_fn: () => 'permitOnce'});
|
2021-12-19 16:47:45 -05:00
|
|
|
await Promise.all([
|
2021-12-20 17:55:00 -05:00
|
|
|
assertAccepted(roSocket, rev + 1),
|
|
|
|
sendUserChanges(roSocket, cs),
|
2021-12-19 16:47:45 -05:00
|
|
|
]);
|
2021-12-12 03:46:58 -05:00
|
|
|
assert.equal(pad.text(), 'hello\n');
|
2021-12-20 17:55:00 -05:00
|
|
|
|
|
|
|
// The next change should be dropped.
|
|
|
|
plugins.hooks.handleMessageSecurity = [];
|
2022-02-22 23:33:03 -05:00
|
|
|
await assert.rejects(sendUserChanges(roSocket, 'Z:6>6=5+6$ world'), errRegEx);
|
2021-12-20 17:55:00 -05:00
|
|
|
assert.equal(pad.head, rev); // Not incremented.
|
|
|
|
assert.equal(pad.text(), 'hello\n');
|
2021-12-12 03:46:58 -05:00
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|