diff --git a/services/real-time/app/js/Router.js b/services/real-time/app/js/Router.js index d25b804a83..7bd4864322 100644 --- a/services/real-time/app/js/Router.js +++ b/services/real-time/app/js/Router.js @@ -26,7 +26,43 @@ const joinDocSchema = z.object({ const applyOtUpdateSchema = z.object({ doc_id: zz.objectId(), - update: z.object(), + update: z.strictObject({ + doc: zz.objectId().optional(), + dupIfSource: z.array(z.string()).optional(), + hash: z.string().optional(), + lastV: z.number().optional(), + meta: z + .strictObject({ + tc: z.string(), + }) + .optional(), + op: z + .array( + z + .strictObject({ + i: z.string(), + p: z.number().int().min(0), + u: z.boolean().optional(), + }) + .or( + z.strictObject({ + d: z.string(), + p: z.number().int().min(0), + u: z.boolean().optional(), + }) + ) + .or( + z.strictObject({ + c: z.string(), + p: z.number().int().min(0), + t: z.string(), + u: z.boolean().optional(), + }) + ) + ) + .min(1), + v: z.number().int().min(0), + }), }) let Router diff --git a/services/real-time/test/acceptance/js/ApplyUpdateTests.js b/services/real-time/test/acceptance/js/ApplyUpdateTests.js index 1452ad1f3b..2901a78f2a 100644 --- a/services/real-time/test/acceptance/js/ApplyUpdateTests.js +++ b/services/real-time/test/acceptance/js/ApplyUpdateTests.js @@ -340,7 +340,7 @@ describe('applyOtUpdate', function () { describe('when authorized to read-only with a comment update', function () { before(function (done) { this.comment_update = { - op: [{ c: 'foo', p: 42 }], + op: [{ c: 'foo', p: 42, t: '1234' }], v: 42, } return async.series( @@ -765,4 +765,250 @@ describe('applyOtUpdate', function () { return null }) }) + + describe('when authorized with an empty update', function () { + before(function (done) { + this.update = { + op: [], + v: 42, + } + return async.series( + [ + cb => { + return FixturesManager.setUpProject( + { + privilegeLevel: 'readOnly', + }, + (e, { project_id: projectId, user_id: userId }) => { + this.project_id = projectId + this.user_id = userId + return cb(e) + } + ) + }, + + cb => { + return FixturesManager.setUpDoc( + this.project_id, + { lines: this.lines, version: this.version, ops: this.ops }, + (e, { doc_id: docId }) => { + this.doc_id = docId + return cb(e) + } + ) + }, + + cb => { + this.client = RealTimeClient.connect(this.project_id, cb) + }, + + cb => { + return this.client.emit('joinDoc', this.doc_id, cb) + }, + + cb => { + return this.client.emit( + 'applyOtUpdate', + this.doc_id, + this.update, + error => { + this.error = error + return cb() + } + ) + }, + ], + done + ) + }) + + it('should return an error', function () { + return expect(this.error).to.exist + }) + + it('should disconnect the client', function (done) { + return setTimeout(() => { + this.client.socket.connected.should.equal(false) + return done() + }, 300) + }) + + return it('should not put the update in redis', function (done) { + rclient.llen( + redisSettings.documentupdater.key_schema.pendingUpdates({ + doc_id: this.doc_id, + }), + (error, len) => { + if (error) return done(error) + len.should.equal(0) + return done() + } + ) + return null + }) + }) + + describe('when authorized with a mixed update', function () { + before(function (done) { + this.update = { + op: [{ c: 'foo', d: 'bar', i: 'foo', p: 42 }], + v: 42, + } + return async.series( + [ + cb => { + return FixturesManager.setUpProject( + { + privilegeLevel: 'readOnly', + }, + (e, { project_id: projectId, user_id: userId }) => { + this.project_id = projectId + this.user_id = userId + return cb(e) + } + ) + }, + + cb => { + return FixturesManager.setUpDoc( + this.project_id, + { lines: this.lines, version: this.version, ops: this.ops }, + (e, { doc_id: docId }) => { + this.doc_id = docId + return cb(e) + } + ) + }, + + cb => { + this.client = RealTimeClient.connect(this.project_id, cb) + }, + + cb => { + return this.client.emit('joinDoc', this.doc_id, cb) + }, + + cb => { + return this.client.emit( + 'applyOtUpdate', + this.doc_id, + this.update, + error => { + this.error = error + return cb() + } + ) + }, + ], + done + ) + }) + + it('should return an error', function () { + return expect(this.error).to.exist + }) + + it('should disconnect the client', function (done) { + return setTimeout(() => { + this.client.socket.connected.should.equal(false) + return done() + }, 300) + }) + + return it('should not put the update in redis', function (done) { + rclient.llen( + redisSettings.documentupdater.key_schema.pendingUpdates({ + doc_id: this.doc_id, + }), + (error, len) => { + if (error) return done(error) + len.should.equal(0) + return done() + } + ) + return null + }) + }) + + describe('when authorized with unknown fields', function () { + before(function (done) { + this.update = { + ...this.update, + foo: 1, + } + return async.series( + [ + cb => { + return FixturesManager.setUpProject( + { + privilegeLevel: 'readOnly', + }, + (e, { project_id: projectId, user_id: userId }) => { + this.project_id = projectId + this.user_id = userId + return cb(e) + } + ) + }, + + cb => { + return FixturesManager.setUpDoc( + this.project_id, + { lines: this.lines, version: this.version, ops: this.ops }, + (e, { doc_id: docId }) => { + this.doc_id = docId + return cb(e) + } + ) + }, + + cb => { + this.client = RealTimeClient.connect(this.project_id, cb) + }, + + cb => { + return this.client.emit('joinDoc', this.doc_id, cb) + }, + + cb => { + return this.client.emit( + 'applyOtUpdate', + this.doc_id, + this.update, + error => { + this.error = error + return cb() + } + ) + }, + ], + done + ) + }) + + it('should return an error', function () { + return expect(this.error).to.exist + }) + + it('should disconnect the client', function (done) { + return setTimeout(() => { + this.client.socket.connected.should.equal(false) + return done() + }, 300) + }) + + return it('should not put the update in redis', function (done) { + rclient.llen( + redisSettings.documentupdater.key_schema.pendingUpdates({ + doc_id: this.doc_id, + }), + (error, len) => { + if (error) return done(error) + len.should.equal(0) + return done() + } + ) + return null + }) + }) })