| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782 |
- import { vi, expect, describe, beforeEach, afterEach, it } from 'vitest'
- /* eslint-disable
- no-return-assign,
- no-throw-literal,
- no-unused-vars,
- */
- // TODO: This file was created by bulk-decaffeinate.
- // Fix any style issues and re-enable lint.
- /*
- * decaffeinate suggestions:
- * DS101: Remove unnecessary use of Array.from
- * DS102: Remove unnecessary code created because of implicit returns
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
- import sinon from 'sinon'
- import tk from 'timekeeper'
- import Errors from '../../../app/js/Errors.js'
- const modulePath = '../../../app/js/WebsocketController.js'
- const { UpdateTooLargeError } = Errors
- describe('WebsocketController', function () {
- beforeEach(async function (ctx) {
- tk.freeze(new Date())
- ctx.project_id = 'project-id-123'
- ctx.user = {
- _id: (ctx.user_id = 'user-id-123'),
- first_name: 'James',
- last_name: 'Allen',
- email: 'james@example.com',
- signUpDate: new Date('2014-01-01'),
- loginCount: 42,
- }
- ctx.callback = sinon.stub()
- ctx.client = {
- disconnected: false,
- id: (ctx.client_id = 'mock-client-id-123'),
- publicId: `other-id-${Math.random()}`,
- ol_context: {},
- joinLeaveEpoch: 0,
- join: sinon.stub(),
- leave: sinon.stub(),
- }
- vi.doMock('../../../app/js/WebApiManager', () => ({
- default: (ctx.WebApiManager = {}),
- }))
- vi.doMock('../../../app/js/AuthorizationManager', () => ({
- default: (ctx.AuthorizationManager = {}),
- }))
- vi.doMock('../../../app/js/DocumentUpdaterManager', () => ({
- default: (ctx.DocumentUpdaterManager = {}),
- }))
- vi.doMock('../../../app/js/ConnectedUsersManager', () => ({
- default: (ctx.ConnectedUsersManager = {}),
- }))
- vi.doMock('../../../app/js/WebsocketLoadBalancer', () => ({
- default: (ctx.WebsocketLoadBalancer = {}),
- }))
- vi.doMock('@overleaf/metrics', () => ({
- default: (ctx.metrics = {
- inc: sinon.stub(),
- set: sinon.stub(),
- }),
- }))
- vi.doMock('../../../app/js/RoomManager', () => ({
- default: (ctx.RoomManager = {}),
- }))
- ctx.WebsocketController = (await import(modulePath)).default
- })
- afterEach(function () {
- return tk.reset()
- })
- describe('joinProject', function () {
- describe('when authorised', function () {
- beforeEach(function (ctx) {
- ctx.client.id = 'mock-client-id'
- ctx.project = {
- name: 'Test Project',
- owner: {
- _id: (ctx.owner_id = 'mock-owner-id-123'),
- },
- }
- ctx.privilegeLevel = 'owner'
- ctx.ConnectedUsersManager.updateUserPosition = sinon
- .stub()
- .callsArgAsync(4)
- ctx.isRestrictedUser = true
- ctx.isTokenMember = true
- ctx.isInvitedMember = true
- ctx.WebApiManager.joinProject = sinon
- .stub()
- .callsArgWith(2, null, ctx.project, ctx.privilegeLevel, {
- isRestrictedUser: ctx.isRestrictedUser,
- isTokenMember: ctx.isTokenMember,
- isInvitedMember: ctx.isInvitedMember,
- })
- ctx.RoomManager.joinProject = sinon.stub().callsArg(2)
- return ctx.WebsocketController.joinProject(
- ctx.client,
- ctx.user,
- ctx.project_id,
- ctx.callback
- )
- })
- it('should load the project from web', function (ctx) {
- return ctx.WebApiManager.joinProject
- .calledWith(ctx.project_id, ctx.user)
- .should.equal(true)
- })
- it('should join the project room', function (ctx) {
- return ctx.RoomManager.joinProject
- .calledWith(ctx.client, ctx.project_id)
- .should.equal(true)
- })
- it('should set the privilege level on the client', function (ctx) {
- return ctx.client.ol_context.privilege_level.should.equal(
- ctx.privilegeLevel
- )
- })
- it("should set the user's id on the client", function (ctx) {
- return ctx.client.ol_context.user_id.should.equal(ctx.user._id)
- })
- it("should set the user's email on the client", function (ctx) {
- return ctx.client.ol_context.email.should.equal(ctx.user.email)
- })
- it("should set the user's first_name on the client", function (ctx) {
- return ctx.client.ol_context.first_name.should.equal(
- ctx.user.first_name
- )
- })
- it("should set the user's last_name on the client", function (ctx) {
- return ctx.client.ol_context.last_name.should.equal(ctx.user.last_name)
- })
- it("should set the user's sign up date on the client", function (ctx) {
- return ctx.client.ol_context.signup_date.should.equal(
- ctx.user.signUpDate
- )
- })
- it("should set the user's login_count on the client", function (ctx) {
- return ctx.client.ol_context.login_count.should.equal(
- ctx.user.loginCount
- )
- })
- it('should set the connected time on the client', function (ctx) {
- return ctx.client.ol_context.connected_time.should.equal(new Date())
- })
- it('should set the project_id on the client', function (ctx) {
- return ctx.client.ol_context.project_id.should.equal(ctx.project_id)
- })
- it('should set the project owner id on the client', function (ctx) {
- return ctx.client.ol_context.owner_id.should.equal(ctx.owner_id)
- })
- it('should set the is_restricted_user flag on the client', function (ctx) {
- return ctx.client.ol_context.is_restricted_user.should.equal(
- ctx.isRestrictedUser
- )
- })
- it('should set the is_token_member flag on the client', function (ctx) {
- ctx.client.ol_context.is_token_member.should.equal(ctx.isTokenMember)
- })
- it('should set the is_invited_member flag on the client', function (ctx) {
- ctx.client.ol_context.is_invited_member.should.equal(
- ctx.isInvitedMember
- )
- })
- it('should call the callback with the project, privilegeLevel and protocolVersion', function (ctx) {
- return ctx.callback
- .calledWith(
- null,
- ctx.project,
- ctx.privilegeLevel,
- ctx.WebsocketController.PROTOCOL_VERSION
- )
- .should.equal(true)
- })
- it('should mark the user as connected in ConnectedUsersManager', function (ctx) {
- return ctx.ConnectedUsersManager.updateUserPosition
- .calledWith(ctx.project_id, ctx.client.publicId, ctx.user, null)
- .should.equal(true)
- })
- return it('should increment the join-project metric', function (ctx) {
- return ctx.metrics.inc
- .calledWith('editor.join-project')
- .should.equal(true)
- })
- })
- describe('when not authorized', function () {
- beforeEach(function (ctx) {
- ctx.WebApiManager.joinProject = sinon
- .stub()
- .callsArgWith(2, null, null, null)
- return ctx.WebsocketController.joinProject(
- ctx.client,
- ctx.user,
- ctx.project_id,
- ctx.callback
- )
- })
- it('should return an error', function (ctx) {
- return ctx.callback
- .calledWith(sinon.match({ message: 'not authorized' }))
- .should.equal(true)
- })
- return it('should not log an error', function (ctx) {
- return ctx.logger.error.called.should.equal(false)
- })
- })
- describe('when the subscribe failed', function () {
- beforeEach(function (ctx) {
- ctx.client.id = 'mock-client-id'
- ctx.project = {
- name: 'Test Project',
- owner: {
- _id: (ctx.owner_id = 'mock-owner-id-123'),
- },
- }
- ctx.privilegeLevel = 'owner'
- ctx.ConnectedUsersManager.updateUserPosition = sinon
- .stub()
- .callsArgAsync(4)
- ctx.isRestrictedUser = true
- ctx.isTokenMember = true
- ctx.isInvitedMember = true
- ctx.WebApiManager.joinProject = sinon
- .stub()
- .callsArgWith(2, null, ctx.project, ctx.privilegeLevel, {
- isRestrictedUser: ctx.isRestrictedUser,
- isTokenMember: ctx.isTokenMember,
- isInvitedMember: ctx.isInvitedMember,
- })
- ctx.RoomManager.joinProject = sinon
- .stub()
- .callsArgWith(2, new Error('subscribe failed'))
- return ctx.WebsocketController.joinProject(
- ctx.client,
- ctx.user,
- ctx.project_id,
- ctx.callback
- )
- })
- return it('should return an error', function (ctx) {
- ctx.callback
- .calledWith(sinon.match({ message: 'subscribe failed' }))
- .should.equal(true)
- return ctx.callback.args[0][0].message.should.equal('subscribe failed')
- })
- })
- describe('when the client has disconnected', function () {
- beforeEach(function (ctx) {
- ctx.client.disconnected = true
- ctx.WebApiManager.joinProject = sinon.stub().callsArg(2)
- return ctx.WebsocketController.joinProject(
- ctx.client,
- ctx.user,
- ctx.project_id,
- ctx.callback
- )
- })
- it('should not call WebApiManager.joinProject', function (ctx) {
- return expect(ctx.WebApiManager.joinProject.called).to.equal(false)
- })
- it('should call the callback with no details', function (ctx) {
- return expect(ctx.callback.args[0]).to.deep.equal([])
- })
- return it('should increment the editor.join-project.disconnected metric with a status', function (ctx) {
- return expect(
- ctx.metrics.inc.calledWith('editor.join-project.disconnected', 1, {
- status: 'immediately',
- })
- ).to.equal(true)
- })
- })
- return describe('when the client disconnects while WebApiManager.joinProject is running', function () {
- beforeEach(function (ctx) {
- ctx.WebApiManager.joinProject = (project, user, cb) => {
- ctx.client.disconnected = true
- return cb(null, ctx.project, ctx.privilegeLevel, {
- isRestrictedUser: ctx.isRestrictedUser,
- isTokenMember: ctx.isTokenMember,
- isInvitedMember: ctx.isInvitedMember,
- })
- }
- return ctx.WebsocketController.joinProject(
- ctx.client,
- ctx.user,
- ctx.project_id,
- ctx.callback
- )
- })
- it('should call the callback with no details', function (ctx) {
- return expect(ctx.callback.args[0]).to.deep.equal([])
- })
- return it('should increment the editor.join-project.disconnected metric with a status', function (ctx) {
- return expect(
- ctx.metrics.inc.calledWith('editor.join-project.disconnected', 1, {
- status: 'after-web-api-call',
- })
- ).to.equal(true)
- })
- })
- })
- describe('leaveProject', function () {
- beforeEach(function (ctx) {
- ctx.DocumentUpdaterManager.flushProjectToMongoAndDelete = sinon
- .stub()
- .callsArg(1)
- ctx.ConnectedUsersManager.markUserAsDisconnected = sinon
- .stub()
- .callsArg(2)
- ctx.WebsocketLoadBalancer.emitToRoom = sinon.stub()
- ctx.RoomManager.leaveProjectAndDocs = sinon.stub()
- ctx.clientsInRoom = []
- ctx.io = {
- sockets: {
- clients: roomId => {
- if (roomId !== ctx.project_id) {
- throw 'expected room_id to be project_id'
- }
- return ctx.clientsInRoom
- },
- },
- }
- ctx.client.ol_context.project_id = ctx.project_id
- ctx.client.ol_context.user_id = ctx.user_id
- ctx.WebsocketController.FLUSH_IF_EMPTY_DELAY = 0
- tk.reset()
- }) // Allow setTimeout to work.
- describe('when the client did not joined a project yet', function () {
- beforeEach(async function (ctx) {
- await new Promise((resolve, reject) => {
- ctx.client.ol_context = {}
- ctx.WebsocketController.leaveProject(ctx.io, ctx.client, err => {
- if (err) return reject(err)
- resolve()
- })
- })
- })
- it('should bail out when calling leaveProject', function (ctx) {
- ctx.WebsocketLoadBalancer.emitToRoom.called.should.equal(false)
- ctx.RoomManager.leaveProjectAndDocs.called.should.equal(false)
- ctx.ConnectedUsersManager.markUserAsDisconnected.called.should.equal(
- false
- )
- })
- it('should not inc any metric', function (ctx) {
- ctx.metrics.inc.called.should.equal(false)
- })
- })
- describe('when the project is empty', function () {
- beforeEach(async function (ctx) {
- await new Promise((resolve, reject) => {
- ctx.clientsInRoom = []
- ctx.WebsocketController.leaveProject(ctx.io, ctx.client, err => {
- if (err) return reject(err)
- resolve()
- })
- })
- })
- it('should end clientTracking.clientDisconnected to the project room', function (ctx) {
- ctx.WebsocketLoadBalancer.emitToRoom
- .calledWith(
- ctx.project_id,
- 'clientTracking.clientDisconnected',
- ctx.client.publicId
- )
- .should.equal(true)
- })
- it('should mark the user as disconnected', function (ctx) {
- ctx.ConnectedUsersManager.markUserAsDisconnected
- .calledWith(ctx.project_id, ctx.client.publicId)
- .should.equal(true)
- })
- it('should flush the project in the document updater', function (ctx) {
- ctx.DocumentUpdaterManager.flushProjectToMongoAndDelete
- .calledWith(ctx.project_id)
- .should.equal(true)
- })
- it('should increment the leave-project metric', function (ctx) {
- ctx.metrics.inc.calledWith('editor.leave-project').should.equal(true)
- })
- it('should track the disconnection in RoomManager', function (ctx) {
- ctx.RoomManager.leaveProjectAndDocs
- .calledWith(ctx.client)
- .should.equal(true)
- })
- })
- describe('when the project is not empty', function () {
- beforeEach(async function (ctx) {
- await new Promise((resolve, reject) => {
- ctx.clientsInRoom = ['mock-remaining-client']
- ctx.io = {
- sockets: {
- clients: roomId => {
- if (roomId !== ctx.project_id) {
- throw 'expected room_id to be project_id'
- }
- return ctx.clientsInRoom
- },
- },
- }
- ctx.WebsocketController.leaveProject(ctx.io, ctx.client, err => {
- if (err) return reject(err)
- resolve()
- })
- })
- })
- it('should not flush the project in the document updater', function (ctx) {
- ctx.DocumentUpdaterManager.flushProjectToMongoAndDelete.called.should.equal(
- false
- )
- })
- })
- describe('when client has not authenticated', function () {
- beforeEach(async function (ctx) {
- await new Promise((resolve, reject) => {
- ctx.client.ol_context.user_id = null
- ctx.client.ol_context.project_id = null
- ctx.WebsocketController.leaveProject(ctx.io, ctx.client, err => {
- if (err) return reject(err)
- resolve()
- })
- })
- })
- it('should not end clientTracking.clientDisconnected to the project room', function (ctx) {
- ctx.WebsocketLoadBalancer.emitToRoom
- .calledWith(
- ctx.project_id,
- 'clientTracking.clientDisconnected',
- ctx.client.publicId
- )
- .should.equal(false)
- })
- it('should not mark the user as disconnected', function (ctx) {
- ctx.ConnectedUsersManager.markUserAsDisconnected
- .calledWith(ctx.project_id, ctx.client.publicId)
- .should.equal(false)
- })
- it('should not flush the project in the document updater', function (ctx) {
- ctx.DocumentUpdaterManager.flushProjectToMongoAndDelete
- .calledWith(ctx.project_id)
- .should.equal(false)
- })
- it('should not increment the leave-project metric', function (ctx) {
- ctx.metrics.inc.calledWith('editor.leave-project').should.equal(false)
- })
- })
- describe('when client has not joined a project', function () {
- beforeEach(async function (ctx) {
- await new Promise((resolve, reject) => {
- ctx.client.ol_context.user_id = ctx.user_id
- ctx.client.ol_context.project_id = null
- ctx.WebsocketController.leaveProject(ctx.io, ctx.client, err => {
- if (err) return reject(err)
- resolve()
- })
- })
- })
- it('should not end clientTracking.clientDisconnected to the project room', function (ctx) {
- ctx.WebsocketLoadBalancer.emitToRoom
- .calledWith(
- ctx.project_id,
- 'clientTracking.clientDisconnected',
- ctx.client.publicId
- )
- .should.equal(false)
- })
- it('should not mark the user as disconnected', function (ctx) {
- ctx.ConnectedUsersManager.markUserAsDisconnected
- .calledWith(ctx.project_id, ctx.client.publicId)
- .should.equal(false)
- })
- it('should not flush the project in the document updater', function (ctx) {
- ctx.DocumentUpdaterManager.flushProjectToMongoAndDelete
- .calledWith(ctx.project_id)
- .should.equal(false)
- })
- it('should not increment the leave-project metric', function (ctx) {
- ctx.metrics.inc.calledWith('editor.leave-project').should.equal(false)
- })
- })
- })
- describe('joinDoc', function () {
- beforeEach(function (ctx) {
- ctx.doc_id = 'doc-id-123'
- ctx.doc_lines = ['doc', 'lines']
- ctx.version = 42
- ctx.ops = ['mock', 'ops']
- ctx.ranges = { mock: 'ranges' }
- ctx.options = {}
- ctx.client.ol_context.project_id = ctx.project_id
- ctx.client.ol_context.is_restricted_user = false
- ctx.AuthorizationManager.addAccessToDoc = sinon.stub().yields()
- ctx.AuthorizationManager.assertClientCanViewProject = sinon
- .stub()
- .callsArgWith(1, null)
- ctx.AuthorizationManager.assertClientCanViewProjectAndDoc = sinon
- .stub()
- .callsArgWith(2, null)
- ctx.DocumentUpdaterManager.getDocument = sinon
- .stub()
- .callsArgWith(3, null, {
- lines: ctx.doc_lines,
- version: ctx.version,
- ranges: ctx.ranges,
- ops: ctx.ops,
- })
- ctx.RoomManager.joinDoc = sinon.stub().callsArg(2)
- })
- describe('works', function () {
- beforeEach(function (ctx) {
- ctx.WebsocketController.joinDoc(
- ctx.client,
- ctx.doc_id,
- -1,
- ctx.options,
- ctx.callback
- )
- })
- it('should inc the joinLeaveEpoch', function (ctx) {
- expect(ctx.client.joinLeaveEpoch).to.equal(1)
- })
- it('should check that the client is authorized to view the project', function (ctx) {
- ctx.AuthorizationManager.assertClientCanViewProject
- .calledWith(ctx.client)
- .should.equal(true)
- })
- it('should get the document from the DocumentUpdaterManager with fromVersion', function (ctx) {
- ctx.DocumentUpdaterManager.getDocument
- .calledWith(ctx.project_id, ctx.doc_id, -1)
- .should.equal(true)
- })
- it('should add permissions for the client to access the doc', function (ctx) {
- ctx.AuthorizationManager.addAccessToDoc
- .calledWith(ctx.client, ctx.doc_id)
- .should.equal(true)
- })
- it('should join the client to room for the doc_id', function (ctx) {
- ctx.RoomManager.joinDoc
- .calledWith(ctx.client, ctx.doc_id)
- .should.equal(true)
- })
- it('should call the callback with the lines, version, ranges and ops', function (ctx) {
- ctx.callback
- .calledWith(null, ctx.doc_lines, ctx.version, ctx.ops, ctx.ranges)
- .should.equal(true)
- })
- it('should increment the join-doc metric', function (ctx) {
- ctx.metrics.inc.calledWith('editor.join-doc').should.equal(true)
- })
- })
- describe('with a fromVersion', function () {
- beforeEach(function (ctx) {
- ctx.fromVersion = 40
- ctx.WebsocketController.joinDoc(
- ctx.client,
- ctx.doc_id,
- ctx.fromVersion,
- ctx.options,
- ctx.callback
- )
- })
- it('should get the document from the DocumentUpdaterManager with fromVersion', function (ctx) {
- ctx.DocumentUpdaterManager.getDocument
- .calledWith(ctx.project_id, ctx.doc_id, ctx.fromVersion)
- .should.equal(true)
- })
- })
- describe('with doclines that need escaping', function () {
- beforeEach(function (ctx) {
- ctx.doc_lines.push(['räksmörgås'])
- ctx.WebsocketController.joinDoc(
- ctx.client,
- ctx.doc_id,
- -1,
- ctx.options,
- ctx.callback
- )
- })
- it('should call the callback with the escaped lines', function (ctx) {
- const escapedLines = ctx.callback.args[0][1]
- const escapedWord = escapedLines.pop()
- escapedWord.should.equal('räksmörgås')
- // Check that unescaping works
- decodeURIComponent(escape(escapedWord)).should.equal('räksmörgås')
- })
- })
- describe('with comments that need encoding', function () {
- beforeEach(function (ctx) {
- ctx.ranges.comments = [{ op: { c: 'räksmörgås' } }]
- ctx.WebsocketController.joinDoc(
- ctx.client,
- ctx.doc_id,
- -1,
- { encodeRanges: true },
- ctx.callback
- )
- })
- it('should call the callback with the encoded comment', function (ctx) {
- const encodedComments = ctx.callback.args[0][4]
- const encodedComment = encodedComments.comments.pop()
- const encodedCommentText = encodedComment.op.c
- encodedCommentText.should.equal('räksmörgås')
- })
- })
- describe('with changes that need encoding', function () {
- it('should call the callback with the encoded insert change', function (ctx) {
- ctx.ranges.changes = [{ op: { i: 'räksmörgås' } }]
- ctx.WebsocketController.joinDoc(
- ctx.client,
- ctx.doc_id,
- -1,
- { encodeRanges: true },
- ctx.callback
- )
- const encodedChanges = ctx.callback.args[0][4]
- const encodedChange = encodedChanges.changes.pop()
- const encodedChangeText = encodedChange.op.i
- encodedChangeText.should.equal('räksmörgås')
- })
- it('should call the callback with the encoded delete change', function (ctx) {
- ctx.ranges.changes = [{ op: { d: 'räksmörgås' } }]
- ctx.WebsocketController.joinDoc(
- ctx.client,
- ctx.doc_id,
- -1,
- { encodeRanges: true },
- ctx.callback
- )
- const encodedChanges = ctx.callback.args[0][4]
- const encodedChange = encodedChanges.changes.pop()
- const encodedChangeText = encodedChange.op.d
- encodedChangeText.should.equal('räksmörgås')
- })
- })
- describe('when not authorized', function () {
- beforeEach(function (ctx) {
- ctx.AuthorizationManager.assertClientCanViewProject = sinon
- .stub()
- .callsArgWith(1, (ctx.err = new Error('not authorized')))
- ctx.WebsocketController.joinDoc(
- ctx.client,
- ctx.doc_id,
- -1,
- ctx.options,
- ctx.callback
- )
- })
- it('should call the callback with an error', function (ctx) {
- ctx.callback
- .calledWith(sinon.match({ message: 'not authorized' }))
- .should.equal(true)
- })
- it('should not call the DocumentUpdaterManager', function (ctx) {
- ctx.DocumentUpdaterManager.getDocument.called.should.equal(false)
- })
- })
- describe('when the DocumentUpdaterManager returns an error', function () {
- beforeEach(function (ctx) {
- ctx.err = new Error('document updater error')
- ctx.DocumentUpdaterManager.getDocument = sinon
- .stub()
- .callsArgWith(3, ctx.err)
- ctx.WebsocketController.joinDoc(
- ctx.client,
- ctx.doc_id,
- -1,
- ctx.options,
- ctx.callback
- )
- })
- it('should call the callback with the error', function (ctx) {
- ctx.callback
- .calledWith(sinon.match({ message: 'document updater error' }))
- .should.equal(true)
- })
- })
- describe('with a restricted client', function () {
- beforeEach(function (ctx) {
- ctx.ranges.comments = [{ op: { a: 1 } }, { op: { a: 2 } }]
- ctx.client.ol_context.is_restricted_user = true
- ctx.WebsocketController.joinDoc(
- ctx.client,
- ctx.doc_id,
- -1,
- ctx.options,
- ctx.callback
- )
- })
- it('should overwrite ranges.comments with an empty list', function (ctx) {
- const ranges = ctx.callback.args[0][4]
- expect(ranges.comments).to.deep.equal([])
- })
- })
- describe('when the client has disconnected', function () {
- beforeEach(function (ctx) {
- ctx.client.disconnected = true
- ctx.WebsocketController.joinDoc(
- ctx.client,
- ctx.doc_id,
- -1,
- ctx.options,
- ctx.callback
- )
- })
- it('should call the callback with no details', function (ctx) {
- expect(ctx.callback.args[0]).to.deep.equal([])
- })
- it('should increment the editor.join-doc.disconnected metric with a status', function (ctx) {
- expect(
- ctx.metrics.inc.calledWith('editor.join-doc.disconnected', 1, {
- status: 'immediately',
- })
- ).to.equal(true)
- })
- it('should not get the document', function (ctx) {
- expect(ctx.DocumentUpdaterManager.getDocument.called).to.equal(false)
- })
- })
- describe('when the client disconnects while auth checks are running', function () {
- beforeEach(async function (ctx) {
- await new Promise((resolve, reject) => {
- ctx.AuthorizationManager.assertClientCanViewProjectAndDoc.yields(
- new Error()
- )
- ctx.DocumentUpdaterManager.checkDocument = (projectId, docId, cb) => {
- ctx.client.disconnected = true
- cb()
- }
- ctx.WebsocketController.joinDoc(
- ctx.client,
- ctx.doc_id,
- -1,
- ctx.options,
- (...args) => {
- ctx.callback(...args)
- resolve()
- }
- )
- })
- })
- it('should call the callback with no details', function (ctx) {
- expect(ctx.callback.called).to.equal(true)
- expect(ctx.callback.args[0]).to.deep.equal([])
- })
- it('should increment the editor.join-doc.disconnected metric with a status', function (ctx) {
- expect(
- ctx.metrics.inc.calledWith('editor.join-doc.disconnected', 1, {
- status: 'after-client-auth-check',
- })
- ).to.equal(true)
- })
- it('should not get the document', function (ctx) {
- expect(ctx.DocumentUpdaterManager.getDocument.called).to.equal(false)
- })
- })
- describe('when the client starts a parallel joinDoc request', function () {
- beforeEach(async function (ctx) {
- await new Promise((resolve, reject) => {
- ctx.AuthorizationManager.assertClientCanViewProjectAndDoc.yields(
- new Error()
- )
- ctx.DocumentUpdaterManager.checkDocument = (projectId, docId, cb) => {
- ctx.DocumentUpdaterManager.checkDocument = sinon.stub().yields()
- ctx.WebsocketController.joinDoc(
- ctx.client,
- ctx.doc_id,
- -1,
- {},
- () => {}
- )
- cb()
- }
- ctx.WebsocketController.joinDoc(
- ctx.client,
- ctx.doc_id,
- -1,
- ctx.options,
- (...args) => {
- ctx.callback(...args)
- // make sure the other joinDoc request completed
- setTimeout(resolve, 5)
- }
- )
- })
- })
- it('should call the callback with an error', function (ctx) {
- expect(ctx.callback.called).to.equal(true)
- expect(ctx.callback.args[0][0].message).to.equal(
- 'joinLeaveEpoch mismatch'
- )
- })
- it('should get the document once (the parallel request wins)', function (ctx) {
- expect(ctx.DocumentUpdaterManager.getDocument.callCount).to.equal(1)
- })
- })
- describe('when the client starts a parallel leaveDoc request', function () {
- beforeEach(async function (ctx) {
- await new Promise((resolve, reject) => {
- ctx.RoomManager.leaveDoc = sinon.stub()
- ctx.AuthorizationManager.assertClientCanViewProjectAndDoc.yields(
- new Error()
- )
- ctx.DocumentUpdaterManager.checkDocument = (projectId, docId, cb) => {
- ctx.WebsocketController.leaveDoc(ctx.client, ctx.doc_id, () => {})
- cb()
- }
- ctx.WebsocketController.joinDoc(
- ctx.client,
- ctx.doc_id,
- -1,
- ctx.options,
- (...args) => {
- ctx.callback(...args)
- resolve()
- }
- )
- })
- })
- it('should call the callback with an error', function (ctx) {
- expect(ctx.callback.called).to.equal(true)
- expect(ctx.callback.args[0][0].message).to.equal(
- 'joinLeaveEpoch mismatch'
- )
- })
- it('should not get the document', function (ctx) {
- expect(ctx.DocumentUpdaterManager.getDocument.called).to.equal(false)
- })
- })
- describe('when the client disconnects while RoomManager.joinDoc is running', function () {
- beforeEach(function (ctx) {
- ctx.RoomManager.joinDoc = (client, docId, cb) => {
- ctx.client.disconnected = true
- cb()
- }
- ctx.WebsocketController.joinDoc(
- ctx.client,
- ctx.doc_id,
- -1,
- ctx.options,
- ctx.callback
- )
- })
- it('should call the callback with no details', function (ctx) {
- expect(ctx.callback.args[0]).to.deep.equal([])
- })
- it('should increment the editor.join-doc.disconnected metric with a status', function (ctx) {
- expect(
- ctx.metrics.inc.calledWith('editor.join-doc.disconnected', 1, {
- status: 'after-joining-room',
- })
- ).to.equal(true)
- })
- it('should not get the document', function (ctx) {
- expect(ctx.DocumentUpdaterManager.getDocument.called).to.equal(false)
- })
- })
- describe('when the client disconnects while DocumentUpdaterManager.getDocument is running', function () {
- beforeEach(function (ctx) {
- ctx.DocumentUpdaterManager.getDocument = (
- projectId,
- docId,
- fromVersion,
- callback
- ) => {
- ctx.client.disconnected = true
- return callback(null, {
- lines: ctx.doc_lines,
- version: ctx.version,
- ranges: ctx.ranges,
- ops: ctx.ops,
- })
- }
- ctx.WebsocketController.joinDoc(
- ctx.client,
- ctx.doc_id,
- -1,
- ctx.options,
- ctx.callback
- )
- })
- it('should call the callback with no details', function (ctx) {
- expect(ctx.callback.args[0]).to.deep.equal([])
- })
- it('should increment the editor.join-doc.disconnected metric with a status', function (ctx) {
- expect(
- ctx.metrics.inc.calledWith('editor.join-doc.disconnected', 1, {
- status: 'after-doc-updater-call',
- })
- ).to.equal(true)
- })
- })
- })
- describe('leaveDoc', function () {
- beforeEach(function (ctx) {
- ctx.doc_id = 'doc-id-123'
- ctx.client.ol_context.project_id = ctx.project_id
- ctx.RoomManager.leaveDoc = sinon.stub()
- ctx.WebsocketController.leaveDoc(ctx.client, ctx.doc_id, ctx.callback)
- })
- it('should inc the joinLeaveEpoch', function (ctx) {
- expect(ctx.client.joinLeaveEpoch).to.equal(1)
- })
- it('should remove the client from the doc_id room', function (ctx) {
- ctx.RoomManager.leaveDoc
- .calledWith(ctx.client, ctx.doc_id)
- .should.equal(true)
- })
- it('should call the callback', function (ctx) {
- ctx.callback.called.should.equal(true)
- })
- it('should increment the leave-doc metric', function (ctx) {
- ctx.metrics.inc.calledWith('editor.leave-doc').should.equal(true)
- })
- })
- describe('getConnectedUsers', function () {
- beforeEach(function (ctx) {
- ctx.client.ol_context.project_id = ctx.project_id
- ctx.users = ['mock', 'users']
- ctx.WebsocketLoadBalancer.emitToRoom = sinon.stub()
- ctx.ConnectedUsersManager.getConnectedUsers = sinon
- .stub()
- .callsArgWith(1, null, ctx.users)
- })
- describe('when authorized', function () {
- beforeEach(async function (ctx) {
- await new Promise((resolve, reject) => {
- ctx.AuthorizationManager.assertClientCanViewProject = sinon
- .stub()
- .callsArgWith(1, null)
- ctx.WebsocketController.getConnectedUsers(ctx.client, (...args) => {
- ctx.callback(...Array.from(args || []))
- resolve()
- })
- })
- })
- it('should check that the client is authorized to view the project', function (ctx) {
- ctx.AuthorizationManager.assertClientCanViewProject
- .calledWith(ctx.client)
- .should.equal(true)
- })
- it('should broadcast a request to update the client list', function (ctx) {
- ctx.WebsocketLoadBalancer.emitToRoom
- .calledWith(ctx.project_id, 'clientTracking.refresh')
- .should.equal(true)
- })
- it('should get the connected users for the project', function (ctx) {
- ctx.ConnectedUsersManager.getConnectedUsers
- .calledWith(ctx.project_id)
- .should.equal(true)
- })
- it('should the users', function (ctx) {
- ctx.callback.calledWith(null, ctx.users).should.equal(true)
- })
- it('should increment the get-connected-users metric', function (ctx) {
- ctx.metrics.inc
- .calledWith('editor.get-connected-users')
- .should.equal(true)
- })
- })
- describe('when not authorized', function () {
- beforeEach(function (ctx) {
- ctx.AuthorizationManager.assertClientCanViewProject = sinon
- .stub()
- .callsArgWith(1, (ctx.err = new Error('not authorized')))
- ctx.WebsocketController.getConnectedUsers(ctx.client, ctx.callback)
- })
- it('should not get the connected users for the project', function (ctx) {
- ctx.ConnectedUsersManager.getConnectedUsers.called.should.equal(false)
- })
- it('should return an error', function (ctx) {
- ctx.callback.calledWith(ctx.err).should.equal(true)
- })
- })
- describe('when restricted user', function () {
- beforeEach(function (ctx) {
- ctx.client.ol_context.is_restricted_user = true
- ctx.AuthorizationManager.assertClientCanViewProject = sinon
- .stub()
- .callsArgWith(1, null)
- ctx.WebsocketController.getConnectedUsers(ctx.client, ctx.callback)
- })
- it('should return an empty array of users', function (ctx) {
- ctx.callback.calledWith(null, []).should.equal(true)
- })
- it('should not get the connected users for the project', function (ctx) {
- ctx.ConnectedUsersManager.getConnectedUsers.called.should.equal(false)
- })
- })
- describe('when the client has disconnected', function () {
- beforeEach(function (ctx) {
- ctx.client.disconnected = true
- ctx.AuthorizationManager.assertClientCanViewProject = sinon.stub()
- ctx.WebsocketController.getConnectedUsers(ctx.client, ctx.callback)
- })
- it('should call the callback with no details', function (ctx) {
- expect(ctx.callback.args[0]).to.deep.equal([])
- })
- it('should not check permissions', function (ctx) {
- expect(
- ctx.AuthorizationManager.assertClientCanViewProject.called
- ).to.equal(false)
- })
- })
- })
- describe('updateClientPosition', function () {
- beforeEach(function (ctx) {
- ctx.WebsocketLoadBalancer.emitToRoom = sinon.stub()
- ctx.ConnectedUsersManager.updateUserPosition = sinon
- .stub()
- .callsArgAsync(4)
- ctx.AuthorizationManager.assertClientCanViewProjectAndDoc = sinon
- .stub()
- .callsArgWith(2, null)
- ctx.update = {
- doc_id: (ctx.doc_id = 'doc-id-123'),
- row: (ctx.row = 42),
- column: (ctx.column = 37),
- }
- })
- describe('with a logged in user', function () {
- beforeEach(async function (ctx) {
- await new Promise((resolve, reject) => {
- ctx.client.ol_context = {
- project_id: ctx.project_id,
- first_name: (ctx.first_name = 'Douglas'),
- last_name: (ctx.last_name = 'Adams'),
- email: (ctx.email = 'joe@example.com'),
- user_id: (ctx.user_id = 'user-id-123'),
- }
- ctx.populatedCursorData = {
- doc_id: ctx.doc_id,
- id: ctx.client.publicId,
- name: `${ctx.first_name} ${ctx.last_name}`,
- row: ctx.row,
- column: ctx.column,
- email: ctx.email,
- user_id: ctx.user_id,
- }
- ctx.WebsocketController.updateClientPosition(
- ctx.client,
- ctx.update,
- err => {
- if (err) return reject(err)
- resolve()
- }
- )
- })
- })
- it("should send the update to the project room with the user's name", function (ctx) {
- ctx.WebsocketLoadBalancer.emitToRoom
- .calledWith(
- ctx.project_id,
- 'clientTracking.clientUpdated',
- ctx.populatedCursorData
- )
- .should.equal(true)
- })
- it('should send the cursor data to the connected user manager', async function (ctx) {
- await new Promise((resolve, reject) => {
- ctx.ConnectedUsersManager.updateUserPosition
- .calledWith(
- ctx.project_id,
- ctx.client.publicId,
- {
- _id: ctx.user_id,
- email: ctx.email,
- first_name: ctx.first_name,
- last_name: ctx.last_name,
- },
- {
- row: ctx.row,
- column: ctx.column,
- doc_id: ctx.doc_id,
- }
- )
- .should.equal(true)
- resolve()
- })
- })
- it('should increment the update-client-position metric at 0.1 frequency', function (ctx) {
- ctx.metrics.inc
- .calledWith('editor.update-client-position', 0.1)
- .should.equal(true)
- })
- })
- describe('with a logged in user who has no last_name set', function () {
- beforeEach(async function (ctx) {
- await new Promise((resolve, reject) => {
- ctx.client.ol_context = {
- project_id: ctx.project_id,
- first_name: (ctx.first_name = 'Douglas'),
- last_name: undefined,
- email: (ctx.email = 'joe@example.com'),
- user_id: (ctx.user_id = 'user-id-123'),
- }
- ctx.populatedCursorData = {
- doc_id: ctx.doc_id,
- id: ctx.client.publicId,
- name: `${ctx.first_name}`,
- row: ctx.row,
- column: ctx.column,
- email: ctx.email,
- user_id: ctx.user_id,
- }
- ctx.WebsocketController.updateClientPosition(
- ctx.client,
- ctx.update,
- err => {
- if (err) return reject(err)
- resolve()
- }
- )
- })
- })
- it("should send the update to the project room with the user's name", function (ctx) {
- ctx.WebsocketLoadBalancer.emitToRoom
- .calledWith(
- ctx.project_id,
- 'clientTracking.clientUpdated',
- ctx.populatedCursorData
- )
- .should.equal(true)
- })
- it('should send the cursor data to the connected user manager', async function (ctx) {
- await new Promise((resolve, reject) => {
- ctx.ConnectedUsersManager.updateUserPosition
- .calledWith(
- ctx.project_id,
- ctx.client.publicId,
- {
- _id: ctx.user_id,
- email: ctx.email,
- first_name: ctx.first_name,
- last_name: undefined,
- },
- {
- row: ctx.row,
- column: ctx.column,
- doc_id: ctx.doc_id,
- }
- )
- .should.equal(true)
- resolve()
- })
- })
- it('should increment the update-client-position metric at 0.1 frequency', function (ctx) {
- ctx.metrics.inc
- .calledWith('editor.update-client-position', 0.1)
- .should.equal(true)
- })
- })
- describe('with a logged in user who has no first_name set', function () {
- beforeEach(async function (ctx) {
- await new Promise((resolve, reject) => {
- ctx.client.ol_context = {
- project_id: ctx.project_id,
- first_name: undefined,
- last_name: (ctx.last_name = 'Adams'),
- email: (ctx.email = 'joe@example.com'),
- user_id: (ctx.user_id = 'user-id-123'),
- }
- ctx.populatedCursorData = {
- doc_id: ctx.doc_id,
- id: ctx.client.publicId,
- name: `${ctx.last_name}`,
- row: ctx.row,
- column: ctx.column,
- email: ctx.email,
- user_id: ctx.user_id,
- }
- ctx.WebsocketController.updateClientPosition(
- ctx.client,
- ctx.update,
- err => {
- if (err) return reject(err)
- resolve()
- }
- )
- })
- })
- it("should send the update to the project room with the user's name", function (ctx) {
- ctx.WebsocketLoadBalancer.emitToRoom
- .calledWith(
- ctx.project_id,
- 'clientTracking.clientUpdated',
- ctx.populatedCursorData
- )
- .should.equal(true)
- })
- it('should send the cursor data to the connected user manager', async function (ctx) {
- await new Promise((resolve, reject) => {
- ctx.ConnectedUsersManager.updateUserPosition
- .calledWith(
- ctx.project_id,
- ctx.client.publicId,
- {
- _id: ctx.user_id,
- email: ctx.email,
- first_name: undefined,
- last_name: ctx.last_name,
- },
- {
- row: ctx.row,
- column: ctx.column,
- doc_id: ctx.doc_id,
- }
- )
- .should.equal(true)
- resolve()
- })
- })
- it('should increment the update-client-position metric at 0.1 frequency', function (ctx) {
- ctx.metrics.inc
- .calledWith('editor.update-client-position', 0.1)
- .should.equal(true)
- })
- })
- describe('with a logged in user who has no names set', function () {
- beforeEach(async function (ctx) {
- await new Promise((resolve, reject) => {
- ctx.client.ol_context = {
- project_id: ctx.project_id,
- first_name: undefined,
- last_name: undefined,
- email: (ctx.email = 'joe@example.com'),
- user_id: (ctx.user_id = 'user-id-123'),
- }
- ctx.WebsocketController.updateClientPosition(
- ctx.client,
- ctx.update,
- err => {
- if (err) return reject(err)
- resolve()
- }
- )
- })
- })
- it('should send the update to the project name with no name', function (ctx) {
- ctx.WebsocketLoadBalancer.emitToRoom
- .calledWith(ctx.project_id, 'clientTracking.clientUpdated', {
- doc_id: ctx.doc_id,
- id: ctx.client.publicId,
- user_id: ctx.user_id,
- name: '',
- row: ctx.row,
- column: ctx.column,
- email: ctx.email,
- })
- .should.equal(true)
- })
- })
- describe('with an anonymous user', function () {
- beforeEach(async function (ctx) {
- await new Promise((resolve, reject) => {
- ctx.client.ol_context = {
- project_id: ctx.project_id,
- }
- ctx.WebsocketController.updateClientPosition(
- ctx.client,
- ctx.update,
- err => {
- if (err) return reject(err)
- resolve()
- }
- )
- })
- })
- it('should send the update to the project room with no name', function (ctx) {
- ctx.WebsocketLoadBalancer.emitToRoom
- .calledWith(ctx.project_id, 'clientTracking.clientUpdated', {
- doc_id: ctx.doc_id,
- id: ctx.client.publicId,
- name: '',
- row: ctx.row,
- column: ctx.column,
- })
- .should.equal(true)
- })
- it('should not send cursor data to the connected user manager', async function (ctx) {
- await new Promise((resolve, reject) => {
- ctx.ConnectedUsersManager.updateUserPosition.called.should.equal(
- false
- )
- resolve()
- })
- })
- })
- describe('when the client has disconnected', function () {
- beforeEach(async function (ctx) {
- await new Promise((resolve, reject) => {
- ctx.client.disconnected = true
- ctx.AuthorizationManager.assertClientCanViewProjectAndDoc =
- sinon.stub()
- ctx.WebsocketController.updateClientPosition(
- ctx.client,
- ctx.update,
- (...args) => {
- ctx.callback(...args)
- if (args[0]) return reject(args[0])
- resolve()
- }
- )
- })
- })
- it('should call the callback with no details', function (ctx) {
- expect(ctx.callback.args[0]).to.deep.equal([])
- })
- it('should not check permissions', function (ctx) {
- expect(
- ctx.AuthorizationManager.assertClientCanViewProjectAndDoc.called
- ).to.equal(false)
- })
- })
- })
- describe('applyOtUpdate', function () {
- beforeEach(function (ctx) {
- ctx.update = { op: { p: 12, t: 'foo' } }
- ctx.client.ol_context.user_id = ctx.user_id
- ctx.client.ol_context.project_id = ctx.project_id
- ctx.WebsocketController._assertClientCanApplyUpdate = sinon
- .stub()
- .yields()
- ctx.DocumentUpdaterManager.queueChange = sinon.stub().callsArg(3)
- })
- describe('succesfully', function () {
- beforeEach(function (ctx) {
- ctx.WebsocketController.applyOtUpdate(
- ctx.client,
- ctx.doc_id,
- ctx.update,
- ctx.callback
- )
- })
- it('should set the source of the update to the client id', function (ctx) {
- ctx.update.meta.source.should.equal(ctx.client.publicId)
- })
- it('should set the user_id of the update to the user id', function (ctx) {
- ctx.update.meta.user_id.should.equal(ctx.user_id)
- })
- it('should queue the update', function (ctx) {
- ctx.DocumentUpdaterManager.queueChange
- .calledWith(ctx.project_id, ctx.doc_id, ctx.update)
- .should.equal(true)
- })
- it('should call the callback', function (ctx) {
- ctx.callback.called.should.equal(true)
- })
- it('should increment the doc updates', function (ctx) {
- ctx.metrics.inc.calledWith('editor.doc-update').should.equal(true)
- })
- })
- describe('unsuccessfully', function () {
- beforeEach(function (ctx) {
- ctx.client.disconnect = sinon.stub()
- ctx.DocumentUpdaterManager.queueChange = sinon
- .stub()
- .callsArgWith(3, (ctx.error = new Error('Something went wrong')))
- ctx.WebsocketController.applyOtUpdate(
- ctx.client,
- ctx.doc_id,
- ctx.update,
- ctx.callback
- )
- })
- it('should disconnect the client', function (ctx) {
- ctx.client.disconnect.called.should.equal(true)
- })
- it('should not log an error', function (ctx) {
- ctx.logger.error.called.should.equal(false)
- })
- it('should call the callback with the error', function (ctx) {
- ctx.callback.calledWith(ctx.error).should.equal(true)
- })
- })
- describe('when not authorized', function () {
- beforeEach(function (ctx) {
- ctx.client.disconnect = sinon.stub()
- ctx.WebsocketController._assertClientCanApplyUpdate = sinon
- .stub()
- .yields((ctx.error = new Error('not authorized')))
- ctx.WebsocketController.applyOtUpdate(
- ctx.client,
- ctx.doc_id,
- ctx.update,
- ctx.callback
- )
- })
- // This happens in a setTimeout to allow the client a chance to receive the error first.
- // I'm not sure how to unit test, but it is acceptance tested.
- // it "should disconnect the client", ->
- // @client.disconnect.called.should.equal true
- it('should not log a warning', function (ctx) {
- ctx.logger.warn.called.should.equal(false)
- })
- it('should call the callback with the error', function (ctx) {
- ctx.callback.calledWith(ctx.error).should.equal(true)
- })
- })
- describe('update_too_large', function () {
- beforeEach(async function (ctx) {
- await new Promise((resolve, reject) => {
- ctx.client.disconnect = sinon.stub()
- ctx.client.emit = sinon.stub()
- ctx.client.ol_context.user_id = ctx.user_id
- ctx.client.ol_context.project_id = ctx.project_id
- const error = new UpdateTooLargeError(7372835)
- ctx.DocumentUpdaterManager.queueChange = sinon
- .stub()
- .callsArgWith(3, error)
- ctx.WebsocketController.applyOtUpdate(
- ctx.client,
- ctx.doc_id,
- ctx.update,
- ctx.callback
- )
- setTimeout(() => resolve(), 1)
- })
- })
- it('should call the callback with no error', function (ctx) {
- ctx.callback.called.should.equal(true)
- ctx.callback.args[0].should.deep.equal([])
- })
- it('should log a warning with the size and context', function (ctx) {
- ctx.logger.warn.called.should.equal(true)
- ctx.logger.warn.args[0].should.deep.equal([
- {
- userId: ctx.user_id,
- projectId: ctx.project_id,
- docId: ctx.doc_id,
- updateSize: 7372835,
- },
- 'update is too large',
- ])
- })
- describe('after 100ms', function () {
- beforeEach(async function () {
- await new Promise(resolve => {
- setTimeout(resolve, 100)
- })
- })
- it('should send an otUpdateError the client', function (ctx) {
- ctx.client.emit.calledWith('otUpdateError').should.equal(true)
- })
- it('should disconnect the client', function (ctx) {
- ctx.client.disconnect.called.should.equal(true)
- })
- })
- describe('when the client disconnects during the next 100ms', function () {
- beforeEach(async function (ctx) {
- await new Promise((resolve, reject) => {
- ctx.client.disconnected = true
- setTimeout(resolve, 100)
- })
- })
- it('should not send an otUpdateError the client', function (ctx) {
- ctx.client.emit.calledWith('otUpdateError').should.equal(false)
- })
- it('should not disconnect the client', function (ctx) {
- ctx.client.disconnect.called.should.equal(false)
- })
- it('should increment the editor.doc-update.disconnected metric with a status', function (ctx) {
- expect(
- ctx.metrics.inc.calledWith('editor.doc-update.disconnected', 1, {
- status: 'at-otUpdateError',
- })
- ).to.equal(true)
- })
- })
- })
- })
- describe('_assertClientCanApplyUpdate', function () {
- beforeEach(function (ctx) {
- ctx.edit_update = {
- op: [
- { i: 'foo', p: 42 },
- { c: 'bar', p: 132 },
- ],
- } // comments may still be in an edit op
- ctx.comment_update = { op: [{ c: 'bar', p: 132 }] }
- ctx.AuthorizationManager.assertClientCanEditProjectAndDoc = sinon.stub()
- ctx.AuthorizationManager.assertClientCanReviewProjectAndDoc = sinon.stub()
- ctx.AuthorizationManager.assertClientCanViewProjectAndDoc = sinon.stub()
- })
- describe('with a read-write client', function () {
- it('should return successfully', async function (ctx) {
- await new Promise((resolve, reject) => {
- ctx.AuthorizationManager.assertClientCanEditProjectAndDoc.yields(null)
- ctx.WebsocketController._assertClientCanApplyUpdate(
- ctx.client,
- ctx.doc_id,
- ctx.edit_update,
- error => {
- expect(error).to.be.null
- resolve()
- }
- )
- })
- })
- })
- describe('with a read-only client and an edit op', function () {
- it('should return an error', async function (ctx) {
- await new Promise((resolve, reject) => {
- ctx.AuthorizationManager.assertClientCanEditProjectAndDoc.yields(
- new Error('not authorized')
- )
- ctx.AuthorizationManager.assertClientCanViewProjectAndDoc.yields(null)
- ctx.WebsocketController._assertClientCanApplyUpdate(
- ctx.client,
- ctx.doc_id,
- ctx.edit_update,
- error => {
- expect(error.message).to.equal('not authorized')
- resolve()
- }
- )
- })
- })
- })
- describe('with a read-only client and a comment op', function () {
- it('should return successfully', async function (ctx) {
- await new Promise((resolve, reject) => {
- ctx.AuthorizationManager.assertClientCanEditProjectAndDoc.yields(
- new Error('not authorized')
- )
- ctx.AuthorizationManager.assertClientCanViewProjectAndDoc.yields(null)
- ctx.WebsocketController._assertClientCanApplyUpdate(
- ctx.client,
- ctx.doc_id,
- ctx.comment_update,
- error => {
- expect(error).to.be.null
- resolve()
- }
- )
- })
- })
- })
- describe('with a totally unauthorized client', function () {
- it('should return an error', async function (ctx) {
- await new Promise((resolve, reject) => {
- ctx.AuthorizationManager.assertClientCanEditProjectAndDoc.yields(
- new Error('not authorized')
- )
- ctx.AuthorizationManager.assertClientCanViewProjectAndDoc.yields(
- new Error('not authorized')
- )
- ctx.WebsocketController._assertClientCanApplyUpdate(
- ctx.client,
- ctx.doc_id,
- ctx.comment_update,
- error => {
- expect(error.message).to.equal('not authorized')
- resolve()
- }
- )
- })
- })
- })
- describe('with a review client', function () {
- it('op with tc should succeed', async function (ctx) {
- await new Promise((resolve, reject) => {
- ctx.AuthorizationManager.assertClientCanEditProjectAndDoc.yields(
- new Error('not authorized')
- )
- ctx.AuthorizationManager.assertClientCanViewProjectAndDoc.yields(null)
- ctx.AuthorizationManager.assertClientCanReviewProjectAndDoc.yields(
- null
- )
- ctx.WebsocketController._assertClientCanApplyUpdate(
- ctx.client,
- ctx.doc_id,
- { op: [{ p: 10, i: 'a' }], meta: { tc: '123456' } },
- error => {
- expect(error).to.be.null
- resolve()
- }
- )
- })
- })
- it('op without tc should fail', async function (ctx) {
- await new Promise((resolve, reject) => {
- ctx.AuthorizationManager.assertClientCanEditProjectAndDoc.yields(
- new Error('not authorized')
- )
- ctx.AuthorizationManager.assertClientCanViewProjectAndDoc.yields(null)
- ctx.AuthorizationManager.assertClientCanReviewProjectAndDoc.yields(
- null
- )
- ctx.WebsocketController._assertClientCanApplyUpdate(
- ctx.client,
- ctx.doc_id,
- { op: [{ p: 10, i: 'a' }] },
- error => {
- expect(error.message).to.equal('not authorized')
- resolve()
- }
- )
- })
- })
- })
- })
- })
|