WebsocketControllerTests.js 53 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656
  1. /* eslint-disable
  2. no-return-assign,
  3. no-throw-literal,
  4. no-unused-vars,
  5. */
  6. // TODO: This file was created by bulk-decaffeinate.
  7. // Fix any style issues and re-enable lint.
  8. /*
  9. * decaffeinate suggestions:
  10. * DS101: Remove unnecessary use of Array.from
  11. * DS102: Remove unnecessary code created because of implicit returns
  12. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  13. */
  14. const sinon = require('sinon')
  15. const { expect } = require('chai')
  16. const modulePath = '../../../app/js/WebsocketController.js'
  17. const SandboxedModule = require('sandboxed-module')
  18. const tk = require('timekeeper')
  19. const { UpdateTooLargeError } = require('../../../app/js/Errors')
  20. describe('WebsocketController', function () {
  21. beforeEach(function () {
  22. tk.freeze(new Date())
  23. this.project_id = 'project-id-123'
  24. this.user = {
  25. _id: (this.user_id = 'user-id-123'),
  26. first_name: 'James',
  27. last_name: 'Allen',
  28. email: 'james@example.com',
  29. signUpDate: new Date('2014-01-01'),
  30. loginCount: 42,
  31. }
  32. this.callback = sinon.stub()
  33. this.client = {
  34. disconnected: false,
  35. id: (this.client_id = 'mock-client-id-123'),
  36. publicId: `other-id-${Math.random()}`,
  37. ol_context: {},
  38. joinLeaveEpoch: 0,
  39. join: sinon.stub(),
  40. leave: sinon.stub(),
  41. }
  42. return (this.WebsocketController = SandboxedModule.require(modulePath, {
  43. requires: {
  44. './WebApiManager': (this.WebApiManager = {}),
  45. './AuthorizationManager': (this.AuthorizationManager = {}),
  46. './DocumentUpdaterManager': (this.DocumentUpdaterManager = {}),
  47. './ConnectedUsersManager': (this.ConnectedUsersManager = {}),
  48. './WebsocketLoadBalancer': (this.WebsocketLoadBalancer = {}),
  49. '@overleaf/metrics': (this.metrics = {
  50. inc: sinon.stub(),
  51. set: sinon.stub(),
  52. }),
  53. './RoomManager': (this.RoomManager = {}),
  54. },
  55. }))
  56. })
  57. afterEach(function () {
  58. return tk.reset()
  59. })
  60. describe('joinProject', function () {
  61. describe('when authorised', function () {
  62. beforeEach(function () {
  63. this.client.id = 'mock-client-id'
  64. this.project = {
  65. name: 'Test Project',
  66. owner: {
  67. _id: (this.owner_id = 'mock-owner-id-123'),
  68. },
  69. }
  70. this.privilegeLevel = 'owner'
  71. this.ConnectedUsersManager.updateUserPosition = sinon
  72. .stub()
  73. .callsArgAsync(4)
  74. this.isRestrictedUser = true
  75. this.isTokenMember = true
  76. this.isInvitedMember = true
  77. this.WebApiManager.joinProject = sinon
  78. .stub()
  79. .callsArgWith(2, null, this.project, this.privilegeLevel, {
  80. isRestrictedUser: this.isRestrictedUser,
  81. isTokenMember: this.isTokenMember,
  82. isInvitedMember: this.isInvitedMember,
  83. })
  84. this.RoomManager.joinProject = sinon.stub().callsArg(2)
  85. return this.WebsocketController.joinProject(
  86. this.client,
  87. this.user,
  88. this.project_id,
  89. this.callback
  90. )
  91. })
  92. it('should load the project from web', function () {
  93. return this.WebApiManager.joinProject
  94. .calledWith(this.project_id, this.user)
  95. .should.equal(true)
  96. })
  97. it('should join the project room', function () {
  98. return this.RoomManager.joinProject
  99. .calledWith(this.client, this.project_id)
  100. .should.equal(true)
  101. })
  102. it('should set the privilege level on the client', function () {
  103. return this.client.ol_context.privilege_level.should.equal(
  104. this.privilegeLevel
  105. )
  106. })
  107. it("should set the user's id on the client", function () {
  108. return this.client.ol_context.user_id.should.equal(this.user._id)
  109. })
  110. it("should set the user's email on the client", function () {
  111. return this.client.ol_context.email.should.equal(this.user.email)
  112. })
  113. it("should set the user's first_name on the client", function () {
  114. return this.client.ol_context.first_name.should.equal(
  115. this.user.first_name
  116. )
  117. })
  118. it("should set the user's last_name on the client", function () {
  119. return this.client.ol_context.last_name.should.equal(
  120. this.user.last_name
  121. )
  122. })
  123. it("should set the user's sign up date on the client", function () {
  124. return this.client.ol_context.signup_date.should.equal(
  125. this.user.signUpDate
  126. )
  127. })
  128. it("should set the user's login_count on the client", function () {
  129. return this.client.ol_context.login_count.should.equal(
  130. this.user.loginCount
  131. )
  132. })
  133. it('should set the connected time on the client', function () {
  134. return this.client.ol_context.connected_time.should.equal(new Date())
  135. })
  136. it('should set the project_id on the client', function () {
  137. return this.client.ol_context.project_id.should.equal(this.project_id)
  138. })
  139. it('should set the project owner id on the client', function () {
  140. return this.client.ol_context.owner_id.should.equal(this.owner_id)
  141. })
  142. it('should set the is_restricted_user flag on the client', function () {
  143. return this.client.ol_context.is_restricted_user.should.equal(
  144. this.isRestrictedUser
  145. )
  146. })
  147. it('should set the is_token_member flag on the client', function () {
  148. this.client.ol_context.is_token_member.should.equal(this.isTokenMember)
  149. })
  150. it('should set the is_invited_member flag on the client', function () {
  151. this.client.ol_context.is_invited_member.should.equal(
  152. this.isInvitedMember
  153. )
  154. })
  155. it('should call the callback with the project, privilegeLevel and protocolVersion', function () {
  156. return this.callback
  157. .calledWith(
  158. null,
  159. this.project,
  160. this.privilegeLevel,
  161. this.WebsocketController.PROTOCOL_VERSION
  162. )
  163. .should.equal(true)
  164. })
  165. it('should mark the user as connected in ConnectedUsersManager', function () {
  166. return this.ConnectedUsersManager.updateUserPosition
  167. .calledWith(this.project_id, this.client.publicId, this.user, null)
  168. .should.equal(true)
  169. })
  170. return it('should increment the join-project metric', function () {
  171. return this.metrics.inc
  172. .calledWith('editor.join-project')
  173. .should.equal(true)
  174. })
  175. })
  176. describe('when not authorized', function () {
  177. beforeEach(function () {
  178. this.WebApiManager.joinProject = sinon
  179. .stub()
  180. .callsArgWith(2, null, null, null)
  181. return this.WebsocketController.joinProject(
  182. this.client,
  183. this.user,
  184. this.project_id,
  185. this.callback
  186. )
  187. })
  188. it('should return an error', function () {
  189. return this.callback
  190. .calledWith(sinon.match({ message: 'not authorized' }))
  191. .should.equal(true)
  192. })
  193. return it('should not log an error', function () {
  194. return this.logger.error.called.should.equal(false)
  195. })
  196. })
  197. describe('when the subscribe failed', function () {
  198. beforeEach(function () {
  199. this.client.id = 'mock-client-id'
  200. this.project = {
  201. name: 'Test Project',
  202. owner: {
  203. _id: (this.owner_id = 'mock-owner-id-123'),
  204. },
  205. }
  206. this.privilegeLevel = 'owner'
  207. this.ConnectedUsersManager.updateUserPosition = sinon
  208. .stub()
  209. .callsArgAsync(4)
  210. this.isRestrictedUser = true
  211. this.isTokenMember = true
  212. this.isInvitedMember = true
  213. this.WebApiManager.joinProject = sinon
  214. .stub()
  215. .callsArgWith(2, null, this.project, this.privilegeLevel, {
  216. isRestrictedUser: this.isRestrictedUser,
  217. isTokenMember: this.isTokenMember,
  218. isInvitedMember: this.isInvitedMember,
  219. })
  220. this.RoomManager.joinProject = sinon
  221. .stub()
  222. .callsArgWith(2, new Error('subscribe failed'))
  223. return this.WebsocketController.joinProject(
  224. this.client,
  225. this.user,
  226. this.project_id,
  227. this.callback
  228. )
  229. })
  230. return it('should return an error', function () {
  231. this.callback
  232. .calledWith(sinon.match({ message: 'subscribe failed' }))
  233. .should.equal(true)
  234. return this.callback.args[0][0].message.should.equal('subscribe failed')
  235. })
  236. })
  237. describe('when the client has disconnected', function () {
  238. beforeEach(function () {
  239. this.client.disconnected = true
  240. this.WebApiManager.joinProject = sinon.stub().callsArg(2)
  241. return this.WebsocketController.joinProject(
  242. this.client,
  243. this.user,
  244. this.project_id,
  245. this.callback
  246. )
  247. })
  248. it('should not call WebApiManager.joinProject', function () {
  249. return expect(this.WebApiManager.joinProject.called).to.equal(false)
  250. })
  251. it('should call the callback with no details', function () {
  252. return expect(this.callback.args[0]).to.deep.equal([])
  253. })
  254. return it('should increment the editor.join-project.disconnected metric with a status', function () {
  255. return expect(
  256. this.metrics.inc.calledWith('editor.join-project.disconnected', 1, {
  257. status: 'immediately',
  258. })
  259. ).to.equal(true)
  260. })
  261. })
  262. return describe('when the client disconnects while WebApiManager.joinProject is running', function () {
  263. beforeEach(function () {
  264. this.WebApiManager.joinProject = (project, user, cb) => {
  265. this.client.disconnected = true
  266. return cb(null, this.project, this.privilegeLevel, {
  267. isRestrictedUser: this.isRestrictedUser,
  268. isTokenMember: this.isTokenMember,
  269. isInvitedMember: this.isInvitedMember,
  270. })
  271. }
  272. return this.WebsocketController.joinProject(
  273. this.client,
  274. this.user,
  275. this.project_id,
  276. this.callback
  277. )
  278. })
  279. it('should call the callback with no details', function () {
  280. return expect(this.callback.args[0]).to.deep.equal([])
  281. })
  282. return it('should increment the editor.join-project.disconnected metric with a status', function () {
  283. return expect(
  284. this.metrics.inc.calledWith('editor.join-project.disconnected', 1, {
  285. status: 'after-web-api-call',
  286. })
  287. ).to.equal(true)
  288. })
  289. })
  290. })
  291. describe('leaveProject', function () {
  292. beforeEach(function () {
  293. this.DocumentUpdaterManager.flushProjectToMongoAndDelete = sinon
  294. .stub()
  295. .callsArg(1)
  296. this.ConnectedUsersManager.markUserAsDisconnected = sinon
  297. .stub()
  298. .callsArg(2)
  299. this.WebsocketLoadBalancer.emitToRoom = sinon.stub()
  300. this.RoomManager.leaveProjectAndDocs = sinon.stub()
  301. this.clientsInRoom = []
  302. this.io = {
  303. sockets: {
  304. clients: roomId => {
  305. if (roomId !== this.project_id) {
  306. throw 'expected room_id to be project_id'
  307. }
  308. return this.clientsInRoom
  309. },
  310. },
  311. }
  312. this.client.ol_context.project_id = this.project_id
  313. this.client.ol_context.user_id = this.user_id
  314. this.WebsocketController.FLUSH_IF_EMPTY_DELAY = 0
  315. return tk.reset()
  316. }) // Allow setTimeout to work.
  317. describe('when the client did not joined a project yet', function () {
  318. beforeEach(function (done) {
  319. this.client.ol_context = {}
  320. return this.WebsocketController.leaveProject(this.io, this.client, done)
  321. })
  322. it('should bail out when calling leaveProject', function () {
  323. this.WebsocketLoadBalancer.emitToRoom.called.should.equal(false)
  324. this.RoomManager.leaveProjectAndDocs.called.should.equal(false)
  325. return this.ConnectedUsersManager.markUserAsDisconnected.called.should.equal(
  326. false
  327. )
  328. })
  329. return it('should not inc any metric', function () {
  330. return this.metrics.inc.called.should.equal(false)
  331. })
  332. })
  333. describe('when the project is empty', function () {
  334. beforeEach(function (done) {
  335. this.clientsInRoom = []
  336. return this.WebsocketController.leaveProject(this.io, this.client, done)
  337. })
  338. it('should end clientTracking.clientDisconnected to the project room', function () {
  339. return this.WebsocketLoadBalancer.emitToRoom
  340. .calledWith(
  341. this.project_id,
  342. 'clientTracking.clientDisconnected',
  343. this.client.publicId
  344. )
  345. .should.equal(true)
  346. })
  347. it('should mark the user as disconnected', function () {
  348. return this.ConnectedUsersManager.markUserAsDisconnected
  349. .calledWith(this.project_id, this.client.publicId)
  350. .should.equal(true)
  351. })
  352. it('should flush the project in the document updater', function () {
  353. return this.DocumentUpdaterManager.flushProjectToMongoAndDelete
  354. .calledWith(this.project_id)
  355. .should.equal(true)
  356. })
  357. it('should increment the leave-project metric', function () {
  358. return this.metrics.inc
  359. .calledWith('editor.leave-project')
  360. .should.equal(true)
  361. })
  362. return it('should track the disconnection in RoomManager', function () {
  363. return this.RoomManager.leaveProjectAndDocs
  364. .calledWith(this.client)
  365. .should.equal(true)
  366. })
  367. })
  368. describe('when the project is not empty', function () {
  369. beforeEach(function (done) {
  370. this.clientsInRoom = ['mock-remaining-client']
  371. this.io = {
  372. sockets: {
  373. clients: roomId => {
  374. if (roomId !== this.project_id) {
  375. throw 'expected room_id to be project_id'
  376. }
  377. return this.clientsInRoom
  378. },
  379. },
  380. }
  381. return this.WebsocketController.leaveProject(this.io, this.client, done)
  382. })
  383. return it('should not flush the project in the document updater', function () {
  384. return this.DocumentUpdaterManager.flushProjectToMongoAndDelete.called.should.equal(
  385. false
  386. )
  387. })
  388. })
  389. describe('when client has not authenticated', function () {
  390. beforeEach(function (done) {
  391. this.client.ol_context.user_id = null
  392. this.client.ol_context.project_id = null
  393. return this.WebsocketController.leaveProject(this.io, this.client, done)
  394. })
  395. it('should not end clientTracking.clientDisconnected to the project room', function () {
  396. return this.WebsocketLoadBalancer.emitToRoom
  397. .calledWith(
  398. this.project_id,
  399. 'clientTracking.clientDisconnected',
  400. this.client.publicId
  401. )
  402. .should.equal(false)
  403. })
  404. it('should not mark the user as disconnected', function () {
  405. return this.ConnectedUsersManager.markUserAsDisconnected
  406. .calledWith(this.project_id, this.client.publicId)
  407. .should.equal(false)
  408. })
  409. it('should not flush the project in the document updater', function () {
  410. return this.DocumentUpdaterManager.flushProjectToMongoAndDelete
  411. .calledWith(this.project_id)
  412. .should.equal(false)
  413. })
  414. return it('should not increment the leave-project metric', function () {
  415. return this.metrics.inc
  416. .calledWith('editor.leave-project')
  417. .should.equal(false)
  418. })
  419. })
  420. return describe('when client has not joined a project', function () {
  421. beforeEach(function (done) {
  422. this.client.ol_context.user_id = this.user_id
  423. this.client.ol_context.project_id = null
  424. return this.WebsocketController.leaveProject(this.io, this.client, done)
  425. })
  426. it('should not end clientTracking.clientDisconnected to the project room', function () {
  427. return this.WebsocketLoadBalancer.emitToRoom
  428. .calledWith(
  429. this.project_id,
  430. 'clientTracking.clientDisconnected',
  431. this.client.publicId
  432. )
  433. .should.equal(false)
  434. })
  435. it('should not mark the user as disconnected', function () {
  436. return this.ConnectedUsersManager.markUserAsDisconnected
  437. .calledWith(this.project_id, this.client.publicId)
  438. .should.equal(false)
  439. })
  440. it('should not flush the project in the document updater', function () {
  441. return this.DocumentUpdaterManager.flushProjectToMongoAndDelete
  442. .calledWith(this.project_id)
  443. .should.equal(false)
  444. })
  445. return it('should not increment the leave-project metric', function () {
  446. return this.metrics.inc
  447. .calledWith('editor.leave-project')
  448. .should.equal(false)
  449. })
  450. })
  451. })
  452. describe('joinDoc', function () {
  453. beforeEach(function () {
  454. this.doc_id = 'doc-id-123'
  455. this.doc_lines = ['doc', 'lines']
  456. this.version = 42
  457. this.ops = ['mock', 'ops']
  458. this.ranges = { mock: 'ranges' }
  459. this.options = {}
  460. this.client.ol_context.project_id = this.project_id
  461. this.client.ol_context.is_restricted_user = false
  462. this.AuthorizationManager.addAccessToDoc = sinon.stub().yields()
  463. this.AuthorizationManager.assertClientCanViewProject = sinon
  464. .stub()
  465. .callsArgWith(1, null)
  466. this.AuthorizationManager.assertClientCanViewProjectAndDoc = sinon
  467. .stub()
  468. .callsArgWith(2, null)
  469. this.DocumentUpdaterManager.getDocument = sinon
  470. .stub()
  471. .callsArgWith(
  472. 3,
  473. null,
  474. this.doc_lines,
  475. this.version,
  476. this.ranges,
  477. this.ops
  478. )
  479. return (this.RoomManager.joinDoc = sinon.stub().callsArg(2))
  480. })
  481. describe('works', function () {
  482. beforeEach(function () {
  483. return this.WebsocketController.joinDoc(
  484. this.client,
  485. this.doc_id,
  486. -1,
  487. this.options,
  488. this.callback
  489. )
  490. })
  491. it('should inc the joinLeaveEpoch', function () {
  492. expect(this.client.joinLeaveEpoch).to.equal(1)
  493. })
  494. it('should check that the client is authorized to view the project', function () {
  495. return this.AuthorizationManager.assertClientCanViewProject
  496. .calledWith(this.client)
  497. .should.equal(true)
  498. })
  499. it('should get the document from the DocumentUpdaterManager with fromVersion', function () {
  500. return this.DocumentUpdaterManager.getDocument
  501. .calledWith(this.project_id, this.doc_id, -1)
  502. .should.equal(true)
  503. })
  504. it('should add permissions for the client to access the doc', function () {
  505. return this.AuthorizationManager.addAccessToDoc
  506. .calledWith(this.client, this.doc_id)
  507. .should.equal(true)
  508. })
  509. it('should join the client to room for the doc_id', function () {
  510. return this.RoomManager.joinDoc
  511. .calledWith(this.client, this.doc_id)
  512. .should.equal(true)
  513. })
  514. it('should call the callback with the lines, version, ranges and ops', function () {
  515. return this.callback
  516. .calledWith(null, this.doc_lines, this.version, this.ops, this.ranges)
  517. .should.equal(true)
  518. })
  519. return it('should increment the join-doc metric', function () {
  520. return this.metrics.inc.calledWith('editor.join-doc').should.equal(true)
  521. })
  522. })
  523. describe('with a fromVersion', function () {
  524. beforeEach(function () {
  525. this.fromVersion = 40
  526. return this.WebsocketController.joinDoc(
  527. this.client,
  528. this.doc_id,
  529. this.fromVersion,
  530. this.options,
  531. this.callback
  532. )
  533. })
  534. return it('should get the document from the DocumentUpdaterManager with fromVersion', function () {
  535. return this.DocumentUpdaterManager.getDocument
  536. .calledWith(this.project_id, this.doc_id, this.fromVersion)
  537. .should.equal(true)
  538. })
  539. })
  540. describe('with doclines that need escaping', function () {
  541. beforeEach(function () {
  542. this.doc_lines.push(['räksmörgås'])
  543. return this.WebsocketController.joinDoc(
  544. this.client,
  545. this.doc_id,
  546. -1,
  547. this.options,
  548. this.callback
  549. )
  550. })
  551. return it('should call the callback with the escaped lines', function () {
  552. const escapedLines = this.callback.args[0][1]
  553. const escapedWord = escapedLines.pop()
  554. escapedWord.should.equal('räksmörgås')
  555. // Check that unescaping works
  556. return decodeURIComponent(escape(escapedWord)).should.equal(
  557. 'räksmörgås'
  558. )
  559. })
  560. })
  561. describe('with comments that need encoding', function () {
  562. beforeEach(function () {
  563. this.ranges.comments = [{ op: { c: 'räksmörgås' } }]
  564. return this.WebsocketController.joinDoc(
  565. this.client,
  566. this.doc_id,
  567. -1,
  568. { encodeRanges: true },
  569. this.callback
  570. )
  571. })
  572. return it('should call the callback with the encoded comment', function () {
  573. const encodedComments = this.callback.args[0][4]
  574. const encodedComment = encodedComments.comments.pop()
  575. const encodedCommentText = encodedComment.op.c
  576. return encodedCommentText.should.equal('räksmörgås')
  577. })
  578. })
  579. describe('with changes that need encoding', function () {
  580. it('should call the callback with the encoded insert change', function () {
  581. this.ranges.changes = [{ op: { i: 'räksmörgås' } }]
  582. this.WebsocketController.joinDoc(
  583. this.client,
  584. this.doc_id,
  585. -1,
  586. { encodeRanges: true },
  587. this.callback
  588. )
  589. const encodedChanges = this.callback.args[0][4]
  590. const encodedChange = encodedChanges.changes.pop()
  591. const encodedChangeText = encodedChange.op.i
  592. return encodedChangeText.should.equal('räksmörgås')
  593. })
  594. return it('should call the callback with the encoded delete change', function () {
  595. this.ranges.changes = [{ op: { d: 'räksmörgås' } }]
  596. this.WebsocketController.joinDoc(
  597. this.client,
  598. this.doc_id,
  599. -1,
  600. { encodeRanges: true },
  601. this.callback
  602. )
  603. const encodedChanges = this.callback.args[0][4]
  604. const encodedChange = encodedChanges.changes.pop()
  605. const encodedChangeText = encodedChange.op.d
  606. return encodedChangeText.should.equal('räksmörgås')
  607. })
  608. })
  609. describe('when not authorized', function () {
  610. beforeEach(function () {
  611. this.AuthorizationManager.assertClientCanViewProject = sinon
  612. .stub()
  613. .callsArgWith(1, (this.err = new Error('not authorized')))
  614. return this.WebsocketController.joinDoc(
  615. this.client,
  616. this.doc_id,
  617. -1,
  618. this.options,
  619. this.callback
  620. )
  621. })
  622. it('should call the callback with an error', function () {
  623. return this.callback
  624. .calledWith(sinon.match({ message: 'not authorized' }))
  625. .should.equal(true)
  626. })
  627. return it('should not call the DocumentUpdaterManager', function () {
  628. return this.DocumentUpdaterManager.getDocument.called.should.equal(
  629. false
  630. )
  631. })
  632. })
  633. describe('with a restricted client', function () {
  634. beforeEach(function () {
  635. this.ranges.comments = [{ op: { a: 1 } }, { op: { a: 2 } }]
  636. this.client.ol_context.is_restricted_user = true
  637. return this.WebsocketController.joinDoc(
  638. this.client,
  639. this.doc_id,
  640. -1,
  641. this.options,
  642. this.callback
  643. )
  644. })
  645. return it('should overwrite ranges.comments with an empty list', function () {
  646. const ranges = this.callback.args[0][4]
  647. return expect(ranges.comments).to.deep.equal([])
  648. })
  649. })
  650. describe('when the client has disconnected', function () {
  651. beforeEach(function () {
  652. this.client.disconnected = true
  653. return this.WebsocketController.joinDoc(
  654. this.client,
  655. this.doc_id,
  656. -1,
  657. this.options,
  658. this.callback
  659. )
  660. })
  661. it('should call the callback with no details', function () {
  662. return expect(this.callback.args[0]).to.deep.equal([])
  663. })
  664. it('should increment the editor.join-doc.disconnected metric with a status', function () {
  665. return expect(
  666. this.metrics.inc.calledWith('editor.join-doc.disconnected', 1, {
  667. status: 'immediately',
  668. })
  669. ).to.equal(true)
  670. })
  671. return it('should not get the document', function () {
  672. return expect(this.DocumentUpdaterManager.getDocument.called).to.equal(
  673. false
  674. )
  675. })
  676. })
  677. describe('when the client disconnects while auth checks are running', function () {
  678. beforeEach(function (done) {
  679. this.AuthorizationManager.assertClientCanViewProjectAndDoc.yields(
  680. new Error()
  681. )
  682. this.DocumentUpdaterManager.checkDocument = (projectId, docId, cb) => {
  683. this.client.disconnected = true
  684. cb()
  685. }
  686. this.WebsocketController.joinDoc(
  687. this.client,
  688. this.doc_id,
  689. -1,
  690. this.options,
  691. (...args) => {
  692. this.callback(...args)
  693. done()
  694. }
  695. )
  696. })
  697. it('should call the callback with no details', function () {
  698. expect(this.callback.called).to.equal(true)
  699. expect(this.callback.args[0]).to.deep.equal([])
  700. })
  701. it('should increment the editor.join-doc.disconnected metric with a status', function () {
  702. expect(
  703. this.metrics.inc.calledWith('editor.join-doc.disconnected', 1, {
  704. status: 'after-client-auth-check',
  705. })
  706. ).to.equal(true)
  707. })
  708. it('should not get the document', function () {
  709. expect(this.DocumentUpdaterManager.getDocument.called).to.equal(false)
  710. })
  711. })
  712. describe('when the client starts a parallel joinDoc request', function () {
  713. beforeEach(function (done) {
  714. this.AuthorizationManager.assertClientCanViewProjectAndDoc.yields(
  715. new Error()
  716. )
  717. this.DocumentUpdaterManager.checkDocument = (projectId, docId, cb) => {
  718. this.DocumentUpdaterManager.checkDocument = sinon.stub().yields()
  719. this.WebsocketController.joinDoc(
  720. this.client,
  721. this.doc_id,
  722. -1,
  723. {},
  724. () => {}
  725. )
  726. cb()
  727. }
  728. this.WebsocketController.joinDoc(
  729. this.client,
  730. this.doc_id,
  731. -1,
  732. this.options,
  733. (...args) => {
  734. this.callback(...args)
  735. // make sure the other joinDoc request completed
  736. setTimeout(done, 5)
  737. }
  738. )
  739. })
  740. it('should call the callback with an error', function () {
  741. expect(this.callback.called).to.equal(true)
  742. expect(this.callback.args[0][0].message).to.equal(
  743. 'joinLeaveEpoch mismatch'
  744. )
  745. })
  746. it('should get the document once (the parallel request wins)', function () {
  747. expect(this.DocumentUpdaterManager.getDocument.callCount).to.equal(1)
  748. })
  749. })
  750. describe('when the client starts a parallel leaveDoc request', function () {
  751. beforeEach(function (done) {
  752. this.RoomManager.leaveDoc = sinon.stub()
  753. this.AuthorizationManager.assertClientCanViewProjectAndDoc.yields(
  754. new Error()
  755. )
  756. this.DocumentUpdaterManager.checkDocument = (projectId, docId, cb) => {
  757. this.WebsocketController.leaveDoc(this.client, this.doc_id, () => {})
  758. cb()
  759. }
  760. this.WebsocketController.joinDoc(
  761. this.client,
  762. this.doc_id,
  763. -1,
  764. this.options,
  765. (...args) => {
  766. this.callback(...args)
  767. done()
  768. }
  769. )
  770. })
  771. it('should call the callback with an error', function () {
  772. expect(this.callback.called).to.equal(true)
  773. expect(this.callback.args[0][0].message).to.equal(
  774. 'joinLeaveEpoch mismatch'
  775. )
  776. })
  777. it('should not get the document', function () {
  778. expect(this.DocumentUpdaterManager.getDocument.called).to.equal(false)
  779. })
  780. })
  781. describe('when the client disconnects while RoomManager.joinDoc is running', function () {
  782. beforeEach(function () {
  783. this.RoomManager.joinDoc = (client, docId, cb) => {
  784. this.client.disconnected = true
  785. return cb()
  786. }
  787. return this.WebsocketController.joinDoc(
  788. this.client,
  789. this.doc_id,
  790. -1,
  791. this.options,
  792. this.callback
  793. )
  794. })
  795. it('should call the callback with no details', function () {
  796. return expect(this.callback.args[0]).to.deep.equal([])
  797. })
  798. it('should increment the editor.join-doc.disconnected metric with a status', function () {
  799. return expect(
  800. this.metrics.inc.calledWith('editor.join-doc.disconnected', 1, {
  801. status: 'after-joining-room',
  802. })
  803. ).to.equal(true)
  804. })
  805. return it('should not get the document', function () {
  806. return expect(this.DocumentUpdaterManager.getDocument.called).to.equal(
  807. false
  808. )
  809. })
  810. })
  811. return describe('when the client disconnects while DocumentUpdaterManager.getDocument is running', function () {
  812. beforeEach(function () {
  813. this.DocumentUpdaterManager.getDocument = (
  814. projectId,
  815. docId,
  816. fromVersion,
  817. callback
  818. ) => {
  819. this.client.disconnected = true
  820. return callback(
  821. null,
  822. this.doc_lines,
  823. this.version,
  824. this.ranges,
  825. this.ops
  826. )
  827. }
  828. return this.WebsocketController.joinDoc(
  829. this.client,
  830. this.doc_id,
  831. -1,
  832. this.options,
  833. this.callback
  834. )
  835. })
  836. it('should call the callback with no details', function () {
  837. return expect(this.callback.args[0]).to.deep.equal([])
  838. })
  839. return it('should increment the editor.join-doc.disconnected metric with a status', function () {
  840. return expect(
  841. this.metrics.inc.calledWith('editor.join-doc.disconnected', 1, {
  842. status: 'after-doc-updater-call',
  843. })
  844. ).to.equal(true)
  845. })
  846. })
  847. })
  848. describe('leaveDoc', function () {
  849. beforeEach(function () {
  850. this.doc_id = 'doc-id-123'
  851. this.client.ol_context.project_id = this.project_id
  852. this.RoomManager.leaveDoc = sinon.stub()
  853. return this.WebsocketController.leaveDoc(
  854. this.client,
  855. this.doc_id,
  856. this.callback
  857. )
  858. })
  859. it('should inc the joinLeaveEpoch', function () {
  860. expect(this.client.joinLeaveEpoch).to.equal(1)
  861. })
  862. it('should remove the client from the doc_id room', function () {
  863. return this.RoomManager.leaveDoc
  864. .calledWith(this.client, this.doc_id)
  865. .should.equal(true)
  866. })
  867. it('should call the callback', function () {
  868. return this.callback.called.should.equal(true)
  869. })
  870. return it('should increment the leave-doc metric', function () {
  871. return this.metrics.inc.calledWith('editor.leave-doc').should.equal(true)
  872. })
  873. })
  874. describe('getConnectedUsers', function () {
  875. beforeEach(function () {
  876. this.client.ol_context.project_id = this.project_id
  877. this.users = ['mock', 'users']
  878. this.WebsocketLoadBalancer.emitToRoom = sinon.stub()
  879. return (this.ConnectedUsersManager.getConnectedUsers = sinon
  880. .stub()
  881. .callsArgWith(1, null, this.users))
  882. })
  883. describe('when authorized', function () {
  884. beforeEach(function (done) {
  885. this.AuthorizationManager.assertClientCanViewProject = sinon
  886. .stub()
  887. .callsArgWith(1, null)
  888. return this.WebsocketController.getConnectedUsers(
  889. this.client,
  890. (...args) => {
  891. this.callback(...Array.from(args || []))
  892. return done()
  893. }
  894. )
  895. })
  896. it('should check that the client is authorized to view the project', function () {
  897. return this.AuthorizationManager.assertClientCanViewProject
  898. .calledWith(this.client)
  899. .should.equal(true)
  900. })
  901. it('should broadcast a request to update the client list', function () {
  902. return this.WebsocketLoadBalancer.emitToRoom
  903. .calledWith(this.project_id, 'clientTracking.refresh')
  904. .should.equal(true)
  905. })
  906. it('should get the connected users for the project', function () {
  907. return this.ConnectedUsersManager.getConnectedUsers
  908. .calledWith(this.project_id)
  909. .should.equal(true)
  910. })
  911. it('should return the users', function () {
  912. return this.callback.calledWith(null, this.users).should.equal(true)
  913. })
  914. return it('should increment the get-connected-users metric', function () {
  915. return this.metrics.inc
  916. .calledWith('editor.get-connected-users')
  917. .should.equal(true)
  918. })
  919. })
  920. describe('when not authorized', function () {
  921. beforeEach(function () {
  922. this.AuthorizationManager.assertClientCanViewProject = sinon
  923. .stub()
  924. .callsArgWith(1, (this.err = new Error('not authorized')))
  925. return this.WebsocketController.getConnectedUsers(
  926. this.client,
  927. this.callback
  928. )
  929. })
  930. it('should not get the connected users for the project', function () {
  931. return this.ConnectedUsersManager.getConnectedUsers.called.should.equal(
  932. false
  933. )
  934. })
  935. return it('should return an error', function () {
  936. return this.callback.calledWith(this.err).should.equal(true)
  937. })
  938. })
  939. describe('when restricted user', function () {
  940. beforeEach(function () {
  941. this.client.ol_context.is_restricted_user = true
  942. this.AuthorizationManager.assertClientCanViewProject = sinon
  943. .stub()
  944. .callsArgWith(1, null)
  945. return this.WebsocketController.getConnectedUsers(
  946. this.client,
  947. this.callback
  948. )
  949. })
  950. it('should return an empty array of users', function () {
  951. return this.callback.calledWith(null, []).should.equal(true)
  952. })
  953. return it('should not get the connected users for the project', function () {
  954. return this.ConnectedUsersManager.getConnectedUsers.called.should.equal(
  955. false
  956. )
  957. })
  958. })
  959. return describe('when the client has disconnected', function () {
  960. beforeEach(function () {
  961. this.client.disconnected = true
  962. this.AuthorizationManager.assertClientCanViewProject = sinon.stub()
  963. return this.WebsocketController.getConnectedUsers(
  964. this.client,
  965. this.callback
  966. )
  967. })
  968. it('should call the callback with no details', function () {
  969. return expect(this.callback.args[0]).to.deep.equal([])
  970. })
  971. return it('should not check permissions', function () {
  972. return expect(
  973. this.AuthorizationManager.assertClientCanViewProject.called
  974. ).to.equal(false)
  975. })
  976. })
  977. })
  978. describe('updateClientPosition', function () {
  979. beforeEach(function () {
  980. this.WebsocketLoadBalancer.emitToRoom = sinon.stub()
  981. this.ConnectedUsersManager.updateUserPosition = sinon
  982. .stub()
  983. .callsArgAsync(4)
  984. this.AuthorizationManager.assertClientCanViewProjectAndDoc = sinon
  985. .stub()
  986. .callsArgWith(2, null)
  987. return (this.update = {
  988. doc_id: (this.doc_id = 'doc-id-123'),
  989. row: (this.row = 42),
  990. column: (this.column = 37),
  991. })
  992. })
  993. describe('with a logged in user', function () {
  994. beforeEach(function (done) {
  995. this.client.ol_context = {
  996. project_id: this.project_id,
  997. first_name: (this.first_name = 'Douglas'),
  998. last_name: (this.last_name = 'Adams'),
  999. email: (this.email = 'joe@example.com'),
  1000. user_id: (this.user_id = 'user-id-123'),
  1001. }
  1002. this.populatedCursorData = {
  1003. doc_id: this.doc_id,
  1004. id: this.client.publicId,
  1005. name: `${this.first_name} ${this.last_name}`,
  1006. row: this.row,
  1007. column: this.column,
  1008. email: this.email,
  1009. user_id: this.user_id,
  1010. }
  1011. this.WebsocketController.updateClientPosition(
  1012. this.client,
  1013. this.update,
  1014. done
  1015. )
  1016. })
  1017. it("should send the update to the project room with the user's name", function () {
  1018. return this.WebsocketLoadBalancer.emitToRoom
  1019. .calledWith(
  1020. this.project_id,
  1021. 'clientTracking.clientUpdated',
  1022. this.populatedCursorData
  1023. )
  1024. .should.equal(true)
  1025. })
  1026. it('should send the cursor data to the connected user manager', function (done) {
  1027. this.ConnectedUsersManager.updateUserPosition
  1028. .calledWith(
  1029. this.project_id,
  1030. this.client.publicId,
  1031. {
  1032. _id: this.user_id,
  1033. email: this.email,
  1034. first_name: this.first_name,
  1035. last_name: this.last_name,
  1036. },
  1037. {
  1038. row: this.row,
  1039. column: this.column,
  1040. doc_id: this.doc_id,
  1041. }
  1042. )
  1043. .should.equal(true)
  1044. return done()
  1045. })
  1046. return it('should increment the update-client-position metric at 0.1 frequency', function () {
  1047. return this.metrics.inc
  1048. .calledWith('editor.update-client-position', 0.1)
  1049. .should.equal(true)
  1050. })
  1051. })
  1052. describe('with a logged in user who has no last_name set', function () {
  1053. beforeEach(function (done) {
  1054. this.client.ol_context = {
  1055. project_id: this.project_id,
  1056. first_name: (this.first_name = 'Douglas'),
  1057. last_name: undefined,
  1058. email: (this.email = 'joe@example.com'),
  1059. user_id: (this.user_id = 'user-id-123'),
  1060. }
  1061. this.populatedCursorData = {
  1062. doc_id: this.doc_id,
  1063. id: this.client.publicId,
  1064. name: `${this.first_name}`,
  1065. row: this.row,
  1066. column: this.column,
  1067. email: this.email,
  1068. user_id: this.user_id,
  1069. }
  1070. this.WebsocketController.updateClientPosition(
  1071. this.client,
  1072. this.update,
  1073. done
  1074. )
  1075. })
  1076. it("should send the update to the project room with the user's name", function () {
  1077. return this.WebsocketLoadBalancer.emitToRoom
  1078. .calledWith(
  1079. this.project_id,
  1080. 'clientTracking.clientUpdated',
  1081. this.populatedCursorData
  1082. )
  1083. .should.equal(true)
  1084. })
  1085. it('should send the cursor data to the connected user manager', function (done) {
  1086. this.ConnectedUsersManager.updateUserPosition
  1087. .calledWith(
  1088. this.project_id,
  1089. this.client.publicId,
  1090. {
  1091. _id: this.user_id,
  1092. email: this.email,
  1093. first_name: this.first_name,
  1094. last_name: undefined,
  1095. },
  1096. {
  1097. row: this.row,
  1098. column: this.column,
  1099. doc_id: this.doc_id,
  1100. }
  1101. )
  1102. .should.equal(true)
  1103. return done()
  1104. })
  1105. return it('should increment the update-client-position metric at 0.1 frequency', function () {
  1106. return this.metrics.inc
  1107. .calledWith('editor.update-client-position', 0.1)
  1108. .should.equal(true)
  1109. })
  1110. })
  1111. describe('with a logged in user who has no first_name set', function () {
  1112. beforeEach(function (done) {
  1113. this.client.ol_context = {
  1114. project_id: this.project_id,
  1115. first_name: undefined,
  1116. last_name: (this.last_name = 'Adams'),
  1117. email: (this.email = 'joe@example.com'),
  1118. user_id: (this.user_id = 'user-id-123'),
  1119. }
  1120. this.populatedCursorData = {
  1121. doc_id: this.doc_id,
  1122. id: this.client.publicId,
  1123. name: `${this.last_name}`,
  1124. row: this.row,
  1125. column: this.column,
  1126. email: this.email,
  1127. user_id: this.user_id,
  1128. }
  1129. this.WebsocketController.updateClientPosition(
  1130. this.client,
  1131. this.update,
  1132. done
  1133. )
  1134. })
  1135. it("should send the update to the project room with the user's name", function () {
  1136. return this.WebsocketLoadBalancer.emitToRoom
  1137. .calledWith(
  1138. this.project_id,
  1139. 'clientTracking.clientUpdated',
  1140. this.populatedCursorData
  1141. )
  1142. .should.equal(true)
  1143. })
  1144. it('should send the cursor data to the connected user manager', function (done) {
  1145. this.ConnectedUsersManager.updateUserPosition
  1146. .calledWith(
  1147. this.project_id,
  1148. this.client.publicId,
  1149. {
  1150. _id: this.user_id,
  1151. email: this.email,
  1152. first_name: undefined,
  1153. last_name: this.last_name,
  1154. },
  1155. {
  1156. row: this.row,
  1157. column: this.column,
  1158. doc_id: this.doc_id,
  1159. }
  1160. )
  1161. .should.equal(true)
  1162. return done()
  1163. })
  1164. return it('should increment the update-client-position metric at 0.1 frequency', function () {
  1165. return this.metrics.inc
  1166. .calledWith('editor.update-client-position', 0.1)
  1167. .should.equal(true)
  1168. })
  1169. })
  1170. describe('with a logged in user who has no names set', function () {
  1171. beforeEach(function (done) {
  1172. this.client.ol_context = {
  1173. project_id: this.project_id,
  1174. first_name: undefined,
  1175. last_name: undefined,
  1176. email: (this.email = 'joe@example.com'),
  1177. user_id: (this.user_id = 'user-id-123'),
  1178. }
  1179. return this.WebsocketController.updateClientPosition(
  1180. this.client,
  1181. this.update,
  1182. done
  1183. )
  1184. })
  1185. return it('should send the update to the project name with no name', function () {
  1186. return this.WebsocketLoadBalancer.emitToRoom
  1187. .calledWith(this.project_id, 'clientTracking.clientUpdated', {
  1188. doc_id: this.doc_id,
  1189. id: this.client.publicId,
  1190. user_id: this.user_id,
  1191. name: '',
  1192. row: this.row,
  1193. column: this.column,
  1194. email: this.email,
  1195. })
  1196. .should.equal(true)
  1197. })
  1198. })
  1199. describe('with an anonymous user', function () {
  1200. beforeEach(function (done) {
  1201. this.client.ol_context = {
  1202. project_id: this.project_id,
  1203. }
  1204. return this.WebsocketController.updateClientPosition(
  1205. this.client,
  1206. this.update,
  1207. done
  1208. )
  1209. })
  1210. it('should send the update to the project room with no name', function () {
  1211. return this.WebsocketLoadBalancer.emitToRoom
  1212. .calledWith(this.project_id, 'clientTracking.clientUpdated', {
  1213. doc_id: this.doc_id,
  1214. id: this.client.publicId,
  1215. name: '',
  1216. row: this.row,
  1217. column: this.column,
  1218. })
  1219. .should.equal(true)
  1220. })
  1221. return it('should not send cursor data to the connected user manager', function (done) {
  1222. this.ConnectedUsersManager.updateUserPosition.called.should.equal(false)
  1223. return done()
  1224. })
  1225. })
  1226. return describe('when the client has disconnected', function () {
  1227. beforeEach(function (done) {
  1228. this.client.disconnected = true
  1229. this.AuthorizationManager.assertClientCanViewProjectAndDoc =
  1230. sinon.stub()
  1231. return this.WebsocketController.updateClientPosition(
  1232. this.client,
  1233. this.update,
  1234. (...args) => {
  1235. this.callback(...args)
  1236. done(args[0])
  1237. }
  1238. )
  1239. })
  1240. it('should call the callback with no details', function () {
  1241. return expect(this.callback.args[0]).to.deep.equal([])
  1242. })
  1243. return it('should not check permissions', function () {
  1244. return expect(
  1245. this.AuthorizationManager.assertClientCanViewProjectAndDoc.called
  1246. ).to.equal(false)
  1247. })
  1248. })
  1249. })
  1250. describe('applyOtUpdate', function () {
  1251. beforeEach(function () {
  1252. this.update = { op: { p: 12, t: 'foo' } }
  1253. this.client.ol_context.user_id = this.user_id
  1254. this.client.ol_context.project_id = this.project_id
  1255. this.WebsocketController._assertClientCanApplyUpdate = sinon
  1256. .stub()
  1257. .yields()
  1258. return (this.DocumentUpdaterManager.queueChange = sinon
  1259. .stub()
  1260. .callsArg(3))
  1261. })
  1262. describe('succesfully', function () {
  1263. beforeEach(function () {
  1264. return this.WebsocketController.applyOtUpdate(
  1265. this.client,
  1266. this.doc_id,
  1267. this.update,
  1268. this.callback
  1269. )
  1270. })
  1271. it('should set the source of the update to the client id', function () {
  1272. return this.update.meta.source.should.equal(this.client.publicId)
  1273. })
  1274. it('should set the user_id of the update to the user id', function () {
  1275. return this.update.meta.user_id.should.equal(this.user_id)
  1276. })
  1277. it('should queue the update', function () {
  1278. return this.DocumentUpdaterManager.queueChange
  1279. .calledWith(this.project_id, this.doc_id, this.update)
  1280. .should.equal(true)
  1281. })
  1282. it('should call the callback', function () {
  1283. return this.callback.called.should.equal(true)
  1284. })
  1285. return it('should increment the doc updates', function () {
  1286. return this.metrics.inc
  1287. .calledWith('editor.doc-update')
  1288. .should.equal(true)
  1289. })
  1290. })
  1291. describe('unsuccessfully', function () {
  1292. beforeEach(function () {
  1293. this.client.disconnect = sinon.stub()
  1294. this.DocumentUpdaterManager.queueChange = sinon
  1295. .stub()
  1296. .callsArgWith(3, (this.error = new Error('Something went wrong')))
  1297. return this.WebsocketController.applyOtUpdate(
  1298. this.client,
  1299. this.doc_id,
  1300. this.update,
  1301. this.callback
  1302. )
  1303. })
  1304. it('should disconnect the client', function () {
  1305. return this.client.disconnect.called.should.equal(true)
  1306. })
  1307. it('should not log an error', function () {
  1308. return this.logger.error.called.should.equal(false)
  1309. })
  1310. return it('should call the callback with the error', function () {
  1311. return this.callback.calledWith(this.error).should.equal(true)
  1312. })
  1313. })
  1314. describe('when not authorized', function () {
  1315. beforeEach(function () {
  1316. this.client.disconnect = sinon.stub()
  1317. this.WebsocketController._assertClientCanApplyUpdate = sinon
  1318. .stub()
  1319. .yields((this.error = new Error('not authorized')))
  1320. return this.WebsocketController.applyOtUpdate(
  1321. this.client,
  1322. this.doc_id,
  1323. this.update,
  1324. this.callback
  1325. )
  1326. })
  1327. // This happens in a setTimeout to allow the client a chance to receive the error first.
  1328. // I'm not sure how to unit test, but it is acceptance tested.
  1329. // it "should disconnect the client", ->
  1330. // @client.disconnect.called.should.equal true
  1331. it('should not log a warning', function () {
  1332. return this.logger.warn.called.should.equal(false)
  1333. })
  1334. return it('should call the callback with the error', function () {
  1335. return this.callback.calledWith(this.error).should.equal(true)
  1336. })
  1337. })
  1338. return describe('update_too_large', function () {
  1339. beforeEach(function (done) {
  1340. this.client.disconnect = sinon.stub()
  1341. this.client.emit = sinon.stub()
  1342. this.client.ol_context.user_id = this.user_id
  1343. this.client.ol_context.project_id = this.project_id
  1344. const error = new UpdateTooLargeError(7372835)
  1345. this.DocumentUpdaterManager.queueChange = sinon
  1346. .stub()
  1347. .callsArgWith(3, error)
  1348. this.WebsocketController.applyOtUpdate(
  1349. this.client,
  1350. this.doc_id,
  1351. this.update,
  1352. this.callback
  1353. )
  1354. return setTimeout(() => done(), 1)
  1355. })
  1356. it('should call the callback with no error', function () {
  1357. this.callback.called.should.equal(true)
  1358. return this.callback.args[0].should.deep.equal([])
  1359. })
  1360. it('should log a warning with the size and context', function () {
  1361. this.logger.warn.called.should.equal(true)
  1362. return this.logger.warn.args[0].should.deep.equal([
  1363. {
  1364. userId: this.user_id,
  1365. projectId: this.project_id,
  1366. docId: this.doc_id,
  1367. updateSize: 7372835,
  1368. },
  1369. 'update is too large',
  1370. ])
  1371. })
  1372. describe('after 100ms', function () {
  1373. beforeEach(function (done) {
  1374. return setTimeout(done, 100)
  1375. })
  1376. it('should send an otUpdateError the client', function () {
  1377. return this.client.emit.calledWith('otUpdateError').should.equal(true)
  1378. })
  1379. return it('should disconnect the client', function () {
  1380. return this.client.disconnect.called.should.equal(true)
  1381. })
  1382. })
  1383. return describe('when the client disconnects during the next 100ms', function () {
  1384. beforeEach(function (done) {
  1385. this.client.disconnected = true
  1386. return setTimeout(done, 100)
  1387. })
  1388. it('should not send an otUpdateError the client', function () {
  1389. return this.client.emit
  1390. .calledWith('otUpdateError')
  1391. .should.equal(false)
  1392. })
  1393. it('should not disconnect the client', function () {
  1394. return this.client.disconnect.called.should.equal(false)
  1395. })
  1396. return it('should increment the editor.doc-update.disconnected metric with a status', function () {
  1397. return expect(
  1398. this.metrics.inc.calledWith('editor.doc-update.disconnected', 1, {
  1399. status: 'at-otUpdateError',
  1400. })
  1401. ).to.equal(true)
  1402. })
  1403. })
  1404. })
  1405. })
  1406. return describe('_assertClientCanApplyUpdate', function () {
  1407. beforeEach(function () {
  1408. this.edit_update = {
  1409. op: [
  1410. { i: 'foo', p: 42 },
  1411. { c: 'bar', p: 132 },
  1412. ],
  1413. } // comments may still be in an edit op
  1414. this.comment_update = { op: [{ c: 'bar', p: 132 }] }
  1415. this.AuthorizationManager.assertClientCanEditProjectAndDoc = sinon.stub()
  1416. return (this.AuthorizationManager.assertClientCanViewProjectAndDoc =
  1417. sinon.stub())
  1418. })
  1419. describe('with a read-write client', function () {
  1420. return it('should return successfully', function (done) {
  1421. this.AuthorizationManager.assertClientCanEditProjectAndDoc.yields(null)
  1422. return this.WebsocketController._assertClientCanApplyUpdate(
  1423. this.client,
  1424. this.doc_id,
  1425. this.edit_update,
  1426. error => {
  1427. expect(error).to.be.null
  1428. return done()
  1429. }
  1430. )
  1431. })
  1432. })
  1433. describe('with a read-only client and an edit op', function () {
  1434. return it('should return an error', function (done) {
  1435. this.AuthorizationManager.assertClientCanEditProjectAndDoc.yields(
  1436. new Error('not authorized')
  1437. )
  1438. this.AuthorizationManager.assertClientCanViewProjectAndDoc.yields(null)
  1439. return this.WebsocketController._assertClientCanApplyUpdate(
  1440. this.client,
  1441. this.doc_id,
  1442. this.edit_update,
  1443. error => {
  1444. expect(error.message).to.equal('not authorized')
  1445. return done()
  1446. }
  1447. )
  1448. })
  1449. })
  1450. describe('with a read-only client and a comment op', function () {
  1451. return it('should return successfully', function (done) {
  1452. this.AuthorizationManager.assertClientCanEditProjectAndDoc.yields(
  1453. new Error('not authorized')
  1454. )
  1455. this.AuthorizationManager.assertClientCanViewProjectAndDoc.yields(null)
  1456. return this.WebsocketController._assertClientCanApplyUpdate(
  1457. this.client,
  1458. this.doc_id,
  1459. this.comment_update,
  1460. error => {
  1461. expect(error).to.be.null
  1462. return done()
  1463. }
  1464. )
  1465. })
  1466. })
  1467. return describe('with a totally unauthorized client', function () {
  1468. return it('should return an error', function (done) {
  1469. this.AuthorizationManager.assertClientCanEditProjectAndDoc.yields(
  1470. new Error('not authorized')
  1471. )
  1472. this.AuthorizationManager.assertClientCanViewProjectAndDoc.yields(
  1473. new Error('not authorized')
  1474. )
  1475. return this.WebsocketController._assertClientCanApplyUpdate(
  1476. this.client,
  1477. this.doc_id,
  1478. this.comment_update,
  1479. error => {
  1480. expect(error.message).to.equal('not authorized')
  1481. return done()
  1482. }
  1483. )
  1484. })
  1485. })
  1486. })
  1487. })