DocumentUpdaterHandlerTests.js 32 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126
  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. timeout: 30 * 1000,
  320. })
  321. .should.equal(true)
  322. })
  323. it('should call the callback with no error', function () {
  324. this.callback.calledWith(null).should.equal(true)
  325. })
  326. })
  327. describe('when the document updater API returns an error', function () {
  328. beforeEach(function () {
  329. this.request.callsArgWith(
  330. 1,
  331. new Error('something went wrong'),
  332. null,
  333. null
  334. )
  335. this.handler.setDocument(
  336. this.project_id,
  337. this.doc_id,
  338. this.user_id,
  339. this.lines,
  340. this.source,
  341. this.callback
  342. )
  343. })
  344. it('should return an error to the callback', function () {
  345. this.callback
  346. .calledWith(sinon.match.instanceOf(Error))
  347. .should.equal(true)
  348. })
  349. })
  350. describe('when the document updater returns a failure error code', function () {
  351. beforeEach(function () {
  352. this.request.callsArgWith(1, null, { statusCode: 500 }, '')
  353. this.handler.setDocument(
  354. this.project_id,
  355. this.doc_id,
  356. this.user_id,
  357. this.lines,
  358. this.source,
  359. this.callback
  360. )
  361. })
  362. it('should return the callback with an error', function () {
  363. this.callback
  364. .calledWith(
  365. sinon.match
  366. .instanceOf(Error)
  367. .and(
  368. sinon.match.has(
  369. 'message',
  370. 'document updater returned a failure status code: 500'
  371. )
  372. )
  373. )
  374. .should.equal(true)
  375. })
  376. })
  377. })
  378. describe('getDocument', function () {
  379. describe('successfully', function () {
  380. beforeEach(function () {
  381. this.body = {
  382. lines: this.lines,
  383. version: this.version,
  384. ops: (this.ops = ['mock-op-1', 'mock-op-2']),
  385. ranges: (this.ranges = { mock: 'ranges' }),
  386. }
  387. this.fromVersion = 2
  388. this.request.callsArgWith(1, null, { statusCode: 200 }, this.body)
  389. this.handler.getDocument(
  390. this.project_id,
  391. this.doc_id,
  392. this.fromVersion,
  393. this.callback
  394. )
  395. })
  396. it('should get the document from the document updater', function () {
  397. this.request
  398. .calledWith({
  399. url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}?fromVersion=${this.fromVersion}`,
  400. method: 'GET',
  401. json: true,
  402. timeout: 30 * 1000,
  403. })
  404. .should.equal(true)
  405. })
  406. it('should call the callback with the lines and version', function () {
  407. this.callback
  408. .calledWith(null, this.lines, this.version, this.ranges, this.ops)
  409. .should.equal(true)
  410. })
  411. })
  412. describe('when the document updater API returns an error', function () {
  413. beforeEach(function () {
  414. this.request.callsArgWith(
  415. 1,
  416. new Error('something went wrong'),
  417. null,
  418. null
  419. )
  420. this.handler.getDocument(
  421. this.project_id,
  422. this.doc_id,
  423. this.fromVersion,
  424. this.callback
  425. )
  426. })
  427. it('should return an error to the callback', function () {
  428. this.callback
  429. .calledWith(sinon.match.instanceOf(Error))
  430. .should.equal(true)
  431. })
  432. })
  433. describe('when the document updater returns a failure error code', function () {
  434. beforeEach(function () {
  435. this.request.callsArgWith(1, null, { statusCode: 500 }, '')
  436. this.handler.getDocument(
  437. this.project_id,
  438. this.doc_id,
  439. this.fromVersion,
  440. this.callback
  441. )
  442. })
  443. it('should return the callback with an error', function () {
  444. this.callback
  445. .calledWith(
  446. sinon.match
  447. .instanceOf(Error)
  448. .and(
  449. sinon.match.has(
  450. 'message',
  451. 'document updater returned a failure status code: 500'
  452. )
  453. )
  454. )
  455. .should.equal(true)
  456. })
  457. })
  458. })
  459. describe('getProjectDocsIfMatch', function () {
  460. beforeEach(function () {
  461. this.project_state_hash = '1234567890abcdef'
  462. })
  463. describe('successfully', function () {
  464. beforeEach(function () {
  465. this.doc0 = {
  466. _id: this.doc_id,
  467. lines: this.lines,
  468. v: this.version,
  469. }
  470. this.docs = [this.doc0, this.doc0, this.doc0]
  471. this.body = JSON.stringify(this.docs)
  472. this.request.post = sinon
  473. .stub()
  474. .callsArgWith(1, null, { statusCode: 200 }, this.body)
  475. this.handler.getProjectDocsIfMatch(
  476. this.project_id,
  477. this.project_state_hash,
  478. this.callback
  479. )
  480. })
  481. it('should get the documents from the document updater', function () {
  482. const url = `${this.settings.apis.documentupdater.url}/project/${this.project_id}/get_and_flush_if_old?state=${this.project_state_hash}`
  483. this.request.post.calledWith(url).should.equal(true)
  484. })
  485. it('should call the callback with the documents', function () {
  486. this.callback.calledWithExactly(null, this.docs).should.equal(true)
  487. })
  488. })
  489. describe('when the document updater API returns an error', function () {
  490. beforeEach(function () {
  491. this.request.post = sinon
  492. .stub()
  493. .callsArgWith(1, new Error('something went wrong'), null, null)
  494. this.handler.getProjectDocsIfMatch(
  495. this.project_id,
  496. this.project_state_hash,
  497. this.callback
  498. )
  499. })
  500. it('should return an error to the callback', function () {
  501. this.callback
  502. .calledWith(sinon.match.instanceOf(Error))
  503. .should.equal(true)
  504. })
  505. })
  506. describe('when the document updater returns a conflict error code', function () {
  507. beforeEach(function () {
  508. this.request.post = sinon
  509. .stub()
  510. .callsArgWith(1, null, { statusCode: 409 }, 'Conflict')
  511. this.handler.getProjectDocsIfMatch(
  512. this.project_id,
  513. this.project_state_hash,
  514. this.callback
  515. )
  516. })
  517. it('should return the callback with no documents', function () {
  518. this.callback.alwaysCalledWithExactly().should.equal(true)
  519. })
  520. })
  521. })
  522. describe('clearProjectState', function () {
  523. describe('successfully', function () {
  524. beforeEach(function () {
  525. this.request.callsArgWith(1, null, { statusCode: 200 })
  526. this.handler.clearProjectState(this.project_id, this.callback)
  527. })
  528. it('should clear the project state from the document updater', function () {
  529. this.request
  530. .calledWithMatch({
  531. url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/clearState`,
  532. method: 'POST',
  533. })
  534. .should.equal(true)
  535. })
  536. it('should call the callback', function () {
  537. this.callback.calledWith(null).should.equal(true)
  538. })
  539. })
  540. describe('when the document updater API returns an error', function () {
  541. beforeEach(function () {
  542. this.request.callsArgWith(
  543. 1,
  544. new Error('something went wrong'),
  545. null,
  546. null
  547. )
  548. this.handler.clearProjectState(this.project_id, this.callback)
  549. })
  550. it('should return an error to the callback', function () {
  551. this.callback
  552. .calledWith(sinon.match.instanceOf(Error))
  553. .should.equal(true)
  554. })
  555. })
  556. describe('when the document updater returns an error code', function () {
  557. beforeEach(function () {
  558. this.request.callsArgWith(1, null, { statusCode: 500 }, null)
  559. this.handler.clearProjectState(this.project_id, this.callback)
  560. })
  561. it('should return the callback with no documents', function () {
  562. this.callback
  563. .calledWith(
  564. sinon.match
  565. .instanceOf(Error)
  566. .and(
  567. sinon.match.has(
  568. 'message',
  569. 'document updater returned a failure status code: 500'
  570. )
  571. )
  572. )
  573. .should.equal(true)
  574. })
  575. })
  576. })
  577. describe('acceptChanges', function () {
  578. beforeEach(function () {
  579. this.change_id = 'mock-change-id-1'
  580. })
  581. describe('successfully', function () {
  582. beforeEach(function () {
  583. this.request.callsArgWith(1, null, { statusCode: 200 }, this.body)
  584. this.handler.acceptChanges(
  585. this.project_id,
  586. this.doc_id,
  587. [this.change_id],
  588. this.callback
  589. )
  590. })
  591. it('should accept the change in the document updater', function () {
  592. this.request
  593. .calledWith({
  594. url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}/change/accept`,
  595. json: {
  596. change_ids: [this.change_id],
  597. },
  598. method: 'POST',
  599. timeout: 30 * 1000,
  600. })
  601. .should.equal(true)
  602. })
  603. it('should call the callback', function () {
  604. this.callback.calledWith(null).should.equal(true)
  605. })
  606. })
  607. describe('when the document updater API returns an error', function () {
  608. beforeEach(function () {
  609. this.request.callsArgWith(
  610. 1,
  611. new Error('something went wrong'),
  612. null,
  613. null
  614. )
  615. this.handler.acceptChanges(
  616. this.project_id,
  617. this.doc_id,
  618. [this.change_id],
  619. this.callback
  620. )
  621. })
  622. it('should return an error to the callback', function () {
  623. this.callback
  624. .calledWith(sinon.match.instanceOf(Error))
  625. .should.equal(true)
  626. })
  627. })
  628. describe('when the document updater returns a failure error code', function () {
  629. beforeEach(function () {
  630. this.request.callsArgWith(1, null, { statusCode: 500 }, '')
  631. this.handler.acceptChanges(
  632. this.project_id,
  633. this.doc_id,
  634. [this.change_id],
  635. this.callback
  636. )
  637. })
  638. it('should return the callback with an error', function () {
  639. this.callback
  640. .calledWith(
  641. sinon.match
  642. .instanceOf(Error)
  643. .and(
  644. sinon.match.has(
  645. 'message',
  646. 'document updater returned a failure status code: 500'
  647. )
  648. )
  649. )
  650. .should.equal(true)
  651. })
  652. })
  653. })
  654. describe('deleteThread', function () {
  655. beforeEach(function () {
  656. this.thread_id = 'mock-thread-id-1'
  657. })
  658. describe('successfully', function () {
  659. beforeEach(function () {
  660. this.request.callsArgWith(1, null, { statusCode: 200 }, this.body)
  661. this.handler.deleteThread(
  662. this.project_id,
  663. this.doc_id,
  664. this.thread_id,
  665. this.callback
  666. )
  667. })
  668. it('should delete the thread in the document updater', function () {
  669. this.request
  670. .calledWithMatch({
  671. url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}/comment/${this.thread_id}`,
  672. method: 'DELETE',
  673. })
  674. .should.equal(true)
  675. })
  676. it('should call the callback', function () {
  677. this.callback.calledWith(null).should.equal(true)
  678. })
  679. })
  680. describe('when the document updater API returns an error', function () {
  681. beforeEach(function () {
  682. this.request.callsArgWith(
  683. 1,
  684. new Error('something went wrong'),
  685. null,
  686. null
  687. )
  688. this.handler.deleteThread(
  689. this.project_id,
  690. this.doc_id,
  691. this.thread_id,
  692. this.callback
  693. )
  694. })
  695. it('should return an error to the callback', function () {
  696. this.callback
  697. .calledWith(sinon.match.instanceOf(Error))
  698. .should.equal(true)
  699. })
  700. })
  701. describe('when the document updater returns a failure error code', function () {
  702. beforeEach(function () {
  703. this.request.callsArgWith(1, null, { statusCode: 500 }, '')
  704. this.handler.deleteThread(
  705. this.project_id,
  706. this.doc_id,
  707. this.thread_id,
  708. this.callback
  709. )
  710. })
  711. it('should return the callback with an error', function () {
  712. this.callback
  713. .calledWith(
  714. sinon.match
  715. .instanceOf(Error)
  716. .and(
  717. sinon.match.has(
  718. 'message',
  719. 'document updater returned a failure status code: 500'
  720. )
  721. )
  722. )
  723. .should.equal(true)
  724. })
  725. })
  726. })
  727. describe('updateProjectStructure ', function () {
  728. beforeEach(function () {
  729. this.user_id = 1234
  730. this.version = 999
  731. })
  732. describe('with project history disabled', function () {
  733. beforeEach(function () {
  734. this.settings.apis.project_history.sendProjectStructureOps = false
  735. this.handler.updateProjectStructure(
  736. this.project_id,
  737. this.projectHistoryId,
  738. this.user_id,
  739. {},
  740. this.callback
  741. )
  742. })
  743. it('does not make a web request', function () {
  744. this.request.called.should.equal(false)
  745. })
  746. it('calls the callback', function () {
  747. this.callback.called.should.equal(true)
  748. })
  749. })
  750. describe('with project history enabled', function () {
  751. beforeEach(function () {
  752. this.settings.apis.project_history.sendProjectStructureOps = true
  753. this.url = `${this.settings.apis.documentupdater.url}/project/${this.project_id}`
  754. this.request.callsArgWith(1, null, { statusCode: 204 }, '')
  755. })
  756. describe('when an entity has changed name', function () {
  757. it('should send the structure update to the document updater', function (done) {
  758. this.docIdA = new ObjectId()
  759. this.docIdB = new ObjectId()
  760. this.changes = {
  761. oldDocs: [
  762. { path: '/old_a', doc: { _id: this.docIdA } },
  763. { path: '/old_b', doc: { _id: this.docIdB } },
  764. ],
  765. // create new instances of the same ObjectIds so that == doesn't pass
  766. newDocs: [
  767. {
  768. path: '/old_a',
  769. doc: { _id: new ObjectId(this.docIdA.toString()) },
  770. },
  771. {
  772. path: '/new_b',
  773. doc: { _id: new ObjectId(this.docIdB.toString()) },
  774. },
  775. ],
  776. newProject: { version: this.version },
  777. }
  778. const updates = [
  779. {
  780. type: 'rename-doc',
  781. id: this.docIdB.toString(),
  782. pathname: '/old_b',
  783. newPathname: '/new_b',
  784. },
  785. ]
  786. this.handler.updateProjectStructure(
  787. this.project_id,
  788. this.projectHistoryId,
  789. this.user_id,
  790. this.changes,
  791. () => {
  792. this.request
  793. .calledWith({
  794. url: this.url,
  795. method: 'POST',
  796. json: {
  797. updates,
  798. userId: this.user_id,
  799. version: this.version,
  800. projectHistoryId: this.projectHistoryId,
  801. },
  802. timeout: 30 * 1000,
  803. })
  804. .should.equal(true)
  805. done()
  806. }
  807. )
  808. })
  809. })
  810. describe('when a doc has been added', function () {
  811. it('should send the structure update to the document updater', function (done) {
  812. this.docId = new ObjectId()
  813. this.changes = {
  814. newDocs: [
  815. { path: '/foo', docLines: 'a\nb', doc: { _id: this.docId } },
  816. ],
  817. newProject: { version: this.version },
  818. }
  819. const updates = [
  820. {
  821. type: 'add-doc',
  822. id: this.docId.toString(),
  823. pathname: '/foo',
  824. docLines: 'a\nb',
  825. url: undefined,
  826. hash: undefined,
  827. },
  828. ]
  829. this.handler.updateProjectStructure(
  830. this.project_id,
  831. this.projectHistoryId,
  832. this.user_id,
  833. this.changes,
  834. () => {
  835. this.request
  836. .calledWith({
  837. url: this.url,
  838. method: 'POST',
  839. json: {
  840. updates,
  841. userId: this.user_id,
  842. version: this.version,
  843. projectHistoryId: this.projectHistoryId,
  844. },
  845. timeout: 30 * 1000,
  846. })
  847. .should.equal(true)
  848. done()
  849. }
  850. )
  851. })
  852. })
  853. describe('when a file has been added', function () {
  854. it('should send the structure update to the document updater', function (done) {
  855. this.fileId = new ObjectId()
  856. this.changes = {
  857. newFiles: [
  858. {
  859. path: '/bar',
  860. url: 'filestore.example.com/file',
  861. file: { _id: this.fileId, hash: '12345' },
  862. },
  863. ],
  864. newProject: { version: this.version },
  865. }
  866. const updates = [
  867. {
  868. type: 'add-file',
  869. id: this.fileId.toString(),
  870. pathname: '/bar',
  871. url: 'filestore.example.com/file',
  872. docLines: undefined,
  873. hash: '12345',
  874. },
  875. ]
  876. this.handler.updateProjectStructure(
  877. this.project_id,
  878. this.projectHistoryId,
  879. this.user_id,
  880. this.changes,
  881. () => {
  882. this.request
  883. .calledWith({
  884. url: this.url,
  885. method: 'POST',
  886. json: {
  887. updates,
  888. userId: this.user_id,
  889. version: this.version,
  890. projectHistoryId: this.projectHistoryId,
  891. },
  892. timeout: 30 * 1000,
  893. })
  894. .should.equal(true)
  895. done()
  896. }
  897. )
  898. })
  899. })
  900. describe('when an entity has been deleted', function () {
  901. it('should end the structure update to the document updater', function (done) {
  902. this.docId = new ObjectId()
  903. this.changes = {
  904. oldDocs: [
  905. { path: '/foo', docLines: 'a\nb', doc: { _id: this.docId } },
  906. ],
  907. newProject: { version: this.version },
  908. }
  909. const updates = [
  910. {
  911. type: 'rename-doc',
  912. id: this.docId.toString(),
  913. pathname: '/foo',
  914. newPathname: '',
  915. },
  916. ]
  917. this.handler.updateProjectStructure(
  918. this.project_id,
  919. this.projectHistoryId,
  920. this.user_id,
  921. this.changes,
  922. () => {
  923. this.request
  924. .calledWith({
  925. url: this.url,
  926. method: 'POST',
  927. json: {
  928. updates,
  929. userId: this.user_id,
  930. version: this.version,
  931. projectHistoryId: this.projectHistoryId,
  932. },
  933. timeout: 30 * 1000,
  934. })
  935. .should.equal(true)
  936. done()
  937. }
  938. )
  939. })
  940. })
  941. describe('when a file is converted to a doc', function () {
  942. it('should send the delete first', function (done) {
  943. this.docId = new ObjectId()
  944. this.fileId = new ObjectId()
  945. this.changes = {
  946. oldFiles: [
  947. {
  948. path: '/foo.doc',
  949. url: 'filestore.example.com/file',
  950. file: { _id: this.fileId },
  951. },
  952. ],
  953. newDocs: [
  954. {
  955. path: '/foo.doc',
  956. docLines: 'hello there',
  957. doc: { _id: this.docId },
  958. },
  959. ],
  960. newProject: { version: this.version },
  961. }
  962. const updates = [
  963. {
  964. type: 'rename-file',
  965. id: this.fileId.toString(),
  966. pathname: '/foo.doc',
  967. newPathname: '',
  968. },
  969. {
  970. type: 'add-doc',
  971. id: this.docId.toString(),
  972. pathname: '/foo.doc',
  973. docLines: 'hello there',
  974. url: undefined,
  975. hash: undefined,
  976. },
  977. ]
  978. this.handler.updateProjectStructure(
  979. this.project_id,
  980. this.projectHistoryId,
  981. this.user_id,
  982. this.changes,
  983. () => {
  984. this.request.should.have.been.calledWith({
  985. url: this.url,
  986. method: 'POST',
  987. json: {
  988. updates,
  989. userId: this.user_id,
  990. version: this.version,
  991. projectHistoryId: this.projectHistoryId,
  992. },
  993. timeout: 30 * 1000,
  994. })
  995. done()
  996. }
  997. )
  998. })
  999. })
  1000. describe('when the project version is missing', function () {
  1001. it('should call the callback with an error', function () {
  1002. this.docId = new ObjectId()
  1003. this.changes = {
  1004. oldDocs: [
  1005. { path: '/foo', docLines: 'a\nb', doc: { _id: this.docId } },
  1006. ],
  1007. }
  1008. this.handler.updateProjectStructure(
  1009. this.project_id,
  1010. this.projectHistoryId,
  1011. this.user_id,
  1012. this.changes,
  1013. this.callback
  1014. )
  1015. this.callback
  1016. .calledWith(sinon.match.instanceOf(Error))
  1017. .should.equal(true)
  1018. const firstCallArgs = this.callback.args[0]
  1019. firstCallArgs[0].message.should.equal(
  1020. 'did not receive project version in changes'
  1021. )
  1022. })
  1023. })
  1024. })
  1025. })
  1026. })