DocumentUpdaterHandlerTests.js 32 KB

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