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. describe('WebsocketController', function () {
  23. beforeEach(function () {
  24. tk.freeze(new Date())
  25. this.project_id = 'project-id-123'
  26. this.user = {
  27. _id: (this.user_id = 'user-id-123'),
  28. first_name: 'James',
  29. last_name: 'Allen',
  30. email: 'james@example.com',
  31. signUpDate: new Date('2014-01-01'),
  32. loginCount: 42
  33. }
  34. this.callback = sinon.stub()
  35. this.client = {
  36. disconnected: false,
  37. id: (this.client_id = 'mock-client-id-123'),
  38. publicId: `other-id-${Math.random()}`,
  39. ol_context: {},
  40. joinLeaveEpoch: 0,
  41. join: sinon.stub(),
  42. leave: sinon.stub()
  43. }
  44. return (this.WebsocketController = SandboxedModule.require(modulePath, {
  45. requires: {
  46. './WebApiManager': (this.WebApiManager = {}),
  47. './AuthorizationManager': (this.AuthorizationManager = {}),
  48. './DocumentUpdaterManager': (this.DocumentUpdaterManager = {}),
  49. './ConnectedUsersManager': (this.ConnectedUsersManager = {}),
  50. './WebsocketLoadBalancer': (this.WebsocketLoadBalancer = {}),
  51. 'logger-sharelatex': (this.logger = {
  52. log: sinon.stub(),
  53. error: sinon.stub(),
  54. warn: sinon.stub()
  55. }),
  56. 'metrics-sharelatex': (this.metrics = {
  57. inc: sinon.stub(),
  58. set: sinon.stub()
  59. }),
  60. './RoomManager': (this.RoomManager = {})
  61. }
  62. }))
  63. })
  64. afterEach(function () {
  65. return tk.reset()
  66. })
  67. describe('joinProject', function () {
  68. describe('when authorised', function () {
  69. beforeEach(function () {
  70. this.client.id = 'mock-client-id'
  71. this.project = {
  72. name: 'Test Project',
  73. owner: {
  74. _id: (this.owner_id = 'mock-owner-id-123')
  75. }
  76. }
  77. this.privilegeLevel = 'owner'
  78. this.ConnectedUsersManager.updateUserPosition = sinon
  79. .stub()
  80. .callsArgAsync(4)
  81. this.isRestrictedUser = true
  82. this.WebApiManager.joinProject = sinon
  83. .stub()
  84. .callsArgWith(
  85. 2,
  86. null,
  87. this.project,
  88. this.privilegeLevel,
  89. this.isRestrictedUser
  90. )
  91. this.RoomManager.joinProject = sinon.stub().callsArg(2)
  92. return this.WebsocketController.joinProject(
  93. this.client,
  94. this.user,
  95. this.project_id,
  96. this.callback
  97. )
  98. })
  99. it('should load the project from web', function () {
  100. return this.WebApiManager.joinProject
  101. .calledWith(this.project_id, this.user)
  102. .should.equal(true)
  103. })
  104. it('should join the project room', function () {
  105. return this.RoomManager.joinProject
  106. .calledWith(this.client, this.project_id)
  107. .should.equal(true)
  108. })
  109. it('should set the privilege level on the client', function () {
  110. return this.client.ol_context.privilege_level.should.equal(
  111. this.privilegeLevel
  112. )
  113. })
  114. it("should set the user's id on the client", function () {
  115. return this.client.ol_context.user_id.should.equal(this.user._id)
  116. })
  117. it("should set the user's email on the client", function () {
  118. return this.client.ol_context.email.should.equal(this.user.email)
  119. })
  120. it("should set the user's first_name on the client", function () {
  121. return this.client.ol_context.first_name.should.equal(
  122. this.user.first_name
  123. )
  124. })
  125. it("should set the user's last_name on the client", function () {
  126. return this.client.ol_context.last_name.should.equal(
  127. this.user.last_name
  128. )
  129. })
  130. it("should set the user's sign up date on the client", function () {
  131. return this.client.ol_context.signup_date.should.equal(
  132. this.user.signUpDate
  133. )
  134. })
  135. it("should set the user's login_count on the client", function () {
  136. return this.client.ol_context.login_count.should.equal(
  137. this.user.loginCount
  138. )
  139. })
  140. it('should set the connected time on the client', function () {
  141. return this.client.ol_context.connected_time.should.equal(new Date())
  142. })
  143. it('should set the project_id on the client', function () {
  144. return this.client.ol_context.project_id.should.equal(this.project_id)
  145. })
  146. it('should set the project owner id on the client', function () {
  147. return this.client.ol_context.owner_id.should.equal(this.owner_id)
  148. })
  149. it('should set the is_restricted_user flag on the client', function () {
  150. return this.client.ol_context.is_restricted_user.should.equal(
  151. this.isRestrictedUser
  152. )
  153. })
  154. it('should call the callback with the project, privilegeLevel and protocolVersion', function () {
  155. return this.callback
  156. .calledWith(
  157. null,
  158. this.project,
  159. this.privilegeLevel,
  160. this.WebsocketController.PROTOCOL_VERSION
  161. )
  162. .should.equal(true)
  163. })
  164. it('should mark the user as connected in ConnectedUsersManager', function () {
  165. return this.ConnectedUsersManager.updateUserPosition
  166. .calledWith(this.project_id, this.client.publicId, this.user, null)
  167. .should.equal(true)
  168. })
  169. return it('should increment the join-project metric', function () {
  170. return this.metrics.inc
  171. .calledWith('editor.join-project')
  172. .should.equal(true)
  173. })
  174. })
  175. describe('when not authorized', function () {
  176. beforeEach(function () {
  177. this.WebApiManager.joinProject = sinon
  178. .stub()
  179. .callsArgWith(2, null, null, null)
  180. return this.WebsocketController.joinProject(
  181. this.client,
  182. this.user,
  183. this.project_id,
  184. this.callback
  185. )
  186. })
  187. it('should return an error', function () {
  188. return this.callback
  189. .calledWith(sinon.match({ message: 'not authorized' }))
  190. .should.equal(true)
  191. })
  192. return it('should not log an error', function () {
  193. return this.logger.error.called.should.equal(false)
  194. })
  195. })
  196. describe('when the subscribe failed', function () {
  197. beforeEach(function () {
  198. this.client.id = 'mock-client-id'
  199. this.project = {
  200. name: 'Test Project',
  201. owner: {
  202. _id: (this.owner_id = 'mock-owner-id-123')
  203. }
  204. }
  205. this.privilegeLevel = 'owner'
  206. this.ConnectedUsersManager.updateUserPosition = sinon
  207. .stub()
  208. .callsArgAsync(4)
  209. this.isRestrictedUser = true
  210. this.WebApiManager.joinProject = sinon
  211. .stub()
  212. .callsArgWith(
  213. 2,
  214. null,
  215. this.project,
  216. this.privilegeLevel,
  217. this.isRestrictedUser
  218. )
  219. this.RoomManager.joinProject = sinon
  220. .stub()
  221. .callsArgWith(2, new Error('subscribe failed'))
  222. return this.WebsocketController.joinProject(
  223. this.client,
  224. this.user,
  225. this.project_id,
  226. this.callback
  227. )
  228. })
  229. return it('should return an error', function () {
  230. this.callback
  231. .calledWith(sinon.match({ message: 'subscribe failed' }))
  232. .should.equal(true)
  233. return this.callback.args[0][0].message.should.equal('subscribe failed')
  234. })
  235. })
  236. describe('when the client has disconnected', function () {
  237. beforeEach(function () {
  238. this.client.disconnected = true
  239. this.WebApiManager.joinProject = sinon.stub().callsArg(2)
  240. return this.WebsocketController.joinProject(
  241. this.client,
  242. this.user,
  243. this.project_id,
  244. this.callback
  245. )
  246. })
  247. it('should not call WebApiManager.joinProject', function () {
  248. return expect(this.WebApiManager.joinProject.called).to.equal(false)
  249. })
  250. it('should call the callback with no details', function () {
  251. return expect(this.callback.args[0]).to.deep.equal([])
  252. })
  253. return it('should increment the editor.join-project.disconnected metric with a status', function () {
  254. return expect(
  255. this.metrics.inc.calledWith('editor.join-project.disconnected', 1, {
  256. status: 'immediately'
  257. })
  258. ).to.equal(true)
  259. })
  260. })
  261. return describe('when the client disconnects while WebApiManager.joinProject is running', function () {
  262. beforeEach(function () {
  263. this.WebApiManager.joinProject = (project, user, cb) => {
  264. this.client.disconnected = true
  265. return cb(
  266. null,
  267. this.project,
  268. this.privilegeLevel,
  269. this.isRestrictedUser
  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: (room_id) => {
  305. if (room_id !== 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: (room_id) => {
  374. if (room_id !== 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 escaped_lines = this.callback.args[0][1]
  553. const escaped_word = escaped_lines.pop()
  554. escaped_word.should.equal('räksmörgås')
  555. // Check that unescaping works
  556. return decodeURIComponent(escape(escaped_word)).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 encoded_comments = this.callback.args[0][4]
  574. const encoded_comment = encoded_comments.comments.pop()
  575. const encoded_comment_text = encoded_comment.op.c
  576. return encoded_comment_text.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 encoded_changes = this.callback.args[0][4]
  590. const encoded_change = encoded_changes.changes.pop()
  591. const encoded_change_text = encoded_change.op.i
  592. return encoded_change_text.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 encoded_changes = this.callback.args[0][4]
  604. const encoded_change = encoded_changes.changes.pop()
  605. const encoded_change_text = encoded_change.op.d
  606. return encoded_change_text.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 = (
  683. project_id,
  684. doc_id,
  685. cb
  686. ) => {
  687. this.client.disconnected = true
  688. cb()
  689. }
  690. this.WebsocketController.joinDoc(
  691. this.client,
  692. this.doc_id,
  693. -1,
  694. this.options,
  695. (...args) => {
  696. this.callback(...args)
  697. done()
  698. }
  699. )
  700. })
  701. it('should call the callback with no details', function () {
  702. expect(this.callback.called).to.equal(true)
  703. expect(this.callback.args[0]).to.deep.equal([])
  704. })
  705. it('should increment the editor.join-doc.disconnected metric with a status', function () {
  706. expect(
  707. this.metrics.inc.calledWith('editor.join-doc.disconnected', 1, {
  708. status: 'after-client-auth-check'
  709. })
  710. ).to.equal(true)
  711. })
  712. it('should not get the document', function () {
  713. expect(this.DocumentUpdaterManager.getDocument.called).to.equal(false)
  714. })
  715. })
  716. describe('when the client starts a parallel joinDoc request', function () {
  717. beforeEach(function (done) {
  718. this.AuthorizationManager.assertClientCanViewProjectAndDoc.yields(
  719. new Error()
  720. )
  721. this.DocumentUpdaterManager.checkDocument = (
  722. project_id,
  723. doc_id,
  724. cb
  725. ) => {
  726. this.DocumentUpdaterManager.checkDocument = sinon.stub().yields()
  727. this.WebsocketController.joinDoc(
  728. this.client,
  729. this.doc_id,
  730. -1,
  731. {},
  732. () => {}
  733. )
  734. cb()
  735. }
  736. this.WebsocketController.joinDoc(
  737. this.client,
  738. this.doc_id,
  739. -1,
  740. this.options,
  741. (...args) => {
  742. this.callback(...args)
  743. // make sure the other joinDoc request completed
  744. setTimeout(done, 5)
  745. }
  746. )
  747. })
  748. it('should call the callback with an error', function () {
  749. expect(this.callback.called).to.equal(true)
  750. expect(this.callback.args[0][0].message).to.equal(
  751. 'joinLeaveEpoch mismatch'
  752. )
  753. })
  754. it('should get the document once (the parallel request wins)', function () {
  755. expect(this.DocumentUpdaterManager.getDocument.callCount).to.equal(1)
  756. })
  757. })
  758. describe('when the client starts a parallel leaveDoc request', function () {
  759. beforeEach(function (done) {
  760. this.RoomManager.leaveDoc = sinon.stub()
  761. this.AuthorizationManager.assertClientCanViewProjectAndDoc.yields(
  762. new Error()
  763. )
  764. this.DocumentUpdaterManager.checkDocument = (
  765. project_id,
  766. doc_id,
  767. cb
  768. ) => {
  769. this.WebsocketController.leaveDoc(this.client, this.doc_id, () => {})
  770. cb()
  771. }
  772. this.WebsocketController.joinDoc(
  773. this.client,
  774. this.doc_id,
  775. -1,
  776. this.options,
  777. (...args) => {
  778. this.callback(...args)
  779. done()
  780. }
  781. )
  782. })
  783. it('should call the callback with an error', function () {
  784. expect(this.callback.called).to.equal(true)
  785. expect(this.callback.args[0][0].message).to.equal(
  786. 'joinLeaveEpoch mismatch'
  787. )
  788. })
  789. it('should not get the document', function () {
  790. expect(this.DocumentUpdaterManager.getDocument.called).to.equal(false)
  791. })
  792. })
  793. describe('when the client disconnects while RoomManager.joinDoc is running', function () {
  794. beforeEach(function () {
  795. this.RoomManager.joinDoc = (client, doc_id, cb) => {
  796. this.client.disconnected = true
  797. return cb()
  798. }
  799. return this.WebsocketController.joinDoc(
  800. this.client,
  801. this.doc_id,
  802. -1,
  803. this.options,
  804. this.callback
  805. )
  806. })
  807. it('should call the callback with no details', function () {
  808. return expect(this.callback.args[0]).to.deep.equal([])
  809. })
  810. it('should increment the editor.join-doc.disconnected metric with a status', function () {
  811. return expect(
  812. this.metrics.inc.calledWith('editor.join-doc.disconnected', 1, {
  813. status: 'after-joining-room'
  814. })
  815. ).to.equal(true)
  816. })
  817. return it('should not get the document', function () {
  818. return expect(this.DocumentUpdaterManager.getDocument.called).to.equal(
  819. false
  820. )
  821. })
  822. })
  823. return describe('when the client disconnects while DocumentUpdaterManager.getDocument is running', function () {
  824. beforeEach(function () {
  825. this.DocumentUpdaterManager.getDocument = (
  826. project_id,
  827. doc_id,
  828. fromVersion,
  829. callback
  830. ) => {
  831. this.client.disconnected = true
  832. return callback(
  833. null,
  834. this.doc_lines,
  835. this.version,
  836. this.ranges,
  837. this.ops
  838. )
  839. }
  840. return this.WebsocketController.joinDoc(
  841. this.client,
  842. this.doc_id,
  843. -1,
  844. this.options,
  845. this.callback
  846. )
  847. })
  848. it('should call the callback with no details', function () {
  849. return expect(this.callback.args[0]).to.deep.equal([])
  850. })
  851. return it('should increment the editor.join-doc.disconnected metric with a status', function () {
  852. return expect(
  853. this.metrics.inc.calledWith('editor.join-doc.disconnected', 1, {
  854. status: 'after-doc-updater-call'
  855. })
  856. ).to.equal(true)
  857. })
  858. })
  859. })
  860. describe('leaveDoc', function () {
  861. beforeEach(function () {
  862. this.doc_id = 'doc-id-123'
  863. this.client.ol_context.project_id = this.project_id
  864. this.RoomManager.leaveDoc = sinon.stub()
  865. return this.WebsocketController.leaveDoc(
  866. this.client,
  867. this.doc_id,
  868. this.callback
  869. )
  870. })
  871. it('should inc the joinLeaveEpoch', function () {
  872. expect(this.client.joinLeaveEpoch).to.equal(1)
  873. })
  874. it('should remove the client from the doc_id room', function () {
  875. return this.RoomManager.leaveDoc
  876. .calledWith(this.client, this.doc_id)
  877. .should.equal(true)
  878. })
  879. it('should call the callback', function () {
  880. return this.callback.called.should.equal(true)
  881. })
  882. return it('should increment the leave-doc metric', function () {
  883. return this.metrics.inc.calledWith('editor.leave-doc').should.equal(true)
  884. })
  885. })
  886. describe('getConnectedUsers', function () {
  887. beforeEach(function () {
  888. this.client.ol_context.project_id = this.project_id
  889. this.users = ['mock', 'users']
  890. this.WebsocketLoadBalancer.emitToRoom = sinon.stub()
  891. return (this.ConnectedUsersManager.getConnectedUsers = sinon
  892. .stub()
  893. .callsArgWith(1, null, this.users))
  894. })
  895. describe('when authorized', function () {
  896. beforeEach(function (done) {
  897. this.AuthorizationManager.assertClientCanViewProject = sinon
  898. .stub()
  899. .callsArgWith(1, null)
  900. return this.WebsocketController.getConnectedUsers(
  901. this.client,
  902. (...args) => {
  903. this.callback(...Array.from(args || []))
  904. return done()
  905. }
  906. )
  907. })
  908. it('should check that the client is authorized to view the project', function () {
  909. return this.AuthorizationManager.assertClientCanViewProject
  910. .calledWith(this.client)
  911. .should.equal(true)
  912. })
  913. it('should broadcast a request to update the client list', function () {
  914. return this.WebsocketLoadBalancer.emitToRoom
  915. .calledWith(this.project_id, 'clientTracking.refresh')
  916. .should.equal(true)
  917. })
  918. it('should get the connected users for the project', function () {
  919. return this.ConnectedUsersManager.getConnectedUsers
  920. .calledWith(this.project_id)
  921. .should.equal(true)
  922. })
  923. it('should return the users', function () {
  924. return this.callback.calledWith(null, this.users).should.equal(true)
  925. })
  926. return it('should increment the get-connected-users metric', function () {
  927. return this.metrics.inc
  928. .calledWith('editor.get-connected-users')
  929. .should.equal(true)
  930. })
  931. })
  932. describe('when not authorized', function () {
  933. beforeEach(function () {
  934. this.AuthorizationManager.assertClientCanViewProject = sinon
  935. .stub()
  936. .callsArgWith(1, (this.err = new Error('not authorized')))
  937. return this.WebsocketController.getConnectedUsers(
  938. this.client,
  939. this.callback
  940. )
  941. })
  942. it('should not get the connected users for the project', function () {
  943. return this.ConnectedUsersManager.getConnectedUsers.called.should.equal(
  944. false
  945. )
  946. })
  947. return it('should return an error', function () {
  948. return this.callback.calledWith(this.err).should.equal(true)
  949. })
  950. })
  951. describe('when restricted user', function () {
  952. beforeEach(function () {
  953. this.client.ol_context.is_restricted_user = true
  954. this.AuthorizationManager.assertClientCanViewProject = sinon
  955. .stub()
  956. .callsArgWith(1, null)
  957. return this.WebsocketController.getConnectedUsers(
  958. this.client,
  959. this.callback
  960. )
  961. })
  962. it('should return an empty array of users', function () {
  963. return this.callback.calledWith(null, []).should.equal(true)
  964. })
  965. return it('should not get the connected users for the project', function () {
  966. return this.ConnectedUsersManager.getConnectedUsers.called.should.equal(
  967. false
  968. )
  969. })
  970. })
  971. return describe('when the client has disconnected', function () {
  972. beforeEach(function () {
  973. this.client.disconnected = true
  974. this.AuthorizationManager.assertClientCanViewProject = sinon.stub()
  975. return this.WebsocketController.getConnectedUsers(
  976. this.client,
  977. this.callback
  978. )
  979. })
  980. it('should call the callback with no details', function () {
  981. return expect(this.callback.args[0]).to.deep.equal([])
  982. })
  983. return it('should not check permissions', function () {
  984. return expect(
  985. this.AuthorizationManager.assertClientCanViewProject.called
  986. ).to.equal(false)
  987. })
  988. })
  989. })
  990. describe('updateClientPosition', function () {
  991. beforeEach(function () {
  992. this.WebsocketLoadBalancer.emitToRoom = sinon.stub()
  993. this.ConnectedUsersManager.updateUserPosition = sinon
  994. .stub()
  995. .callsArgAsync(4)
  996. this.AuthorizationManager.assertClientCanViewProjectAndDoc = sinon
  997. .stub()
  998. .callsArgWith(2, null)
  999. return (this.update = {
  1000. doc_id: (this.doc_id = 'doc-id-123'),
  1001. row: (this.row = 42),
  1002. column: (this.column = 37)
  1003. })
  1004. })
  1005. describe('with a logged in user', function () {
  1006. beforeEach(function (done) {
  1007. this.client.ol_context = {
  1008. project_id: this.project_id,
  1009. first_name: (this.first_name = 'Douglas'),
  1010. last_name: (this.last_name = 'Adams'),
  1011. email: (this.email = 'joe@example.com'),
  1012. user_id: (this.user_id = 'user-id-123')
  1013. }
  1014. this.populatedCursorData = {
  1015. doc_id: this.doc_id,
  1016. id: this.client.publicId,
  1017. name: `${this.first_name} ${this.last_name}`,
  1018. row: this.row,
  1019. column: this.column,
  1020. email: this.email,
  1021. user_id: this.user_id
  1022. }
  1023. this.WebsocketController.updateClientPosition(
  1024. this.client,
  1025. this.update,
  1026. done
  1027. )
  1028. })
  1029. it("should send the update to the project room with the user's name", function () {
  1030. return this.WebsocketLoadBalancer.emitToRoom
  1031. .calledWith(
  1032. this.project_id,
  1033. 'clientTracking.clientUpdated',
  1034. this.populatedCursorData
  1035. )
  1036. .should.equal(true)
  1037. })
  1038. it('should send the cursor data to the connected user manager', function (done) {
  1039. this.ConnectedUsersManager.updateUserPosition
  1040. .calledWith(
  1041. this.project_id,
  1042. this.client.publicId,
  1043. {
  1044. _id: this.user_id,
  1045. email: this.email,
  1046. first_name: this.first_name,
  1047. last_name: this.last_name
  1048. },
  1049. {
  1050. row: this.row,
  1051. column: this.column,
  1052. doc_id: this.doc_id
  1053. }
  1054. )
  1055. .should.equal(true)
  1056. return done()
  1057. })
  1058. return it('should increment the update-client-position metric at 0.1 frequency', function () {
  1059. return this.metrics.inc
  1060. .calledWith('editor.update-client-position', 0.1)
  1061. .should.equal(true)
  1062. })
  1063. })
  1064. describe('with a logged in user who has no last_name set', function () {
  1065. beforeEach(function (done) {
  1066. this.client.ol_context = {
  1067. project_id: this.project_id,
  1068. first_name: (this.first_name = 'Douglas'),
  1069. last_name: undefined,
  1070. email: (this.email = 'joe@example.com'),
  1071. user_id: (this.user_id = 'user-id-123')
  1072. }
  1073. this.populatedCursorData = {
  1074. doc_id: this.doc_id,
  1075. id: this.client.publicId,
  1076. name: `${this.first_name}`,
  1077. row: this.row,
  1078. column: this.column,
  1079. email: this.email,
  1080. user_id: this.user_id
  1081. }
  1082. this.WebsocketController.updateClientPosition(
  1083. this.client,
  1084. this.update,
  1085. done
  1086. )
  1087. })
  1088. it("should send the update to the project room with the user's name", function () {
  1089. return this.WebsocketLoadBalancer.emitToRoom
  1090. .calledWith(
  1091. this.project_id,
  1092. 'clientTracking.clientUpdated',
  1093. this.populatedCursorData
  1094. )
  1095. .should.equal(true)
  1096. })
  1097. it('should send the cursor data to the connected user manager', function (done) {
  1098. this.ConnectedUsersManager.updateUserPosition
  1099. .calledWith(
  1100. this.project_id,
  1101. this.client.publicId,
  1102. {
  1103. _id: this.user_id,
  1104. email: this.email,
  1105. first_name: this.first_name,
  1106. last_name: undefined
  1107. },
  1108. {
  1109. row: this.row,
  1110. column: this.column,
  1111. doc_id: this.doc_id
  1112. }
  1113. )
  1114. .should.equal(true)
  1115. return done()
  1116. })
  1117. return it('should increment the update-client-position metric at 0.1 frequency', function () {
  1118. return this.metrics.inc
  1119. .calledWith('editor.update-client-position', 0.1)
  1120. .should.equal(true)
  1121. })
  1122. })
  1123. describe('with a logged in user who has no first_name set', function () {
  1124. beforeEach(function (done) {
  1125. this.client.ol_context = {
  1126. project_id: this.project_id,
  1127. first_name: undefined,
  1128. last_name: (this.last_name = 'Adams'),
  1129. email: (this.email = 'joe@example.com'),
  1130. user_id: (this.user_id = 'user-id-123')
  1131. }
  1132. this.populatedCursorData = {
  1133. doc_id: this.doc_id,
  1134. id: this.client.publicId,
  1135. name: `${this.last_name}`,
  1136. row: this.row,
  1137. column: this.column,
  1138. email: this.email,
  1139. user_id: this.user_id
  1140. }
  1141. this.WebsocketController.updateClientPosition(
  1142. this.client,
  1143. this.update,
  1144. done
  1145. )
  1146. })
  1147. it("should send the update to the project room with the user's name", function () {
  1148. return this.WebsocketLoadBalancer.emitToRoom
  1149. .calledWith(
  1150. this.project_id,
  1151. 'clientTracking.clientUpdated',
  1152. this.populatedCursorData
  1153. )
  1154. .should.equal(true)
  1155. })
  1156. it('should send the cursor data to the connected user manager', function (done) {
  1157. this.ConnectedUsersManager.updateUserPosition
  1158. .calledWith(
  1159. this.project_id,
  1160. this.client.publicId,
  1161. {
  1162. _id: this.user_id,
  1163. email: this.email,
  1164. first_name: undefined,
  1165. last_name: this.last_name
  1166. },
  1167. {
  1168. row: this.row,
  1169. column: this.column,
  1170. doc_id: this.doc_id
  1171. }
  1172. )
  1173. .should.equal(true)
  1174. return done()
  1175. })
  1176. return it('should increment the update-client-position metric at 0.1 frequency', function () {
  1177. return this.metrics.inc
  1178. .calledWith('editor.update-client-position', 0.1)
  1179. .should.equal(true)
  1180. })
  1181. })
  1182. describe('with a logged in user who has no names set', function () {
  1183. beforeEach(function (done) {
  1184. this.client.ol_context = {
  1185. project_id: this.project_id,
  1186. first_name: undefined,
  1187. last_name: undefined,
  1188. email: (this.email = 'joe@example.com'),
  1189. user_id: (this.user_id = 'user-id-123')
  1190. }
  1191. return this.WebsocketController.updateClientPosition(
  1192. this.client,
  1193. this.update,
  1194. done
  1195. )
  1196. })
  1197. return it('should send the update to the project name with no name', function () {
  1198. return this.WebsocketLoadBalancer.emitToRoom
  1199. .calledWith(this.project_id, 'clientTracking.clientUpdated', {
  1200. doc_id: this.doc_id,
  1201. id: this.client.publicId,
  1202. user_id: this.user_id,
  1203. name: '',
  1204. row: this.row,
  1205. column: this.column,
  1206. email: this.email
  1207. })
  1208. .should.equal(true)
  1209. })
  1210. })
  1211. describe('with an anonymous user', function () {
  1212. beforeEach(function (done) {
  1213. this.client.ol_context = {
  1214. project_id: this.project_id
  1215. }
  1216. return this.WebsocketController.updateClientPosition(
  1217. this.client,
  1218. this.update,
  1219. done
  1220. )
  1221. })
  1222. it('should send the update to the project room with no name', function () {
  1223. return this.WebsocketLoadBalancer.emitToRoom
  1224. .calledWith(this.project_id, 'clientTracking.clientUpdated', {
  1225. doc_id: this.doc_id,
  1226. id: this.client.publicId,
  1227. name: '',
  1228. row: this.row,
  1229. column: this.column
  1230. })
  1231. .should.equal(true)
  1232. })
  1233. return it('should not send cursor data to the connected user manager', function (done) {
  1234. this.ConnectedUsersManager.updateUserPosition.called.should.equal(false)
  1235. return done()
  1236. })
  1237. })
  1238. return describe('when the client has disconnected', function () {
  1239. beforeEach(function (done) {
  1240. this.client.disconnected = true
  1241. this.AuthorizationManager.assertClientCanViewProjectAndDoc = sinon.stub()
  1242. return this.WebsocketController.updateClientPosition(
  1243. this.client,
  1244. this.update,
  1245. (...args) => {
  1246. this.callback(...args)
  1247. done(args[0])
  1248. }
  1249. )
  1250. })
  1251. it('should call the callback with no details', function () {
  1252. return expect(this.callback.args[0]).to.deep.equal([])
  1253. })
  1254. return it('should not check permissions', function () {
  1255. return expect(
  1256. this.AuthorizationManager.assertClientCanViewProjectAndDoc.called
  1257. ).to.equal(false)
  1258. })
  1259. })
  1260. })
  1261. describe('applyOtUpdate', function () {
  1262. beforeEach(function () {
  1263. this.update = { op: { p: 12, t: 'foo' } }
  1264. this.client.ol_context.user_id = this.user_id
  1265. this.client.ol_context.project_id = this.project_id
  1266. this.WebsocketController._assertClientCanApplyUpdate = sinon
  1267. .stub()
  1268. .yields()
  1269. return (this.DocumentUpdaterManager.queueChange = sinon
  1270. .stub()
  1271. .callsArg(3))
  1272. })
  1273. describe('succesfully', function () {
  1274. beforeEach(function () {
  1275. return this.WebsocketController.applyOtUpdate(
  1276. this.client,
  1277. this.doc_id,
  1278. this.update,
  1279. this.callback
  1280. )
  1281. })
  1282. it('should set the source of the update to the client id', function () {
  1283. return this.update.meta.source.should.equal(this.client.publicId)
  1284. })
  1285. it('should set the user_id of the update to the user id', function () {
  1286. return this.update.meta.user_id.should.equal(this.user_id)
  1287. })
  1288. it('should queue the update', function () {
  1289. return this.DocumentUpdaterManager.queueChange
  1290. .calledWith(this.project_id, this.doc_id, this.update)
  1291. .should.equal(true)
  1292. })
  1293. it('should call the callback', function () {
  1294. return this.callback.called.should.equal(true)
  1295. })
  1296. return it('should increment the doc updates', function () {
  1297. return this.metrics.inc
  1298. .calledWith('editor.doc-update')
  1299. .should.equal(true)
  1300. })
  1301. })
  1302. describe('unsuccessfully', function () {
  1303. beforeEach(function () {
  1304. this.client.disconnect = sinon.stub()
  1305. this.DocumentUpdaterManager.queueChange = sinon
  1306. .stub()
  1307. .callsArgWith(3, (this.error = new Error('Something went wrong')))
  1308. return this.WebsocketController.applyOtUpdate(
  1309. this.client,
  1310. this.doc_id,
  1311. this.update,
  1312. this.callback
  1313. )
  1314. })
  1315. it('should disconnect the client', function () {
  1316. return this.client.disconnect.called.should.equal(true)
  1317. })
  1318. it('should log an error', function () {
  1319. return this.logger.error.called.should.equal(true)
  1320. })
  1321. return it('should call the callback with the error', function () {
  1322. return this.callback.calledWith(this.error).should.equal(true)
  1323. })
  1324. })
  1325. describe('when not authorized', function () {
  1326. beforeEach(function () {
  1327. this.client.disconnect = sinon.stub()
  1328. this.WebsocketController._assertClientCanApplyUpdate = sinon
  1329. .stub()
  1330. .yields((this.error = new Error('not authorized')))
  1331. return this.WebsocketController.applyOtUpdate(
  1332. this.client,
  1333. this.doc_id,
  1334. this.update,
  1335. this.callback
  1336. )
  1337. })
  1338. // This happens in a setTimeout to allow the client a chance to receive the error first.
  1339. // I'm not sure how to unit test, but it is acceptance tested.
  1340. // it "should disconnect the client", ->
  1341. // @client.disconnect.called.should.equal true
  1342. it('should log a warning', function () {
  1343. return this.logger.warn.called.should.equal(true)
  1344. })
  1345. return it('should call the callback with the error', function () {
  1346. return this.callback.calledWith(this.error).should.equal(true)
  1347. })
  1348. })
  1349. return describe('update_too_large', function () {
  1350. beforeEach(function (done) {
  1351. this.client.disconnect = sinon.stub()
  1352. this.client.emit = sinon.stub()
  1353. this.client.ol_context.user_id = this.user_id
  1354. this.client.ol_context.project_id = this.project_id
  1355. const error = new Error('update is too large')
  1356. error.updateSize = 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. })