DocumentUpdaterHandlerTests.js 32 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118
  1. const sinon = require('sinon')
  2. const SandboxedModule = require('sandboxed-module')
  3. const path = require('path')
  4. const { ObjectId } = require('mongodb')
  5. const modulePath = path.join(
  6. __dirname,
  7. '../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler'
  8. )
  9. describe('DocumentUpdaterHandler', function () {
  10. beforeEach(function () {
  11. this.project_id = 'project-id-923'
  12. this.projectHistoryId = 'ol-project-id-1'
  13. this.doc_id = 'doc-id-394'
  14. this.lines = ['one', 'two', 'three']
  15. this.version = 42
  16. this.user_id = 'mock-user-id-123'
  17. this.project = { _id: this.project_id }
  18. this.request = sinon.stub()
  19. this.projectEntityHandler = {}
  20. this.settings = {
  21. apis: {
  22. documentupdater: {
  23. url: 'http://document_updater.example.com',
  24. },
  25. project_history: {
  26. url: 'http://project_history.example.com',
  27. },
  28. },
  29. }
  30. this.callback = sinon.stub()
  31. this.handler = SandboxedModule.require(modulePath, {
  32. requires: {
  33. request: {
  34. defaults: () => {
  35. return this.request
  36. },
  37. },
  38. '@overleaf/settings': this.settings,
  39. '../Project/ProjectEntityHandler': this.projectEntityHandler,
  40. '../../models/Project': {
  41. Project: (this.Project = {}),
  42. },
  43. '../../Features/Project/ProjectLocator': {},
  44. '@overleaf/metrics': {
  45. Timer: class {
  46. done() {}
  47. },
  48. },
  49. },
  50. })
  51. })
  52. describe('flushProjectToMongo', function () {
  53. describe('successfully', function () {
  54. beforeEach(function () {
  55. this.request.callsArgWith(1, null, { statusCode: 204 }, '')
  56. this.handler.flushProjectToMongo(this.project_id, this.callback)
  57. })
  58. it('should flush the document from the document updater', function () {
  59. this.request
  60. .calledWithMatch({
  61. url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/flush`,
  62. method: 'POST',
  63. })
  64. .should.equal(true)
  65. })
  66. it('should call the callback with no error', function () {
  67. this.callback.calledWith(null).should.equal(true)
  68. })
  69. })
  70. describe('when the document updater API returns an error', function () {
  71. beforeEach(function () {
  72. this.request.callsArgWith(
  73. 1,
  74. new Error('something went wrong'),
  75. null,
  76. null
  77. )
  78. this.handler.flushProjectToMongo(this.project_id, this.callback)
  79. })
  80. it('should return an error to the callback', function () {
  81. this.callback
  82. .calledWith(sinon.match.instanceOf(Error))
  83. .should.equal(true)
  84. })
  85. })
  86. describe('when the document updater returns a failure error code', function () {
  87. beforeEach(function () {
  88. this.request.callsArgWith(1, null, { statusCode: 500 }, '')
  89. this.handler.flushProjectToMongo(this.project_id, this.callback)
  90. })
  91. it('should return the callback with an error', function () {
  92. this.callback
  93. .calledWith(
  94. sinon.match
  95. .instanceOf(Error)
  96. .and(
  97. sinon.match.has(
  98. 'message',
  99. 'document updater returned a failure status code: 500'
  100. )
  101. )
  102. )
  103. .should.equal(true)
  104. })
  105. })
  106. })
  107. describe('flushProjectToMongoAndDelete', function () {
  108. describe('successfully', function () {
  109. beforeEach(function () {
  110. this.request.callsArgWith(1, null, { statusCode: 204 }, '')
  111. this.handler.flushProjectToMongoAndDelete(
  112. this.project_id,
  113. this.callback
  114. )
  115. })
  116. it('should delete the project from the document updater', function () {
  117. this.request
  118. .calledWithMatch({
  119. url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}`,
  120. method: 'DELETE',
  121. })
  122. .should.equal(true)
  123. })
  124. it('should call the callback with no error', function () {
  125. this.callback.calledWith(null).should.equal(true)
  126. })
  127. })
  128. describe('when the document updater API returns an error', function () {
  129. beforeEach(function () {
  130. this.request.callsArgWith(
  131. 1,
  132. new Error('something went wrong'),
  133. null,
  134. null
  135. )
  136. this.handler.flushProjectToMongoAndDelete(
  137. this.project_id,
  138. this.callback
  139. )
  140. })
  141. it('should return an error to the callback', function () {
  142. this.callback
  143. .calledWith(sinon.match.instanceOf(Error))
  144. .should.equal(true)
  145. })
  146. })
  147. describe('when the document updater returns a failure error code', function () {
  148. beforeEach(function () {
  149. this.request.callsArgWith(1, null, { statusCode: 500 }, '')
  150. this.handler.flushProjectToMongoAndDelete(
  151. this.project_id,
  152. this.callback
  153. )
  154. })
  155. it('should return the callback with an error', function () {
  156. this.callback
  157. .calledWith(
  158. sinon.match
  159. .instanceOf(Error)
  160. .and(
  161. sinon.match.has(
  162. 'message',
  163. 'document updater returned a failure status code: 500'
  164. )
  165. )
  166. )
  167. .should.equal(true)
  168. })
  169. })
  170. })
  171. describe('flushDocToMongo', function () {
  172. describe('successfully', function () {
  173. beforeEach(function () {
  174. this.request.callsArgWith(1, null, { statusCode: 204 }, '')
  175. this.handler.flushDocToMongo(
  176. this.project_id,
  177. this.doc_id,
  178. this.callback
  179. )
  180. })
  181. it('should flush the document from the document updater', function () {
  182. this.request
  183. .calledWithMatch({
  184. url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}/flush`,
  185. method: 'POST',
  186. })
  187. .should.equal(true)
  188. })
  189. it('should call the callback with no error', function () {
  190. this.callback.calledWith(null).should.equal(true)
  191. })
  192. })
  193. describe('when the document updater API returns an error', function () {
  194. beforeEach(function () {
  195. this.request.callsArgWith(
  196. 1,
  197. new Error('something went wrong'),
  198. null,
  199. null
  200. )
  201. this.handler.flushDocToMongo(
  202. this.project_id,
  203. this.doc_id,
  204. this.callback
  205. )
  206. })
  207. it('should return an error to the callback', function () {
  208. this.callback
  209. .calledWith(sinon.match.instanceOf(Error))
  210. .should.equal(true)
  211. })
  212. })
  213. describe('when the document updater returns a failure error code', function () {
  214. beforeEach(function () {
  215. this.request.callsArgWith(1, null, { statusCode: 500 }, '')
  216. this.handler.flushDocToMongo(
  217. this.project_id,
  218. this.doc_id,
  219. this.callback
  220. )
  221. })
  222. it('should return the callback with an error', function () {
  223. this.callback
  224. .calledWith(
  225. sinon.match
  226. .instanceOf(Error)
  227. .and(
  228. sinon.match.has(
  229. 'message',
  230. 'document updater returned a failure status code: 500'
  231. )
  232. )
  233. )
  234. .should.equal(true)
  235. })
  236. })
  237. })
  238. describe('deleteDoc', function () {
  239. describe('successfully', function () {
  240. beforeEach(function () {
  241. this.request.callsArgWith(1, null, { statusCode: 204 }, '')
  242. this.handler.deleteDoc(this.project_id, this.doc_id, this.callback)
  243. })
  244. it('should delete the document from the document updater', function () {
  245. this.request
  246. .calledWithMatch({
  247. url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}`,
  248. method: 'DELETE',
  249. })
  250. .should.equal(true)
  251. })
  252. it('should call the callback with no error', function () {
  253. this.callback.calledWith(null).should.equal(true)
  254. })
  255. })
  256. describe('when the document updater API returns an error', function () {
  257. beforeEach(function () {
  258. this.request.callsArgWith(
  259. 1,
  260. new Error('something went wrong'),
  261. null,
  262. null
  263. )
  264. this.handler.deleteDoc(this.project_id, this.doc_id, this.callback)
  265. })
  266. it('should return an error to the callback', function () {
  267. this.callback
  268. .calledWith(sinon.match.instanceOf(Error))
  269. .should.equal(true)
  270. })
  271. })
  272. describe('when the document updater returns a failure error code', function () {
  273. beforeEach(function () {
  274. this.request.callsArgWith(1, null, { statusCode: 500 }, '')
  275. this.handler.deleteDoc(this.project_id, this.doc_id, this.callback)
  276. })
  277. it('should return the callback with an error', function () {
  278. this.callback
  279. .calledWith(
  280. sinon.match
  281. .instanceOf(Error)
  282. .and(
  283. sinon.match.has(
  284. 'message',
  285. 'document updater returned a failure status code: 500'
  286. )
  287. )
  288. )
  289. .should.equal(true)
  290. })
  291. })
  292. })
  293. describe('setDocument', function () {
  294. beforeEach(function () {
  295. this.source = 'dropbox'
  296. })
  297. describe('successfully', function () {
  298. beforeEach(function () {
  299. this.request.callsArgWith(1, null, { statusCode: 204 }, '')
  300. this.handler.setDocument(
  301. this.project_id,
  302. this.doc_id,
  303. this.user_id,
  304. this.lines,
  305. this.source,
  306. this.callback
  307. )
  308. })
  309. it('should set the document in the document updater', function () {
  310. this.request
  311. .calledWith({
  312. url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}`,
  313. json: {
  314. lines: this.lines,
  315. source: this.source,
  316. user_id: this.user_id,
  317. },
  318. method: 'POST',
  319. })
  320. .should.equal(true)
  321. })
  322. it('should call the callback with no error', function () {
  323. this.callback.calledWith(null).should.equal(true)
  324. })
  325. })
  326. describe('when the document updater API returns an error', function () {
  327. beforeEach(function () {
  328. this.request.callsArgWith(
  329. 1,
  330. new Error('something went wrong'),
  331. null,
  332. null
  333. )
  334. this.handler.setDocument(
  335. this.project_id,
  336. this.doc_id,
  337. this.user_id,
  338. this.lines,
  339. this.source,
  340. this.callback
  341. )
  342. })
  343. it('should return an error to the callback', function () {
  344. this.callback
  345. .calledWith(sinon.match.instanceOf(Error))
  346. .should.equal(true)
  347. })
  348. })
  349. describe('when the document updater returns a failure error code', function () {
  350. beforeEach(function () {
  351. this.request.callsArgWith(1, null, { statusCode: 500 }, '')
  352. this.handler.setDocument(
  353. this.project_id,
  354. this.doc_id,
  355. this.user_id,
  356. this.lines,
  357. this.source,
  358. this.callback
  359. )
  360. })
  361. it('should return the callback with an error', function () {
  362. this.callback
  363. .calledWith(
  364. sinon.match
  365. .instanceOf(Error)
  366. .and(
  367. sinon.match.has(
  368. 'message',
  369. 'document updater returned a failure status code: 500'
  370. )
  371. )
  372. )
  373. .should.equal(true)
  374. })
  375. })
  376. })
  377. describe('getDocument', function () {
  378. describe('successfully', function () {
  379. beforeEach(function () {
  380. this.body = {
  381. lines: this.lines,
  382. version: this.version,
  383. ops: (this.ops = ['mock-op-1', 'mock-op-2']),
  384. ranges: (this.ranges = { mock: 'ranges' }),
  385. }
  386. this.fromVersion = 2
  387. this.request.callsArgWith(1, null, { statusCode: 200 }, this.body)
  388. this.handler.getDocument(
  389. this.project_id,
  390. this.doc_id,
  391. this.fromVersion,
  392. this.callback
  393. )
  394. })
  395. it('should get the document from the document updater', function () {
  396. this.request
  397. .calledWith({
  398. url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}?fromVersion=${this.fromVersion}`,
  399. method: 'GET',
  400. json: true,
  401. })
  402. .should.equal(true)
  403. })
  404. it('should call the callback with the lines and version', function () {
  405. this.callback
  406. .calledWith(null, this.lines, this.version, this.ranges, this.ops)
  407. .should.equal(true)
  408. })
  409. })
  410. describe('when the document updater API returns an error', function () {
  411. beforeEach(function () {
  412. this.request.callsArgWith(
  413. 1,
  414. new Error('something went wrong'),
  415. null,
  416. null
  417. )
  418. this.handler.getDocument(
  419. this.project_id,
  420. this.doc_id,
  421. this.fromVersion,
  422. this.callback
  423. )
  424. })
  425. it('should return an error to the callback', function () {
  426. this.callback
  427. .calledWith(sinon.match.instanceOf(Error))
  428. .should.equal(true)
  429. })
  430. })
  431. describe('when the document updater returns a failure error code', function () {
  432. beforeEach(function () {
  433. this.request.callsArgWith(1, null, { statusCode: 500 }, '')
  434. this.handler.getDocument(
  435. this.project_id,
  436. this.doc_id,
  437. this.fromVersion,
  438. this.callback
  439. )
  440. })
  441. it('should return the callback with an error', function () {
  442. this.callback
  443. .calledWith(
  444. sinon.match
  445. .instanceOf(Error)
  446. .and(
  447. sinon.match.has(
  448. 'message',
  449. 'document updater returned a failure status code: 500'
  450. )
  451. )
  452. )
  453. .should.equal(true)
  454. })
  455. })
  456. })
  457. describe('getProjectDocsIfMatch', function () {
  458. beforeEach(function () {
  459. this.project_state_hash = '1234567890abcdef'
  460. })
  461. describe('successfully', function () {
  462. beforeEach(function () {
  463. this.doc0 = {
  464. _id: this.doc_id,
  465. lines: this.lines,
  466. v: this.version,
  467. }
  468. this.docs = [this.doc0, this.doc0, this.doc0]
  469. this.body = JSON.stringify(this.docs)
  470. this.request.post = sinon
  471. .stub()
  472. .callsArgWith(1, null, { statusCode: 200 }, this.body)
  473. this.handler.getProjectDocsIfMatch(
  474. this.project_id,
  475. this.project_state_hash,
  476. this.callback
  477. )
  478. })
  479. it('should get the documents from the document updater', function () {
  480. const url = `${this.settings.apis.documentupdater.url}/project/${this.project_id}/get_and_flush_if_old?state=${this.project_state_hash}`
  481. this.request.post.calledWith(url).should.equal(true)
  482. })
  483. it('should call the callback with the documents', function () {
  484. this.callback.calledWithExactly(null, this.docs).should.equal(true)
  485. })
  486. })
  487. describe('when the document updater API returns an error', function () {
  488. beforeEach(function () {
  489. this.request.post = sinon
  490. .stub()
  491. .callsArgWith(1, new Error('something went wrong'), null, null)
  492. this.handler.getProjectDocsIfMatch(
  493. this.project_id,
  494. this.project_state_hash,
  495. this.callback
  496. )
  497. })
  498. it('should return an error to the callback', function () {
  499. this.callback
  500. .calledWith(sinon.match.instanceOf(Error))
  501. .should.equal(true)
  502. })
  503. })
  504. describe('when the document updater returns a conflict error code', function () {
  505. beforeEach(function () {
  506. this.request.post = sinon
  507. .stub()
  508. .callsArgWith(1, null, { statusCode: 409 }, 'Conflict')
  509. this.handler.getProjectDocsIfMatch(
  510. this.project_id,
  511. this.project_state_hash,
  512. this.callback
  513. )
  514. })
  515. it('should return the callback with no documents', function () {
  516. this.callback.alwaysCalledWithExactly().should.equal(true)
  517. })
  518. })
  519. })
  520. describe('clearProjectState', function () {
  521. describe('successfully', function () {
  522. beforeEach(function () {
  523. this.request.callsArgWith(1, null, { statusCode: 200 })
  524. this.handler.clearProjectState(this.project_id, this.callback)
  525. })
  526. it('should clear the project state from the document updater', function () {
  527. this.request
  528. .calledWithMatch({
  529. url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/clearState`,
  530. method: 'POST',
  531. })
  532. .should.equal(true)
  533. })
  534. it('should call the callback', function () {
  535. this.callback.calledWith(null).should.equal(true)
  536. })
  537. })
  538. describe('when the document updater API returns an error', function () {
  539. beforeEach(function () {
  540. this.request.callsArgWith(
  541. 1,
  542. new Error('something went wrong'),
  543. null,
  544. null
  545. )
  546. this.handler.clearProjectState(this.project_id, this.callback)
  547. })
  548. it('should return an error to the callback', function () {
  549. this.callback
  550. .calledWith(sinon.match.instanceOf(Error))
  551. .should.equal(true)
  552. })
  553. })
  554. describe('when the document updater returns an error code', function () {
  555. beforeEach(function () {
  556. this.request.callsArgWith(1, null, { statusCode: 500 }, null)
  557. this.handler.clearProjectState(this.project_id, this.callback)
  558. })
  559. it('should return the callback with no documents', function () {
  560. this.callback
  561. .calledWith(
  562. sinon.match
  563. .instanceOf(Error)
  564. .and(
  565. sinon.match.has(
  566. 'message',
  567. 'document updater returned a failure status code: 500'
  568. )
  569. )
  570. )
  571. .should.equal(true)
  572. })
  573. })
  574. })
  575. describe('acceptChanges', function () {
  576. beforeEach(function () {
  577. this.change_id = 'mock-change-id-1'
  578. })
  579. describe('successfully', function () {
  580. beforeEach(function () {
  581. this.request.callsArgWith(1, null, { statusCode: 200 }, this.body)
  582. this.handler.acceptChanges(
  583. this.project_id,
  584. this.doc_id,
  585. [this.change_id],
  586. this.callback
  587. )
  588. })
  589. it('should accept the change in the document updater', function () {
  590. this.request
  591. .calledWith({
  592. url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}/change/accept`,
  593. json: {
  594. change_ids: [this.change_id],
  595. },
  596. method: 'POST',
  597. })
  598. .should.equal(true)
  599. })
  600. it('should call the callback', function () {
  601. this.callback.calledWith(null).should.equal(true)
  602. })
  603. })
  604. describe('when the document updater API returns an error', function () {
  605. beforeEach(function () {
  606. this.request.callsArgWith(
  607. 1,
  608. new Error('something went wrong'),
  609. null,
  610. null
  611. )
  612. this.handler.acceptChanges(
  613. this.project_id,
  614. this.doc_id,
  615. [this.change_id],
  616. this.callback
  617. )
  618. })
  619. it('should return an error to the callback', function () {
  620. this.callback
  621. .calledWith(sinon.match.instanceOf(Error))
  622. .should.equal(true)
  623. })
  624. })
  625. describe('when the document updater returns a failure error code', function () {
  626. beforeEach(function () {
  627. this.request.callsArgWith(1, null, { statusCode: 500 }, '')
  628. this.handler.acceptChanges(
  629. this.project_id,
  630. this.doc_id,
  631. [this.change_id],
  632. this.callback
  633. )
  634. })
  635. it('should return the callback with an error', function () {
  636. this.callback
  637. .calledWith(
  638. sinon.match
  639. .instanceOf(Error)
  640. .and(
  641. sinon.match.has(
  642. 'message',
  643. 'document updater returned a failure status code: 500'
  644. )
  645. )
  646. )
  647. .should.equal(true)
  648. })
  649. })
  650. })
  651. describe('deleteThread', function () {
  652. beforeEach(function () {
  653. this.thread_id = 'mock-thread-id-1'
  654. })
  655. describe('successfully', function () {
  656. beforeEach(function () {
  657. this.request.callsArgWith(1, null, { statusCode: 200 }, this.body)
  658. this.handler.deleteThread(
  659. this.project_id,
  660. this.doc_id,
  661. this.thread_id,
  662. this.callback
  663. )
  664. })
  665. it('should delete the thread in the document updater', function () {
  666. this.request
  667. .calledWithMatch({
  668. url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}/comment/${this.thread_id}`,
  669. method: 'DELETE',
  670. })
  671. .should.equal(true)
  672. })
  673. it('should call the callback', function () {
  674. this.callback.calledWith(null).should.equal(true)
  675. })
  676. })
  677. describe('when the document updater API returns an error', function () {
  678. beforeEach(function () {
  679. this.request.callsArgWith(
  680. 1,
  681. new Error('something went wrong'),
  682. null,
  683. null
  684. )
  685. this.handler.deleteThread(
  686. this.project_id,
  687. this.doc_id,
  688. this.thread_id,
  689. this.callback
  690. )
  691. })
  692. it('should return an error to the callback', function () {
  693. this.callback
  694. .calledWith(sinon.match.instanceOf(Error))
  695. .should.equal(true)
  696. })
  697. })
  698. describe('when the document updater returns a failure error code', function () {
  699. beforeEach(function () {
  700. this.request.callsArgWith(1, null, { statusCode: 500 }, '')
  701. this.handler.deleteThread(
  702. this.project_id,
  703. this.doc_id,
  704. this.thread_id,
  705. this.callback
  706. )
  707. })
  708. it('should return the callback with an error', function () {
  709. this.callback
  710. .calledWith(
  711. sinon.match
  712. .instanceOf(Error)
  713. .and(
  714. sinon.match.has(
  715. 'message',
  716. 'document updater returned a failure status code: 500'
  717. )
  718. )
  719. )
  720. .should.equal(true)
  721. })
  722. })
  723. })
  724. describe('updateProjectStructure ', function () {
  725. beforeEach(function () {
  726. this.user_id = 1234
  727. this.version = 999
  728. })
  729. describe('with project history disabled', function () {
  730. beforeEach(function () {
  731. this.settings.apis.project_history.sendProjectStructureOps = false
  732. this.handler.updateProjectStructure(
  733. this.project_id,
  734. this.projectHistoryId,
  735. this.user_id,
  736. {},
  737. this.callback
  738. )
  739. })
  740. it('does not make a web request', function () {
  741. this.request.called.should.equal(false)
  742. })
  743. it('calls the callback', function () {
  744. this.callback.called.should.equal(true)
  745. })
  746. })
  747. describe('with project history enabled', function () {
  748. beforeEach(function () {
  749. this.settings.apis.project_history.sendProjectStructureOps = true
  750. this.url = `${this.settings.apis.documentupdater.url}/project/${this.project_id}`
  751. this.request.callsArgWith(1, null, { statusCode: 204 }, '')
  752. })
  753. describe('when an entity has changed name', function () {
  754. it('should send the structure update to the document updater', function (done) {
  755. this.docIdA = new ObjectId()
  756. this.docIdB = new ObjectId()
  757. this.changes = {
  758. oldDocs: [
  759. { path: '/old_a', doc: { _id: this.docIdA } },
  760. { path: '/old_b', doc: { _id: this.docIdB } },
  761. ],
  762. // create new instances of the same ObjectIds so that == doesn't pass
  763. newDocs: [
  764. {
  765. path: '/old_a',
  766. doc: { _id: new ObjectId(this.docIdA.toString()) },
  767. },
  768. {
  769. path: '/new_b',
  770. doc: { _id: new ObjectId(this.docIdB.toString()) },
  771. },
  772. ],
  773. newProject: { version: this.version },
  774. }
  775. const updates = [
  776. {
  777. type: 'rename-doc',
  778. id: this.docIdB.toString(),
  779. pathname: '/old_b',
  780. newPathname: '/new_b',
  781. },
  782. ]
  783. this.handler.updateProjectStructure(
  784. this.project_id,
  785. this.projectHistoryId,
  786. this.user_id,
  787. this.changes,
  788. () => {
  789. this.request
  790. .calledWith({
  791. url: this.url,
  792. method: 'POST',
  793. json: {
  794. updates,
  795. userId: this.user_id,
  796. version: this.version,
  797. projectHistoryId: this.projectHistoryId,
  798. },
  799. })
  800. .should.equal(true)
  801. done()
  802. }
  803. )
  804. })
  805. })
  806. describe('when a doc has been added', function () {
  807. it('should send the structure update to the document updater', function (done) {
  808. this.docId = new ObjectId()
  809. this.changes = {
  810. newDocs: [
  811. { path: '/foo', docLines: 'a\nb', doc: { _id: this.docId } },
  812. ],
  813. newProject: { version: this.version },
  814. }
  815. const updates = [
  816. {
  817. type: 'add-doc',
  818. id: this.docId.toString(),
  819. pathname: '/foo',
  820. docLines: 'a\nb',
  821. url: undefined,
  822. hash: undefined,
  823. },
  824. ]
  825. this.handler.updateProjectStructure(
  826. this.project_id,
  827. this.projectHistoryId,
  828. this.user_id,
  829. this.changes,
  830. () => {
  831. this.request
  832. .calledWith({
  833. url: this.url,
  834. method: 'POST',
  835. json: {
  836. updates,
  837. userId: this.user_id,
  838. version: this.version,
  839. projectHistoryId: this.projectHistoryId,
  840. },
  841. })
  842. .should.equal(true)
  843. done()
  844. }
  845. )
  846. })
  847. })
  848. describe('when a file has been added', function () {
  849. it('should send the structure update to the document updater', function (done) {
  850. this.fileId = new ObjectId()
  851. this.changes = {
  852. newFiles: [
  853. {
  854. path: '/bar',
  855. url: 'filestore.example.com/file',
  856. file: { _id: this.fileId, hash: '12345' },
  857. },
  858. ],
  859. newProject: { version: this.version },
  860. }
  861. const updates = [
  862. {
  863. type: 'add-file',
  864. id: this.fileId.toString(),
  865. pathname: '/bar',
  866. url: 'filestore.example.com/file',
  867. docLines: undefined,
  868. hash: '12345',
  869. },
  870. ]
  871. this.handler.updateProjectStructure(
  872. this.project_id,
  873. this.projectHistoryId,
  874. this.user_id,
  875. this.changes,
  876. () => {
  877. this.request
  878. .calledWith({
  879. url: this.url,
  880. method: 'POST',
  881. json: {
  882. updates,
  883. userId: this.user_id,
  884. version: this.version,
  885. projectHistoryId: this.projectHistoryId,
  886. },
  887. })
  888. .should.equal(true)
  889. done()
  890. }
  891. )
  892. })
  893. })
  894. describe('when an entity has been deleted', function () {
  895. it('should end the structure update to the document updater', function (done) {
  896. this.docId = new ObjectId()
  897. this.changes = {
  898. oldDocs: [
  899. { path: '/foo', docLines: 'a\nb', doc: { _id: this.docId } },
  900. ],
  901. newProject: { version: this.version },
  902. }
  903. const updates = [
  904. {
  905. type: 'rename-doc',
  906. id: this.docId.toString(),
  907. pathname: '/foo',
  908. newPathname: '',
  909. },
  910. ]
  911. this.handler.updateProjectStructure(
  912. this.project_id,
  913. this.projectHistoryId,
  914. this.user_id,
  915. this.changes,
  916. () => {
  917. this.request
  918. .calledWith({
  919. url: this.url,
  920. method: 'POST',
  921. json: {
  922. updates,
  923. userId: this.user_id,
  924. version: this.version,
  925. projectHistoryId: this.projectHistoryId,
  926. },
  927. })
  928. .should.equal(true)
  929. done()
  930. }
  931. )
  932. })
  933. })
  934. describe('when a file is converted to a doc', function () {
  935. it('should send the delete first', function (done) {
  936. this.docId = new ObjectId()
  937. this.fileId = new ObjectId()
  938. this.changes = {
  939. oldFiles: [
  940. {
  941. path: '/foo.doc',
  942. url: 'filestore.example.com/file',
  943. file: { _id: this.fileId },
  944. },
  945. ],
  946. newDocs: [
  947. {
  948. path: '/foo.doc',
  949. docLines: 'hello there',
  950. doc: { _id: this.docId },
  951. },
  952. ],
  953. newProject: { version: this.version },
  954. }
  955. const updates = [
  956. {
  957. type: 'rename-file',
  958. id: this.fileId.toString(),
  959. pathname: '/foo.doc',
  960. newPathname: '',
  961. },
  962. {
  963. type: 'add-doc',
  964. id: this.docId.toString(),
  965. pathname: '/foo.doc',
  966. docLines: 'hello there',
  967. url: undefined,
  968. hash: undefined,
  969. },
  970. ]
  971. this.handler.updateProjectStructure(
  972. this.project_id,
  973. this.projectHistoryId,
  974. this.user_id,
  975. this.changes,
  976. () => {
  977. this.request.should.have.been.calledWith({
  978. url: this.url,
  979. method: 'POST',
  980. json: {
  981. updates,
  982. userId: this.user_id,
  983. version: this.version,
  984. projectHistoryId: this.projectHistoryId,
  985. },
  986. })
  987. done()
  988. }
  989. )
  990. })
  991. })
  992. describe('when the project version is missing', function () {
  993. it('should call the callback with an error', function () {
  994. this.docId = new ObjectId()
  995. this.changes = {
  996. oldDocs: [
  997. { path: '/foo', docLines: 'a\nb', doc: { _id: this.docId } },
  998. ],
  999. }
  1000. this.handler.updateProjectStructure(
  1001. this.project_id,
  1002. this.projectHistoryId,
  1003. this.user_id,
  1004. this.changes,
  1005. this.callback
  1006. )
  1007. this.callback
  1008. .calledWith(sinon.match.instanceOf(Error))
  1009. .should.equal(true)
  1010. const firstCallArgs = this.callback.args[0]
  1011. firstCallArgs[0].message.should.equal(
  1012. 'did not receive project version in changes'
  1013. )
  1014. })
  1015. })
  1016. })
  1017. })
  1018. })