WebsocketControllerTests.js 52 KB

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