From b193fe7121751fec4093cae98abb9194ba66877c Mon Sep 17 00:00:00 2001 From: Jakob Ackermann Date: Mon, 15 Jun 2026 07:29:26 +0200 Subject: [PATCH] [real-time] flag mismatching update.doc field and add fallback (#34531) GitOrigin-RevId: ade5aefc473a74da8a99df7cc33a88bc49885d5d --- .../real-time/app/js/WebsocketController.js | 8 + .../test/acceptance/js/ApplyUpdateTests.js | 165 ++++++++++++++++++ 2 files changed, 173 insertions(+) diff --git a/services/real-time/app/js/WebsocketController.js b/services/real-time/app/js/WebsocketController.js index 90128be1e5..2cbd54b4c8 100644 --- a/services/real-time/app/js/WebsocketController.js +++ b/services/real-time/app/js/WebsocketController.js @@ -576,6 +576,14 @@ export default WebsocketController = { ) return callback(error) } + if (update.doc && update.doc !== docId) { + return callback( + new Errors.CodedError( + 'update.doc must be identical to docId parameter in applyOtUpdate(docId, update)' + ) + ) + } + update.doc = docId if (!update.meta) { update.meta = {} } diff --git a/services/real-time/test/acceptance/js/ApplyUpdateTests.js b/services/real-time/test/acceptance/js/ApplyUpdateTests.js index cf854d11fe..dd9aa544dd 100644 --- a/services/real-time/test/acceptance/js/ApplyUpdateTests.js +++ b/services/real-time/test/acceptance/js/ApplyUpdateTests.js @@ -440,6 +440,97 @@ describe('applyOtUpdate', function () { }) }) + describe('when authorized with an edit update to a missing doc field', function () { + before(function (done) { + return async.series( + [ + cb => { + return FixturesManager.setUpProject( + { + privilegeLevel: 'readAndWrite', + }, + (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 + ) + }) + + after(function (done) { + async.series( + [ + cb => clearPendingUpdatesList(cb), + cb => + rclient.del( + redisSettings.documentupdater.key_schema.pendingUpdates({ + doc_id: this.doc_id, + }), + cb + ), + ], + done + ) + }) + + it('should not return an error', function () { + expect(this.error).to.not.exist + }) + + it('should put the update in redis', function (done) { + rclient.lrange( + redisSettings.documentupdater.key_schema.pendingUpdates({ + doc_id: this.doc_id, + }), + 0, + -1, + (error, entries) => { + if (error) return done(error) + entries.length.should.equal(1) + const update = JSON.parse(entries[0]) + update.should.include({ doc: this.doc_id }) + return done() + } + ) + return null + }) + }) + describe('when authorized with an edit update to an invalid doc', function () { before(function (done) { return async.series( @@ -518,6 +609,80 @@ describe('applyOtUpdate', function () { }) }) + describe('when authorized with an edit update to a different doc', function () { + before(function (done) { + return async.series( + [ + cb => { + return FixturesManager.setUpProject( + { + privilegeLevel: 'readAndWrite', + }, + (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, + { doc: 'other-doc' }, + error => { + this.error = error + return cb() + } + ) + }, + ], + done + ) + }) + + it('should return an error', function () { + expect(this.error).to.deep.equal({ + message: + 'update.doc must be identical to docId parameter in applyOtUpdate(docId, update)', + }) + }) + + 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 an invalid edit update', function () { before(function (done) { return async.series(