DocumentUpdaterHandlerTests.js 57 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976
  1. const sinon = require('sinon')
  2. const SandboxedModule = require('sandboxed-module')
  3. const path = require('path')
  4. const { ObjectId } = require('mongodb-legacy')
  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. moduleImportSequence: [],
  30. }
  31. this.source = 'dropbox'
  32. this.callback = sinon.stub()
  33. this.handler = SandboxedModule.require(modulePath, {
  34. requires: {
  35. request: {
  36. defaults: () => {
  37. return this.request
  38. },
  39. },
  40. '@overleaf/settings': this.settings,
  41. '../Project/ProjectEntityHandler': this.projectEntityHandler,
  42. '../../models/Project': {
  43. Project: (this.Project = {}),
  44. },
  45. '../Project/ProjectGetter': (this.ProjectGetter = {
  46. getProjectWithoutLock: sinon.stub(),
  47. }),
  48. '../../Features/Project/ProjectLocator': {},
  49. '@overleaf/metrics': {
  50. Timer: class {
  51. done() {}
  52. },
  53. },
  54. '../FileStore/FileStoreHandler': {
  55. _buildUrl: sinon.stub().callsFake((projectId, fileId) => {
  56. return `http://filestore/project/${projectId}/file/${fileId}`
  57. }),
  58. },
  59. },
  60. })
  61. this.ProjectGetter.getProjectWithoutLock
  62. .withArgs(this.project_id)
  63. .yields(null, this.project)
  64. })
  65. describe('flushProjectToMongo', function () {
  66. describe('successfully', function () {
  67. beforeEach(function () {
  68. this.request.callsArgWith(1, null, { statusCode: 204 }, '')
  69. this.handler.flushProjectToMongo(this.project_id, this.callback)
  70. })
  71. it('should flush the document from the document updater', function () {
  72. this.request
  73. .calledWithMatch({
  74. url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/flush`,
  75. method: 'POST',
  76. })
  77. .should.equal(true)
  78. })
  79. it('should call the callback with no error', function () {
  80. this.callback.calledWith(null).should.equal(true)
  81. })
  82. })
  83. describe('when the document updater API returns an error', function () {
  84. beforeEach(function () {
  85. this.request.callsArgWith(
  86. 1,
  87. new Error('something went wrong'),
  88. null,
  89. null
  90. )
  91. this.handler.flushProjectToMongo(this.project_id, this.callback)
  92. })
  93. it('should return an error to the callback', function () {
  94. this.callback
  95. .calledWith(sinon.match.instanceOf(Error))
  96. .should.equal(true)
  97. })
  98. })
  99. describe('when the document updater returns a failure error code', function () {
  100. beforeEach(function () {
  101. this.request.callsArgWith(1, null, { statusCode: 500 }, '')
  102. this.handler.flushProjectToMongo(this.project_id, this.callback)
  103. })
  104. it('should return the callback with an error', function () {
  105. this.callback
  106. .calledWith(
  107. sinon.match
  108. .instanceOf(Error)
  109. .and(
  110. sinon.match.has(
  111. 'message',
  112. 'document updater returned a failure status code: 500'
  113. )
  114. )
  115. )
  116. .should.equal(true)
  117. })
  118. })
  119. })
  120. describe('flushProjectToMongoAndDelete', function () {
  121. describe('successfully', function () {
  122. beforeEach(function () {
  123. this.request.callsArgWith(1, null, { statusCode: 204 }, '')
  124. this.handler.flushProjectToMongoAndDelete(
  125. this.project_id,
  126. this.callback
  127. )
  128. })
  129. it('should delete the project from the document updater', function () {
  130. this.request
  131. .calledWithMatch({
  132. url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}`,
  133. method: 'DELETE',
  134. })
  135. .should.equal(true)
  136. })
  137. it('should call the callback with no error', function () {
  138. this.callback.calledWith(null).should.equal(true)
  139. })
  140. })
  141. describe('when the document updater API returns an error', function () {
  142. beforeEach(function () {
  143. this.request.callsArgWith(
  144. 1,
  145. new Error('something went wrong'),
  146. null,
  147. null
  148. )
  149. this.handler.flushProjectToMongoAndDelete(
  150. this.project_id,
  151. this.callback
  152. )
  153. })
  154. it('should return an error to the callback', function () {
  155. this.callback
  156. .calledWith(sinon.match.instanceOf(Error))
  157. .should.equal(true)
  158. })
  159. })
  160. describe('when the document updater returns a failure error code', function () {
  161. beforeEach(function () {
  162. this.request.callsArgWith(1, null, { statusCode: 500 }, '')
  163. this.handler.flushProjectToMongoAndDelete(
  164. this.project_id,
  165. this.callback
  166. )
  167. })
  168. it('should return the callback with an error', function () {
  169. this.callback
  170. .calledWith(
  171. sinon.match
  172. .instanceOf(Error)
  173. .and(
  174. sinon.match.has(
  175. 'message',
  176. 'document updater returned a failure status code: 500'
  177. )
  178. )
  179. )
  180. .should.equal(true)
  181. })
  182. })
  183. })
  184. describe('flushDocToMongo', function () {
  185. describe('successfully', function () {
  186. beforeEach(function () {
  187. this.request.callsArgWith(1, null, { statusCode: 204 }, '')
  188. this.handler.flushDocToMongo(
  189. this.project_id,
  190. this.doc_id,
  191. this.callback
  192. )
  193. })
  194. it('should flush the document from the document updater', function () {
  195. this.request
  196. .calledWithMatch({
  197. url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}/flush`,
  198. method: 'POST',
  199. })
  200. .should.equal(true)
  201. })
  202. it('should call the callback with no error', function () {
  203. this.callback.calledWith(null).should.equal(true)
  204. })
  205. })
  206. describe('when the document updater API returns an error', function () {
  207. beforeEach(function () {
  208. this.request.callsArgWith(
  209. 1,
  210. new Error('something went wrong'),
  211. null,
  212. null
  213. )
  214. this.handler.flushDocToMongo(
  215. this.project_id,
  216. this.doc_id,
  217. this.callback
  218. )
  219. })
  220. it('should return an error to the callback', function () {
  221. this.callback
  222. .calledWith(sinon.match.instanceOf(Error))
  223. .should.equal(true)
  224. })
  225. })
  226. describe('when the document updater returns a failure error code', function () {
  227. beforeEach(function () {
  228. this.request.callsArgWith(1, null, { statusCode: 500 }, '')
  229. this.handler.flushDocToMongo(
  230. this.project_id,
  231. this.doc_id,
  232. this.callback
  233. )
  234. })
  235. it('should return the callback with an error', function () {
  236. this.callback
  237. .calledWith(
  238. sinon.match
  239. .instanceOf(Error)
  240. .and(
  241. sinon.match.has(
  242. 'message',
  243. 'document updater returned a failure status code: 500'
  244. )
  245. )
  246. )
  247. .should.equal(true)
  248. })
  249. })
  250. })
  251. describe('deleteDoc', function () {
  252. describe('successfully', function () {
  253. beforeEach(function () {
  254. this.request.callsArgWith(1, null, { statusCode: 204 }, '')
  255. this.handler.deleteDoc(this.project_id, this.doc_id, this.callback)
  256. })
  257. it('should delete the document from the document updater', function () {
  258. this.request
  259. .calledWithMatch({
  260. url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}`,
  261. method: 'DELETE',
  262. })
  263. .should.equal(true)
  264. })
  265. it('should call the callback with no error', function () {
  266. this.callback.calledWith(null).should.equal(true)
  267. })
  268. })
  269. describe('when the document updater API returns an error', function () {
  270. beforeEach(function () {
  271. this.request.callsArgWith(
  272. 1,
  273. new Error('something went wrong'),
  274. null,
  275. null
  276. )
  277. this.handler.deleteDoc(this.project_id, this.doc_id, this.callback)
  278. })
  279. it('should return an error to the callback', function () {
  280. this.callback
  281. .calledWith(sinon.match.instanceOf(Error))
  282. .should.equal(true)
  283. })
  284. })
  285. describe('when the document updater returns a failure error code', function () {
  286. beforeEach(function () {
  287. this.request.callsArgWith(1, null, { statusCode: 500 }, '')
  288. this.handler.deleteDoc(this.project_id, this.doc_id, this.callback)
  289. })
  290. it('should return the callback with an error', function () {
  291. this.callback
  292. .calledWith(
  293. sinon.match
  294. .instanceOf(Error)
  295. .and(
  296. sinon.match.has(
  297. 'message',
  298. 'document updater returned a failure status code: 500'
  299. )
  300. )
  301. )
  302. .should.equal(true)
  303. })
  304. })
  305. describe("with 'ignoreFlushErrors' option", function () {
  306. beforeEach(function () {
  307. this.request.callsArgWith(1, null, { statusCode: 204 }, '')
  308. })
  309. it('when option is true, should send a `ignore_flush_errors=true` URL query to document-updater', function () {
  310. this.handler.deleteDoc(
  311. this.project_id,
  312. this.doc_id,
  313. true,
  314. this.callback
  315. )
  316. this.request
  317. .calledWithMatch({
  318. url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}?ignore_flush_errors=true`,
  319. method: 'DELETE',
  320. })
  321. .should.equal(true)
  322. })
  323. it("when option is false, shouldn't send any URL query to document-updater", function () {
  324. this.handler.deleteDoc(
  325. this.project_id,
  326. this.doc_id,
  327. false,
  328. this.callback
  329. )
  330. this.request
  331. .calledWithMatch({
  332. url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}`,
  333. method: 'DELETE',
  334. })
  335. .should.equal(true)
  336. })
  337. })
  338. })
  339. describe('setDocument', function () {
  340. describe('successfully', function () {
  341. beforeEach(function () {
  342. this.request.callsArgWith(1, null, { statusCode: 204 }, '')
  343. this.handler.setDocument(
  344. this.project_id,
  345. this.doc_id,
  346. this.user_id,
  347. this.lines,
  348. this.source,
  349. this.callback
  350. )
  351. })
  352. it('should set the document in the document updater', function () {
  353. this.request
  354. .calledWith({
  355. url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}`,
  356. json: {
  357. lines: this.lines,
  358. source: this.source,
  359. user_id: this.user_id,
  360. },
  361. method: 'POST',
  362. timeout: 30 * 1000,
  363. })
  364. .should.equal(true)
  365. })
  366. it('should call the callback with no error', function () {
  367. this.callback.calledWith(null).should.equal(true)
  368. })
  369. })
  370. describe('when the document updater API returns an error', function () {
  371. beforeEach(function () {
  372. this.request.callsArgWith(
  373. 1,
  374. new Error('something went wrong'),
  375. null,
  376. null
  377. )
  378. this.handler.setDocument(
  379. this.project_id,
  380. this.doc_id,
  381. this.user_id,
  382. this.lines,
  383. this.source,
  384. this.callback
  385. )
  386. })
  387. it('should return an error to the callback', function () {
  388. this.callback
  389. .calledWith(sinon.match.instanceOf(Error))
  390. .should.equal(true)
  391. })
  392. })
  393. describe('when the document updater returns a failure error code', function () {
  394. beforeEach(function () {
  395. this.request.callsArgWith(1, null, { statusCode: 500 }, '')
  396. this.handler.setDocument(
  397. this.project_id,
  398. this.doc_id,
  399. this.user_id,
  400. this.lines,
  401. this.source,
  402. this.callback
  403. )
  404. })
  405. it('should return the callback with an error', function () {
  406. this.callback
  407. .calledWith(
  408. sinon.match
  409. .instanceOf(Error)
  410. .and(
  411. sinon.match.has(
  412. 'message',
  413. 'document updater returned a failure status code: 500'
  414. )
  415. )
  416. )
  417. .should.equal(true)
  418. })
  419. })
  420. })
  421. describe('getComment', function () {
  422. describe('successfully', function () {
  423. beforeEach(function () {
  424. this.comment = {
  425. id: 'mock-comment-id-1',
  426. }
  427. this.body = this.comment
  428. this.request.callsArgWith(1, null, { statusCode: 200 }, this.body)
  429. this.handler.getComment(
  430. this.project_id,
  431. this.doc_id,
  432. this.comment.id,
  433. this.callback
  434. )
  435. })
  436. it('should get the comment from the document updater', function () {
  437. const url = `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}/comment/${this.comment.id}`
  438. this.request
  439. .calledWith({
  440. url,
  441. method: 'GET',
  442. json: true,
  443. timeout: 30 * 1000,
  444. })
  445. .should.equal(true)
  446. })
  447. it('should call the callback with the comment', function () {
  448. this.callback.calledWithExactly(null, this.comment).should.equal(true)
  449. })
  450. })
  451. })
  452. describe('getDocument', function () {
  453. describe('successfully', function () {
  454. beforeEach(function () {
  455. this.body = {
  456. lines: this.lines,
  457. version: this.version,
  458. ops: (this.ops = ['mock-op-1', 'mock-op-2']),
  459. ranges: (this.ranges = { mock: 'ranges' }),
  460. }
  461. this.fromVersion = 2
  462. this.request.callsArgWith(1, null, { statusCode: 200 }, this.body)
  463. this.handler.getDocument(
  464. this.project_id,
  465. this.doc_id,
  466. this.fromVersion,
  467. this.callback
  468. )
  469. })
  470. it('should get the document from the document updater', function () {
  471. this.request
  472. .calledWith({
  473. url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}?fromVersion=${this.fromVersion}`,
  474. method: 'GET',
  475. json: true,
  476. timeout: 30 * 1000,
  477. })
  478. .should.equal(true)
  479. })
  480. it('should call the callback with the lines and version', function () {
  481. this.callback
  482. .calledWith(null, this.lines, this.version, this.ranges, this.ops)
  483. .should.equal(true)
  484. })
  485. })
  486. describe('when the document updater API returns an error', function () {
  487. beforeEach(function () {
  488. this.request.callsArgWith(
  489. 1,
  490. new Error('something went wrong'),
  491. null,
  492. null
  493. )
  494. this.handler.getDocument(
  495. this.project_id,
  496. this.doc_id,
  497. this.fromVersion,
  498. this.callback
  499. )
  500. })
  501. it('should return an error to the callback', function () {
  502. this.callback
  503. .calledWith(sinon.match.instanceOf(Error))
  504. .should.equal(true)
  505. })
  506. })
  507. describe('when the document updater returns a failure error code', function () {
  508. beforeEach(function () {
  509. this.request.callsArgWith(1, null, { statusCode: 500 }, '')
  510. this.handler.getDocument(
  511. this.project_id,
  512. this.doc_id,
  513. this.fromVersion,
  514. this.callback
  515. )
  516. })
  517. it('should return the callback with an error', function () {
  518. this.callback
  519. .calledWith(
  520. sinon.match
  521. .instanceOf(Error)
  522. .and(
  523. sinon.match.has(
  524. 'message',
  525. 'document updater returned a failure status code: 500'
  526. )
  527. )
  528. )
  529. .should.equal(true)
  530. })
  531. })
  532. })
  533. describe('getProjectDocsIfMatch', function () {
  534. beforeEach(function () {
  535. this.project_state_hash = '1234567890abcdef'
  536. })
  537. describe('successfully', function () {
  538. beforeEach(function () {
  539. this.doc0 = {
  540. _id: this.doc_id,
  541. lines: this.lines,
  542. v: this.version,
  543. }
  544. this.docs = [this.doc0, this.doc0, this.doc0]
  545. this.body = JSON.stringify(this.docs)
  546. this.request.post = sinon
  547. .stub()
  548. .callsArgWith(1, null, { statusCode: 200 }, this.body)
  549. this.handler.getProjectDocsIfMatch(
  550. this.project_id,
  551. this.project_state_hash,
  552. this.callback
  553. )
  554. })
  555. it('should get the documents from the document updater', function () {
  556. const url = `${this.settings.apis.documentupdater.url}/project/${this.project_id}/get_and_flush_if_old?state=${this.project_state_hash}`
  557. this.request.post.calledWith(url).should.equal(true)
  558. })
  559. it('should call the callback with the documents', function () {
  560. this.callback.calledWithExactly(null, this.docs).should.equal(true)
  561. })
  562. })
  563. describe('when the document updater API returns an error', function () {
  564. beforeEach(function () {
  565. this.request.post = sinon
  566. .stub()
  567. .callsArgWith(1, new Error('something went wrong'), null, null)
  568. this.handler.getProjectDocsIfMatch(
  569. this.project_id,
  570. this.project_state_hash,
  571. this.callback
  572. )
  573. })
  574. it('should return an error to the callback', function () {
  575. this.callback
  576. .calledWith(sinon.match.instanceOf(Error))
  577. .should.equal(true)
  578. })
  579. })
  580. describe('when the document updater returns a conflict error code', function () {
  581. beforeEach(function () {
  582. this.request.post = sinon
  583. .stub()
  584. .callsArgWith(1, null, { statusCode: 409 }, 'Conflict')
  585. this.handler.getProjectDocsIfMatch(
  586. this.project_id,
  587. this.project_state_hash,
  588. this.callback
  589. )
  590. })
  591. it('should return the callback with no documents', function () {
  592. this.callback.alwaysCalledWithExactly().should.equal(true)
  593. })
  594. })
  595. })
  596. describe('clearProjectState', function () {
  597. describe('successfully', function () {
  598. beforeEach(function () {
  599. this.request.callsArgWith(1, null, { statusCode: 200 })
  600. this.handler.clearProjectState(this.project_id, this.callback)
  601. })
  602. it('should clear the project state from the document updater', function () {
  603. this.request
  604. .calledWithMatch({
  605. url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/clearState`,
  606. method: 'POST',
  607. })
  608. .should.equal(true)
  609. })
  610. it('should call the callback', function () {
  611. this.callback.calledWith(null).should.equal(true)
  612. })
  613. })
  614. describe('when the document updater API returns an error', function () {
  615. beforeEach(function () {
  616. this.request.callsArgWith(
  617. 1,
  618. new Error('something went wrong'),
  619. null,
  620. null
  621. )
  622. this.handler.clearProjectState(this.project_id, this.callback)
  623. })
  624. it('should return an error to the callback', function () {
  625. this.callback
  626. .calledWith(sinon.match.instanceOf(Error))
  627. .should.equal(true)
  628. })
  629. })
  630. describe('when the document updater returns an error code', function () {
  631. beforeEach(function () {
  632. this.request.callsArgWith(1, null, { statusCode: 500 }, null)
  633. this.handler.clearProjectState(this.project_id, this.callback)
  634. })
  635. it('should return the callback with no documents', 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('acceptChanges', function () {
  652. beforeEach(function () {
  653. this.change_id = 'mock-change-id-1'
  654. })
  655. describe('successfully', function () {
  656. beforeEach(function () {
  657. this.request.callsArgWith(1, null, { statusCode: 200 }, this.body)
  658. this.handler.acceptChanges(
  659. this.project_id,
  660. this.doc_id,
  661. [this.change_id],
  662. this.callback
  663. )
  664. })
  665. it('should accept the change in the document updater', function () {
  666. this.request
  667. .calledWith({
  668. url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}/change/accept`,
  669. json: {
  670. change_ids: [this.change_id],
  671. },
  672. method: 'POST',
  673. timeout: 30 * 1000,
  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.acceptChanges(
  690. this.project_id,
  691. this.doc_id,
  692. [this.change_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.acceptChanges(
  706. this.project_id,
  707. this.doc_id,
  708. [this.change_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('deleteThread', function () {
  729. beforeEach(function () {
  730. this.thread_id = 'mock-thread-id-1'
  731. })
  732. describe('successfully', function () {
  733. beforeEach(function () {
  734. this.request.callsArgWith(1, null, { statusCode: 200 }, this.body)
  735. this.handler.deleteThread(
  736. this.project_id,
  737. this.doc_id,
  738. this.thread_id,
  739. this.user_id,
  740. this.callback
  741. )
  742. })
  743. it('should delete the thread in the document updater', function () {
  744. this.request
  745. .calledWithMatch({
  746. url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}/comment/${this.thread_id}`,
  747. method: 'DELETE',
  748. })
  749. .should.equal(true)
  750. })
  751. it('should call the callback', function () {
  752. this.callback.calledWith(null).should.equal(true)
  753. })
  754. })
  755. describe('when the document updater API returns an error', function () {
  756. beforeEach(function () {
  757. this.request.callsArgWith(
  758. 1,
  759. new Error('something went wrong'),
  760. null,
  761. null
  762. )
  763. this.handler.deleteThread(
  764. this.project_id,
  765. this.doc_id,
  766. this.thread_id,
  767. this.user_id,
  768. this.callback
  769. )
  770. })
  771. it('should return an error to the callback', function () {
  772. this.callback
  773. .calledWith(sinon.match.instanceOf(Error))
  774. .should.equal(true)
  775. })
  776. })
  777. describe('when the document updater returns a failure error code', function () {
  778. beforeEach(function () {
  779. this.request.callsArgWith(1, null, { statusCode: 500 }, '')
  780. this.handler.deleteThread(
  781. this.project_id,
  782. this.doc_id,
  783. this.thread_id,
  784. this.user_id,
  785. this.callback
  786. )
  787. })
  788. it('should return the callback with an error', function () {
  789. this.callback
  790. .calledWith(
  791. sinon.match
  792. .instanceOf(Error)
  793. .and(
  794. sinon.match.has(
  795. 'message',
  796. 'document updater returned a failure status code: 500'
  797. )
  798. )
  799. )
  800. .should.equal(true)
  801. })
  802. })
  803. })
  804. describe('resolveThread', function () {
  805. beforeEach(function () {
  806. this.thread_id = 'mock-thread-id-1'
  807. })
  808. describe('successfully', function () {
  809. beforeEach(function () {
  810. this.request.callsArgWith(1, null, { statusCode: 200 }, this.body)
  811. this.handler.resolveThread(
  812. this.project_id,
  813. this.doc_id,
  814. this.thread_id,
  815. this.user_id,
  816. this.callback
  817. )
  818. })
  819. it('should resolve the thread in the document updater', function () {
  820. this.request
  821. .calledWithMatch({
  822. url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}/comment/${this.thread_id}/resolve`,
  823. method: 'POST',
  824. })
  825. .should.equal(true)
  826. })
  827. it('should call the callback', function () {
  828. this.callback.calledWith(null).should.equal(true)
  829. })
  830. })
  831. describe('when the document updater API returns an error', function () {
  832. beforeEach(function () {
  833. this.request.callsArgWith(
  834. 1,
  835. new Error('something went wrong'),
  836. null,
  837. null
  838. )
  839. this.handler.resolveThread(
  840. this.project_id,
  841. this.doc_id,
  842. this.thread_id,
  843. this.user_id,
  844. this.callback
  845. )
  846. })
  847. it('should return an error to the callback', function () {
  848. this.callback
  849. .calledWith(sinon.match.instanceOf(Error))
  850. .should.equal(true)
  851. })
  852. })
  853. describe('when the document updater returns a failure error code', function () {
  854. beforeEach(function () {
  855. this.request.callsArgWith(1, null, { statusCode: 500 }, '')
  856. this.handler.resolveThread(
  857. this.project_id,
  858. this.doc_id,
  859. this.thread_id,
  860. this.user_id,
  861. this.callback
  862. )
  863. })
  864. it('should return the callback with an error', function () {
  865. this.callback
  866. .calledWith(
  867. sinon.match
  868. .instanceOf(Error)
  869. .and(
  870. sinon.match.has(
  871. 'message',
  872. 'document updater returned a failure status code: 500'
  873. )
  874. )
  875. )
  876. .should.equal(true)
  877. })
  878. })
  879. })
  880. describe('reopenThread', function () {
  881. beforeEach(function () {
  882. this.thread_id = 'mock-thread-id-1'
  883. })
  884. describe('successfully', function () {
  885. beforeEach(function () {
  886. this.request.callsArgWith(1, null, { statusCode: 200 }, this.body)
  887. this.handler.reopenThread(
  888. this.project_id,
  889. this.doc_id,
  890. this.thread_id,
  891. this.user_id,
  892. this.callback
  893. )
  894. })
  895. it('should reopen the thread in the document updater', function () {
  896. this.request
  897. .calledWithMatch({
  898. url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}/comment/${this.thread_id}/reopen`,
  899. method: 'POST',
  900. })
  901. .should.equal(true)
  902. })
  903. it('should call the callback', function () {
  904. this.callback.calledWith(null).should.equal(true)
  905. })
  906. })
  907. describe('when the document updater API returns an error', function () {
  908. beforeEach(function () {
  909. this.request.callsArgWith(
  910. 1,
  911. new Error('something went wrong'),
  912. null,
  913. null
  914. )
  915. this.handler.reopenThread(
  916. this.project_id,
  917. this.doc_id,
  918. this.thread_id,
  919. this.user_id,
  920. this.callback
  921. )
  922. })
  923. it('should return an error to the callback', function () {
  924. this.callback
  925. .calledWith(sinon.match.instanceOf(Error))
  926. .should.equal(true)
  927. })
  928. })
  929. describe('when the document updater returns a failure error code', function () {
  930. beforeEach(function () {
  931. this.request.callsArgWith(1, null, { statusCode: 500 }, '')
  932. this.handler.reopenThread(
  933. this.project_id,
  934. this.doc_id,
  935. this.thread_id,
  936. this.user_id,
  937. this.callback
  938. )
  939. })
  940. it('should return the callback with an error', function () {
  941. this.callback
  942. .calledWith(
  943. sinon.match
  944. .instanceOf(Error)
  945. .and(
  946. sinon.match.has(
  947. 'message',
  948. 'document updater returned a failure status code: 500'
  949. )
  950. )
  951. )
  952. .should.equal(true)
  953. })
  954. })
  955. })
  956. describe('updateProjectStructure ', function () {
  957. beforeEach(function () {
  958. this.user_id = 1234
  959. this.version = 999
  960. })
  961. describe('with project history disabled', function () {
  962. beforeEach(function () {
  963. this.settings.apis.project_history.sendProjectStructureOps = false
  964. this.handler.updateProjectStructure(
  965. this.project_id,
  966. this.projectHistoryId,
  967. this.user_id,
  968. {},
  969. this.source,
  970. this.callback
  971. )
  972. })
  973. it('does not make a web request', function () {
  974. this.request.called.should.equal(false)
  975. })
  976. it('calls the callback', function () {
  977. this.callback.called.should.equal(true)
  978. })
  979. })
  980. describe('with project history enabled', function () {
  981. beforeEach(function () {
  982. this.settings.apis.project_history.sendProjectStructureOps = true
  983. this.url = `${this.settings.apis.documentupdater.url}/project/${this.project_id}`
  984. this.request.callsArgWith(1, null, { statusCode: 204 }, '')
  985. })
  986. describe('when an entity has changed name', function () {
  987. it('should send the structure update to the document updater', function (done) {
  988. this.docIdA = new ObjectId()
  989. this.docIdB = new ObjectId()
  990. this.changes = {
  991. oldDocs: [
  992. { path: '/old_a', doc: { _id: this.docIdA } },
  993. { path: '/old_b', doc: { _id: this.docIdB } },
  994. ],
  995. // create new instances of the same ObjectIds so that == doesn't pass
  996. newDocs: [
  997. {
  998. path: '/old_a',
  999. doc: { _id: new ObjectId(this.docIdA.toString()) },
  1000. },
  1001. {
  1002. path: '/new_b',
  1003. doc: { _id: new ObjectId(this.docIdB.toString()) },
  1004. },
  1005. ],
  1006. newProject: { version: this.version },
  1007. }
  1008. const updates = [
  1009. {
  1010. type: 'rename-doc',
  1011. id: this.docIdB.toString(),
  1012. pathname: '/old_b',
  1013. newPathname: '/new_b',
  1014. },
  1015. ]
  1016. this.handler.updateProjectStructure(
  1017. this.project_id,
  1018. this.projectHistoryId,
  1019. this.user_id,
  1020. this.changes,
  1021. this.source,
  1022. () => {
  1023. this.request
  1024. .calledWith({
  1025. url: this.url,
  1026. method: 'POST',
  1027. json: {
  1028. updates,
  1029. userId: this.user_id,
  1030. version: this.version,
  1031. projectHistoryId: this.projectHistoryId,
  1032. source: this.source,
  1033. },
  1034. timeout: 30 * 1000,
  1035. })
  1036. .should.equal(true)
  1037. done()
  1038. }
  1039. )
  1040. })
  1041. })
  1042. describe('when a doc has been added', function () {
  1043. it('should send the structure update to the document updater', function (done) {
  1044. this.docId = new ObjectId()
  1045. this.changes = {
  1046. newDocs: [
  1047. { path: '/foo', docLines: 'a\nb', doc: { _id: this.docId } },
  1048. ],
  1049. newProject: { version: this.version },
  1050. }
  1051. const updates = [
  1052. {
  1053. type: 'add-doc',
  1054. id: this.docId.toString(),
  1055. pathname: '/foo',
  1056. docLines: 'a\nb',
  1057. historyRangesSupport: false,
  1058. url: undefined,
  1059. hash: undefined,
  1060. ranges: undefined,
  1061. metadata: undefined,
  1062. createdBlob: false,
  1063. },
  1064. ]
  1065. this.handler.updateProjectStructure(
  1066. this.project_id,
  1067. this.projectHistoryId,
  1068. this.user_id,
  1069. this.changes,
  1070. this.source,
  1071. () => {
  1072. this.request.should.have.been.calledWith({
  1073. url: this.url,
  1074. method: 'POST',
  1075. json: {
  1076. updates,
  1077. userId: this.user_id,
  1078. version: this.version,
  1079. projectHistoryId: this.projectHistoryId,
  1080. source: this.source,
  1081. },
  1082. timeout: 30 * 1000,
  1083. })
  1084. done()
  1085. }
  1086. )
  1087. })
  1088. })
  1089. describe('when a file has been added', function () {
  1090. it('should send the structure update to the document updater', function (done) {
  1091. this.fileId = new ObjectId()
  1092. this.changes = {
  1093. newFiles: [
  1094. {
  1095. path: '/bar',
  1096. url: 'filestore.example.com/file',
  1097. file: { _id: this.fileId, hash: '12345' },
  1098. },
  1099. ],
  1100. newProject: { version: this.version },
  1101. }
  1102. const updates = [
  1103. {
  1104. type: 'add-file',
  1105. id: this.fileId.toString(),
  1106. pathname: '/bar',
  1107. url: 'filestore.example.com/file',
  1108. docLines: undefined,
  1109. historyRangesSupport: false,
  1110. hash: '12345',
  1111. ranges: undefined,
  1112. createdBlob: false,
  1113. metadata: undefined,
  1114. },
  1115. ]
  1116. this.handler.updateProjectStructure(
  1117. this.project_id,
  1118. this.projectHistoryId,
  1119. this.user_id,
  1120. this.changes,
  1121. this.source,
  1122. () => {
  1123. this.request.should.have.been.calledWith({
  1124. url: this.url,
  1125. method: 'POST',
  1126. json: {
  1127. updates,
  1128. userId: this.user_id,
  1129. version: this.version,
  1130. projectHistoryId: this.projectHistoryId,
  1131. source: this.source,
  1132. },
  1133. timeout: 30 * 1000,
  1134. })
  1135. done()
  1136. }
  1137. )
  1138. })
  1139. })
  1140. describe('when an entity has been deleted', function () {
  1141. it('should end the structure update to the document updater', function (done) {
  1142. this.docId = new ObjectId()
  1143. this.changes = {
  1144. oldDocs: [
  1145. { path: '/foo', docLines: 'a\nb', doc: { _id: this.docId } },
  1146. ],
  1147. newProject: { version: this.version },
  1148. }
  1149. const updates = [
  1150. {
  1151. type: 'rename-doc',
  1152. id: this.docId.toString(),
  1153. pathname: '/foo',
  1154. newPathname: '',
  1155. },
  1156. ]
  1157. this.handler.updateProjectStructure(
  1158. this.project_id,
  1159. this.projectHistoryId,
  1160. this.user_id,
  1161. this.changes,
  1162. this.source,
  1163. () => {
  1164. this.request.should.have.been.calledWith({
  1165. url: this.url,
  1166. method: 'POST',
  1167. json: {
  1168. updates,
  1169. userId: this.user_id,
  1170. version: this.version,
  1171. projectHistoryId: this.projectHistoryId,
  1172. source: this.source,
  1173. },
  1174. timeout: 30 * 1000,
  1175. })
  1176. done()
  1177. }
  1178. )
  1179. })
  1180. })
  1181. describe('when a file is converted to a doc', function () {
  1182. it('should send the delete first', function (done) {
  1183. this.docId = new ObjectId()
  1184. this.fileId = new ObjectId()
  1185. this.changes = {
  1186. oldFiles: [
  1187. {
  1188. path: '/foo.doc',
  1189. url: 'filestore.example.com/file',
  1190. file: { _id: this.fileId },
  1191. },
  1192. ],
  1193. newDocs: [
  1194. {
  1195. path: '/foo.doc',
  1196. docLines: 'hello there',
  1197. doc: { _id: this.docId },
  1198. },
  1199. ],
  1200. newProject: { version: this.version },
  1201. }
  1202. const updates = [
  1203. {
  1204. type: 'rename-file',
  1205. id: this.fileId.toString(),
  1206. pathname: '/foo.doc',
  1207. newPathname: '',
  1208. },
  1209. {
  1210. type: 'add-doc',
  1211. id: this.docId.toString(),
  1212. pathname: '/foo.doc',
  1213. docLines: 'hello there',
  1214. historyRangesSupport: false,
  1215. url: undefined,
  1216. hash: undefined,
  1217. ranges: undefined,
  1218. metadata: undefined,
  1219. createdBlob: false,
  1220. },
  1221. ]
  1222. this.handler.updateProjectStructure(
  1223. this.project_id,
  1224. this.projectHistoryId,
  1225. this.user_id,
  1226. this.changes,
  1227. this.source,
  1228. () => {
  1229. this.request.should.have.been.calledWith({
  1230. url: this.url,
  1231. method: 'POST',
  1232. json: {
  1233. updates,
  1234. userId: this.user_id,
  1235. version: this.version,
  1236. projectHistoryId: this.projectHistoryId,
  1237. source: this.source,
  1238. },
  1239. timeout: 30 * 1000,
  1240. })
  1241. done()
  1242. }
  1243. )
  1244. })
  1245. })
  1246. describe('when the project version is missing', function () {
  1247. it('should call the callback with an error', function () {
  1248. this.docId = new ObjectId()
  1249. this.changes = {
  1250. oldDocs: [
  1251. { path: '/foo', docLines: 'a\nb', doc: { _id: this.docId } },
  1252. ],
  1253. }
  1254. this.handler.updateProjectStructure(
  1255. this.project_id,
  1256. this.projectHistoryId,
  1257. this.user_id,
  1258. this.changes,
  1259. this.source,
  1260. this.callback
  1261. )
  1262. this.callback
  1263. .calledWith(sinon.match.instanceOf(Error))
  1264. .should.equal(true)
  1265. const firstCallArgs = this.callback.args[0]
  1266. firstCallArgs[0].message.should.equal(
  1267. 'did not receive project version in changes'
  1268. )
  1269. })
  1270. })
  1271. describe('when ranges are present', function () {
  1272. beforeEach(function () {
  1273. this.docId = new ObjectId()
  1274. this.ranges = {
  1275. changes: [
  1276. {
  1277. op: { p: 0, i: 'foo' },
  1278. metadata: { ts: '2024-01-01T00:00:00.000Z', user_id: 'user-1' },
  1279. },
  1280. {
  1281. op: { p: 7, d: ' baz' },
  1282. metadata: { ts: '2024-02-01T00:00:00.000Z', user_id: 'user-1' },
  1283. },
  1284. ],
  1285. comments: [
  1286. {
  1287. op: { p: 4, c: 'bar', t: 'comment-1' },
  1288. metadata: { resolved: false },
  1289. },
  1290. ],
  1291. }
  1292. this.changes = {
  1293. newDocs: [
  1294. {
  1295. path: '/foo',
  1296. docLines: 'foo\nbar',
  1297. doc: { _id: this.docId },
  1298. ranges: this.ranges,
  1299. },
  1300. ],
  1301. newProject: { version: this.version },
  1302. }
  1303. })
  1304. it('should forward ranges', function (done) {
  1305. const updates = [
  1306. {
  1307. type: 'add-doc',
  1308. id: this.docId.toString(),
  1309. pathname: '/foo',
  1310. docLines: 'foo\nbar',
  1311. historyRangesSupport: false,
  1312. url: undefined,
  1313. hash: undefined,
  1314. ranges: this.ranges,
  1315. metadata: undefined,
  1316. createdBlob: false,
  1317. },
  1318. ]
  1319. this.handler.updateProjectStructure(
  1320. this.project_id,
  1321. this.projectHistoryId,
  1322. this.user_id,
  1323. this.changes,
  1324. this.source,
  1325. () => {
  1326. this.request.should.have.been.calledWith({
  1327. url: this.url,
  1328. method: 'POST',
  1329. json: {
  1330. updates,
  1331. userId: this.user_id,
  1332. version: this.version,
  1333. projectHistoryId: this.projectHistoryId,
  1334. source: this.source,
  1335. },
  1336. timeout: 30 * 1000,
  1337. })
  1338. done()
  1339. }
  1340. )
  1341. })
  1342. it('should include flag when history ranges support is enabled', function (done) {
  1343. this.ProjectGetter.getProjectWithoutLock
  1344. .withArgs(this.project_id)
  1345. .yields(null, {
  1346. _id: this.project_id,
  1347. overleaf: { history: { rangesSupportEnabled: true } },
  1348. })
  1349. const updates = [
  1350. {
  1351. type: 'add-doc',
  1352. id: this.docId.toString(),
  1353. pathname: '/foo',
  1354. docLines: 'foo\nbar',
  1355. historyRangesSupport: true,
  1356. url: undefined,
  1357. hash: undefined,
  1358. ranges: this.ranges,
  1359. metadata: undefined,
  1360. createdBlob: false,
  1361. },
  1362. ]
  1363. this.handler.updateProjectStructure(
  1364. this.project_id,
  1365. this.projectHistoryId,
  1366. this.user_id,
  1367. this.changes,
  1368. this.source,
  1369. () => {
  1370. this.request.should.have.been.calledWith({
  1371. url: this.url,
  1372. method: 'POST',
  1373. json: {
  1374. updates,
  1375. userId: this.user_id,
  1376. version: this.version,
  1377. projectHistoryId: this.projectHistoryId,
  1378. source: this.source,
  1379. },
  1380. timeout: 30 * 1000,
  1381. })
  1382. done()
  1383. }
  1384. )
  1385. })
  1386. })
  1387. describe('with filestore disabled', function () {
  1388. beforeEach(function () {
  1389. this.settings.disableFilestore = true
  1390. })
  1391. it('should add files without URL and with createdBlob', function (done) {
  1392. this.fileId = new ObjectId()
  1393. this.changes = {
  1394. newFiles: [
  1395. {
  1396. path: '/bar',
  1397. url: 'filestore.example.com/file',
  1398. file: { _id: this.fileId, hash: '12345' },
  1399. },
  1400. ],
  1401. newProject: { version: this.version },
  1402. }
  1403. const updates = [
  1404. {
  1405. type: 'add-file',
  1406. id: this.fileId.toString(),
  1407. pathname: '/bar',
  1408. docLines: undefined,
  1409. historyRangesSupport: false,
  1410. url: undefined,
  1411. hash: '12345',
  1412. ranges: undefined,
  1413. createdBlob: true,
  1414. metadata: undefined,
  1415. },
  1416. ]
  1417. this.handler.updateProjectStructure(
  1418. this.project_id,
  1419. this.projectHistoryId,
  1420. this.user_id,
  1421. this.changes,
  1422. this.source,
  1423. () => {
  1424. this.request.should.have.been.calledWith({
  1425. url: this.url,
  1426. method: 'POST',
  1427. json: {
  1428. updates,
  1429. userId: this.user_id,
  1430. version: this.version,
  1431. projectHistoryId: this.projectHistoryId,
  1432. source: this.source,
  1433. },
  1434. timeout: 30 * 1000,
  1435. })
  1436. done()
  1437. }
  1438. )
  1439. })
  1440. it('should flag files without hash', function (done) {
  1441. this.fileId = new ObjectId()
  1442. this.changes = {
  1443. newFiles: [
  1444. {
  1445. path: '/bar',
  1446. url: 'filestore.example.com/file',
  1447. file: { _id: this.fileId },
  1448. },
  1449. ],
  1450. newProject: { version: this.version },
  1451. }
  1452. this.handler.updateProjectStructure(
  1453. this.project_id,
  1454. this.projectHistoryId,
  1455. this.user_id,
  1456. this.changes,
  1457. this.source,
  1458. err => {
  1459. err.should.match(/found file with missing hash/)
  1460. this.request.should.not.have.been.called
  1461. done()
  1462. }
  1463. )
  1464. })
  1465. })
  1466. })
  1467. })
  1468. describe('resyncProjectHistory', function () {
  1469. it('should add docs', function (done) {
  1470. const docId1 = new ObjectId()
  1471. const docId2 = new ObjectId()
  1472. const docs = [
  1473. { doc: { _id: docId1 }, path: 'main.tex' },
  1474. { doc: { _id: docId2 }, path: 'references.bib' },
  1475. ]
  1476. const files = []
  1477. this.request.yields(null, { statusCode: 200 })
  1478. const projectId = new ObjectId()
  1479. const projectHistoryId = 99
  1480. this.handler.resyncProjectHistory(
  1481. projectId,
  1482. projectHistoryId,
  1483. docs,
  1484. files,
  1485. {},
  1486. () => {
  1487. this.request.should.have.been.calledWith({
  1488. url: `${this.settings.apis.documentupdater.url}/project/${projectId}/history/resync`,
  1489. method: 'POST',
  1490. json: {
  1491. docs: [
  1492. { doc: docId1, path: 'main.tex' },
  1493. { doc: docId2, path: 'references.bib' },
  1494. ],
  1495. files: [],
  1496. projectHistoryId,
  1497. },
  1498. timeout: 6 * 60 * 1000,
  1499. })
  1500. done()
  1501. }
  1502. )
  1503. })
  1504. it('should add files', function (done) {
  1505. const fileId1 = new ObjectId()
  1506. const fileId2 = new ObjectId()
  1507. const fileId3 = new ObjectId()
  1508. const fileCreated2 = new Date()
  1509. const fileCreated3 = new Date()
  1510. const otherProjectId = new ObjectId().toString()
  1511. const files = [
  1512. { file: { _id: fileId1, hash: '42' }, path: '1.png' },
  1513. {
  1514. file: {
  1515. _id: fileId2,
  1516. hash: '1337',
  1517. created: fileCreated2,
  1518. linkedFileData: {
  1519. provider: 'references-provider',
  1520. },
  1521. },
  1522. path: '1.bib',
  1523. },
  1524. {
  1525. file: {
  1526. _id: fileId3,
  1527. hash: '21',
  1528. created: fileCreated3,
  1529. linkedFileData: {
  1530. provider: 'project_output_file',
  1531. build_id: '1234-abc',
  1532. clsiServerId: 'server-1',
  1533. source_project_id: otherProjectId,
  1534. source_output_file_path: 'foo/bar.txt',
  1535. },
  1536. },
  1537. path: 'bar.txt',
  1538. },
  1539. ]
  1540. const docs = []
  1541. this.request.yields(null, { statusCode: 200 })
  1542. const projectId = new ObjectId()
  1543. const projectHistoryId = 99
  1544. this.handler.resyncProjectHistory(
  1545. projectId,
  1546. projectHistoryId,
  1547. docs,
  1548. files,
  1549. {},
  1550. () => {
  1551. this.request.should.have.been.calledWith({
  1552. url: `${this.settings.apis.documentupdater.url}/project/${projectId}/history/resync`,
  1553. method: 'POST',
  1554. json: {
  1555. docs: [],
  1556. files: [
  1557. {
  1558. file: fileId1,
  1559. _hash: '42',
  1560. path: '1.png',
  1561. url: `http://filestore/project/${projectId}/file/${fileId1}`,
  1562. createdBlob: false,
  1563. metadata: undefined,
  1564. },
  1565. {
  1566. file: fileId2,
  1567. _hash: '1337',
  1568. path: '1.bib',
  1569. url: `http://filestore/project/${projectId}/file/${fileId2}`,
  1570. createdBlob: false,
  1571. metadata: {
  1572. importedAt: fileCreated2,
  1573. provider: 'references-provider',
  1574. },
  1575. },
  1576. {
  1577. file: fileId3,
  1578. _hash: '21',
  1579. path: 'bar.txt',
  1580. url: `http://filestore/project/${projectId}/file/${fileId3}`,
  1581. createdBlob: false,
  1582. metadata: {
  1583. importedAt: fileCreated3,
  1584. provider: 'project_output_file',
  1585. source_project_id: otherProjectId,
  1586. source_output_file_path: 'foo/bar.txt',
  1587. // build_id and clsiServerId are omitted
  1588. },
  1589. },
  1590. ],
  1591. projectHistoryId,
  1592. },
  1593. timeout: 6 * 60 * 1000,
  1594. })
  1595. done()
  1596. }
  1597. )
  1598. })
  1599. describe('with filestore disabled', function () {
  1600. beforeEach(function () {
  1601. this.settings.disableFilestore = true
  1602. })
  1603. it('should add files without URL', function (done) {
  1604. const fileId1 = new ObjectId()
  1605. const fileId2 = new ObjectId()
  1606. const fileId3 = new ObjectId()
  1607. const fileCreated2 = new Date()
  1608. const fileCreated3 = new Date()
  1609. const otherProjectId = new ObjectId().toString()
  1610. const files = [
  1611. { file: { _id: fileId1, hash: '42' }, path: '1.png' },
  1612. {
  1613. file: {
  1614. _id: fileId2,
  1615. hash: '1337',
  1616. created: fileCreated2,
  1617. linkedFileData: {
  1618. provider: 'references-provider',
  1619. },
  1620. },
  1621. path: '1.bib',
  1622. },
  1623. {
  1624. file: {
  1625. _id: fileId3,
  1626. hash: '21',
  1627. created: fileCreated3,
  1628. linkedFileData: {
  1629. provider: 'project_output_file',
  1630. build_id: '1234-abc',
  1631. clsiServerId: 'server-1',
  1632. source_project_id: otherProjectId,
  1633. source_output_file_path: 'foo/bar.txt',
  1634. },
  1635. },
  1636. path: 'bar.txt',
  1637. },
  1638. ]
  1639. const docs = []
  1640. this.request.yields(null, { statusCode: 200 })
  1641. const projectId = new ObjectId()
  1642. const projectHistoryId = 99
  1643. this.handler.resyncProjectHistory(
  1644. projectId,
  1645. projectHistoryId,
  1646. docs,
  1647. files,
  1648. {},
  1649. () => {
  1650. this.request.should.have.been.calledWith({
  1651. url: `${this.settings.apis.documentupdater.url}/project/${projectId}/history/resync`,
  1652. method: 'POST',
  1653. json: {
  1654. docs: [],
  1655. files: [
  1656. {
  1657. file: fileId1,
  1658. _hash: '42',
  1659. path: '1.png',
  1660. url: undefined,
  1661. createdBlob: true,
  1662. metadata: undefined,
  1663. },
  1664. {
  1665. file: fileId2,
  1666. _hash: '1337',
  1667. path: '1.bib',
  1668. url: undefined,
  1669. createdBlob: true,
  1670. metadata: {
  1671. importedAt: fileCreated2,
  1672. provider: 'references-provider',
  1673. },
  1674. },
  1675. {
  1676. file: fileId3,
  1677. _hash: '21',
  1678. path: 'bar.txt',
  1679. url: undefined,
  1680. createdBlob: true,
  1681. metadata: {
  1682. importedAt: fileCreated3,
  1683. provider: 'project_output_file',
  1684. source_project_id: otherProjectId,
  1685. source_output_file_path: 'foo/bar.txt',
  1686. // build_id and clsiServerId are omitted
  1687. },
  1688. },
  1689. ],
  1690. projectHistoryId,
  1691. },
  1692. timeout: 6 * 60 * 1000,
  1693. })
  1694. done()
  1695. }
  1696. )
  1697. })
  1698. it('should flag files with missing hashes', function (done) {
  1699. const fileId1 = new ObjectId()
  1700. const fileId2 = new ObjectId()
  1701. const fileId3 = new ObjectId()
  1702. const fileCreated2 = new Date()
  1703. const fileCreated3 = new Date()
  1704. const otherProjectId = new ObjectId().toString()
  1705. const files = [
  1706. { file: { _id: fileId1, hash: '42' }, path: '1.png' },
  1707. {
  1708. file: {
  1709. _id: fileId2,
  1710. created: fileCreated2,
  1711. linkedFileData: {
  1712. provider: 'references-provider',
  1713. },
  1714. },
  1715. path: '1.bib',
  1716. },
  1717. {
  1718. file: {
  1719. _id: fileId3,
  1720. hash: '21',
  1721. created: fileCreated3,
  1722. linkedFileData: {
  1723. provider: 'project_output_file',
  1724. build_id: '1234-abc',
  1725. clsiServerId: 'server-1',
  1726. source_project_id: otherProjectId,
  1727. source_output_file_path: 'foo/bar.txt',
  1728. },
  1729. },
  1730. path: 'bar.txt',
  1731. },
  1732. ]
  1733. const docs = []
  1734. this.request.yields(null, { statusCode: 200 })
  1735. const projectId = new ObjectId()
  1736. const projectHistoryId = 99
  1737. this.handler.resyncProjectHistory(
  1738. projectId,
  1739. projectHistoryId,
  1740. docs,
  1741. files,
  1742. {},
  1743. err => {
  1744. err.should.match(/found file with missing hash/)
  1745. this.request.should.not.have.been.called
  1746. done()
  1747. }
  1748. )
  1749. })
  1750. })
  1751. })
  1752. describe('appendToDocument', function () {
  1753. describe('successfully', function () {
  1754. beforeEach(function () {
  1755. this.body = {
  1756. rev: 1,
  1757. }
  1758. this.request.callsArgWith(1, null, { statusCode: 200 }, this.body)
  1759. this.handler.appendToDocument(
  1760. this.project_id,
  1761. this.doc_id,
  1762. this.user_id,
  1763. this.lines,
  1764. this.source,
  1765. this.callback
  1766. )
  1767. })
  1768. it('should append to the document in the document updater', function () {
  1769. this.request
  1770. .calledWith({
  1771. url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}/append`,
  1772. json: {
  1773. lines: this.lines,
  1774. source: this.source,
  1775. user_id: this.user_id,
  1776. },
  1777. method: 'POST',
  1778. timeout: 30 * 1000,
  1779. })
  1780. .should.equal(true)
  1781. })
  1782. it('should call the callback with no error', function () {
  1783. this.callback.calledWith(null).should.equal(true)
  1784. })
  1785. })
  1786. describe('when the document updater API returns an error', function () {
  1787. beforeEach(function () {
  1788. this.request.callsArgWith(
  1789. 1,
  1790. new Error('something went wrong'),
  1791. null,
  1792. null
  1793. )
  1794. this.handler.appendToDocument(
  1795. this.project_id,
  1796. this.doc_id,
  1797. this.user_id,
  1798. this.lines,
  1799. this.source,
  1800. this.callback
  1801. )
  1802. })
  1803. it('should return an error to the callback', function () {
  1804. this.callback
  1805. .calledWith(sinon.match.instanceOf(Error))
  1806. .should.equal(true)
  1807. })
  1808. })
  1809. describe('when the document updater returns a failure error code', function () {
  1810. beforeEach(function () {
  1811. this.request.callsArgWith(1, null, { statusCode: 500 }, '')
  1812. this.handler.appendToDocument(
  1813. this.project_id,
  1814. this.doc_id,
  1815. this.user_id,
  1816. this.lines,
  1817. this.source,
  1818. this.callback
  1819. )
  1820. })
  1821. it('should return the callback with an error', function () {
  1822. this.callback
  1823. .calledWith(
  1824. sinon.match
  1825. .instanceOf(Error)
  1826. .and(
  1827. sinon.match.has(
  1828. 'message',
  1829. 'document updater returned a failure status code: 500'
  1830. )
  1831. )
  1832. )
  1833. .should.equal(true)
  1834. })
  1835. })
  1836. })
  1837. })