DocstoreManagerTests.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631
  1. const sinon = require('sinon')
  2. const modulePath = '../../../../app/src/Features/Docstore/DocstoreManager'
  3. const SandboxedModule = require('sandboxed-module')
  4. const Errors = require('../../../../app/src/Features/Errors/Errors')
  5. const tk = require('timekeeper')
  6. describe('DocstoreManager', function () {
  7. beforeEach(function () {
  8. this.requestDefaults = sinon.stub().returns((this.request = sinon.stub()))
  9. this.DocstoreManager = SandboxedModule.require(modulePath, {
  10. requires: {
  11. request: {
  12. defaults: this.requestDefaults,
  13. },
  14. '@overleaf/settings': (this.settings = {
  15. apis: {
  16. docstore: {
  17. url: 'docstore.overleaf.com',
  18. },
  19. },
  20. }),
  21. },
  22. })
  23. this.requestDefaults.calledWith({ jar: false }).should.equal(true)
  24. this.project_id = 'project-id-123'
  25. this.doc_id = 'doc-id-123'
  26. this.callback = sinon.stub()
  27. })
  28. describe('deleteDoc', function () {
  29. describe('with a successful response code', function () {
  30. // for assertions on the deletedAt timestamp, we need to freeze the clock.
  31. before(function () {
  32. tk.freeze(Date.now())
  33. })
  34. after(function () {
  35. tk.reset()
  36. })
  37. beforeEach(function () {
  38. this.request.patch = sinon
  39. .stub()
  40. .callsArgWith(1, null, { statusCode: 204 }, '')
  41. this.DocstoreManager.deleteDoc(
  42. this.project_id,
  43. this.doc_id,
  44. 'wombat.tex',
  45. new Date(),
  46. this.callback
  47. )
  48. })
  49. it('should delete the doc in the docstore api', function () {
  50. this.request.patch
  51. .calledWith({
  52. url: `${this.settings.apis.docstore.url}/project/${this.project_id}/doc/${this.doc_id}`,
  53. json: { deleted: true, deletedAt: new Date(), name: 'wombat.tex' },
  54. timeout: 30 * 1000,
  55. })
  56. .should.equal(true)
  57. })
  58. it('should call the callback without an error', function () {
  59. this.callback.calledWith(null).should.equal(true)
  60. })
  61. })
  62. describe('with a failed response code', function () {
  63. beforeEach(function () {
  64. this.request.patch = sinon
  65. .stub()
  66. .callsArgWith(1, null, { statusCode: 500 }, '')
  67. this.DocstoreManager.deleteDoc(
  68. this.project_id,
  69. this.doc_id,
  70. 'main.tex',
  71. new Date(),
  72. this.callback
  73. )
  74. })
  75. it('should call the callback with an error', function () {
  76. this.callback
  77. .calledWith(
  78. sinon.match
  79. .instanceOf(Error)
  80. .and(
  81. sinon.match.has(
  82. 'message',
  83. 'docstore api responded with non-success code: 500'
  84. )
  85. )
  86. )
  87. .should.equal(true)
  88. })
  89. })
  90. describe('with a missing (404) response code', function () {
  91. beforeEach(function () {
  92. this.request.patch = sinon
  93. .stub()
  94. .callsArgWith(1, null, { statusCode: 404 }, '')
  95. this.DocstoreManager.deleteDoc(
  96. this.project_id,
  97. this.doc_id,
  98. 'main.tex',
  99. new Date(),
  100. this.callback
  101. )
  102. })
  103. it('should call the callback with an error', function () {
  104. this.callback
  105. .calledWith(
  106. sinon.match
  107. .instanceOf(Errors.NotFoundError)
  108. .and(
  109. sinon.match.has(
  110. 'message',
  111. 'tried to delete doc not in docstore'
  112. )
  113. )
  114. )
  115. .should.equal(true)
  116. })
  117. })
  118. })
  119. describe('updateDoc', function () {
  120. beforeEach(function () {
  121. this.lines = ['mock', 'doc', 'lines']
  122. this.rev = 5
  123. this.version = 42
  124. this.ranges = { mock: 'ranges' }
  125. this.modified = true
  126. })
  127. describe('with a successful response code', function () {
  128. beforeEach(function () {
  129. this.request.post = sinon
  130. .stub()
  131. .callsArgWith(
  132. 1,
  133. null,
  134. { statusCode: 204 },
  135. { modified: this.modified, rev: this.rev }
  136. )
  137. this.DocstoreManager.updateDoc(
  138. this.project_id,
  139. this.doc_id,
  140. this.lines,
  141. this.version,
  142. this.ranges,
  143. this.callback
  144. )
  145. })
  146. it('should update the doc in the docstore api', function () {
  147. this.request.post
  148. .calledWith({
  149. url: `${this.settings.apis.docstore.url}/project/${this.project_id}/doc/${this.doc_id}`,
  150. timeout: 30 * 1000,
  151. json: {
  152. lines: this.lines,
  153. version: this.version,
  154. ranges: this.ranges,
  155. },
  156. })
  157. .should.equal(true)
  158. })
  159. it('should call the callback with the modified status and revision', function () {
  160. this.callback
  161. .calledWith(null, this.modified, this.rev)
  162. .should.equal(true)
  163. })
  164. })
  165. describe('with a failed response code', function () {
  166. beforeEach(function () {
  167. this.request.post = sinon
  168. .stub()
  169. .callsArgWith(1, null, { statusCode: 500 }, '')
  170. this.DocstoreManager.updateDoc(
  171. this.project_id,
  172. this.doc_id,
  173. this.lines,
  174. this.version,
  175. this.ranges,
  176. this.callback
  177. )
  178. })
  179. it('should call the callback with an error', function () {
  180. this.callback
  181. .calledWith(
  182. sinon.match
  183. .instanceOf(Error)
  184. .and(
  185. sinon.match.has(
  186. 'message',
  187. 'docstore api responded with non-success code: 500'
  188. )
  189. )
  190. )
  191. .should.equal(true)
  192. })
  193. })
  194. })
  195. describe('getDoc', function () {
  196. beforeEach(function () {
  197. this.doc = {
  198. lines: (this.lines = ['mock', 'doc', 'lines']),
  199. rev: (this.rev = 5),
  200. version: (this.version = 42),
  201. ranges: (this.ranges = { mock: 'ranges' }),
  202. }
  203. })
  204. describe('with a successful response code', function () {
  205. beforeEach(function () {
  206. this.request.get = sinon
  207. .stub()
  208. .callsArgWith(1, null, { statusCode: 204 }, this.doc)
  209. this.DocstoreManager.getDoc(this.project_id, this.doc_id, this.callback)
  210. })
  211. it('should get the doc from the docstore api', function () {
  212. this.request.get.should.have.been.calledWith({
  213. url: `${this.settings.apis.docstore.url}/project/${this.project_id}/doc/${this.doc_id}`,
  214. timeout: 30 * 1000,
  215. json: true,
  216. })
  217. })
  218. it('should call the callback with the lines, version and rev', function () {
  219. this.callback
  220. .calledWith(null, this.lines, this.rev, this.version, this.ranges)
  221. .should.equal(true)
  222. })
  223. })
  224. describe('with a failed response code', function () {
  225. beforeEach(function () {
  226. this.request.get = sinon
  227. .stub()
  228. .callsArgWith(1, null, { statusCode: 500 }, '')
  229. this.DocstoreManager.getDoc(this.project_id, this.doc_id, this.callback)
  230. })
  231. it('should call the callback with an error', function () {
  232. this.callback
  233. .calledWith(
  234. sinon.match
  235. .instanceOf(Error)
  236. .and(
  237. sinon.match.has(
  238. 'message',
  239. 'docstore api responded with non-success code: 500'
  240. )
  241. )
  242. )
  243. .should.equal(true)
  244. })
  245. })
  246. describe('with include_deleted=true', function () {
  247. beforeEach(function () {
  248. this.request.get = sinon
  249. .stub()
  250. .callsArgWith(1, null, { statusCode: 204 }, this.doc)
  251. this.DocstoreManager.getDoc(
  252. this.project_id,
  253. this.doc_id,
  254. { include_deleted: true },
  255. this.callback
  256. )
  257. })
  258. it('should get the doc from the docstore api (including deleted)', function () {
  259. this.request.get.should.have.been.calledWith({
  260. url: `${this.settings.apis.docstore.url}/project/${this.project_id}/doc/${this.doc_id}`,
  261. qs: { include_deleted: 'true' },
  262. timeout: 30 * 1000,
  263. json: true,
  264. })
  265. })
  266. it('should call the callback with the lines, version and rev', function () {
  267. this.callback
  268. .calledWith(null, this.lines, this.rev, this.version, this.ranges)
  269. .should.equal(true)
  270. })
  271. })
  272. describe('with peek=true', function () {
  273. beforeEach(function () {
  274. this.request.get = sinon
  275. .stub()
  276. .callsArgWith(1, null, { statusCode: 204 }, this.doc)
  277. this.DocstoreManager.getDoc(
  278. this.project_id,
  279. this.doc_id,
  280. { peek: true },
  281. this.callback
  282. )
  283. })
  284. it('should call the docstore peek url', function () {
  285. this.request.get.should.have.been.calledWith({
  286. url: `${this.settings.apis.docstore.url}/project/${this.project_id}/doc/${this.doc_id}/peek`,
  287. timeout: 30 * 1000,
  288. json: true,
  289. })
  290. })
  291. })
  292. describe('with a missing (404) response code', function () {
  293. beforeEach(function () {
  294. this.request.get = sinon
  295. .stub()
  296. .callsArgWith(1, null, { statusCode: 404 }, '')
  297. this.DocstoreManager.getDoc(this.project_id, this.doc_id, this.callback)
  298. })
  299. it('should call the callback with an error', function () {
  300. this.callback
  301. .calledWith(
  302. sinon.match
  303. .instanceOf(Errors.NotFoundError)
  304. .and(sinon.match.has('message', 'doc not found in docstore'))
  305. )
  306. .should.equal(true)
  307. })
  308. })
  309. })
  310. describe('getAllDocs', function () {
  311. describe('with a successful response code', function () {
  312. beforeEach(function () {
  313. this.request.get = sinon
  314. .stub()
  315. .callsArgWith(
  316. 1,
  317. null,
  318. { statusCode: 204 },
  319. (this.docs = [{ _id: 'mock-doc-id' }])
  320. )
  321. this.DocstoreManager.getAllDocs(this.project_id, this.callback)
  322. })
  323. it('should get all the project docs in the docstore api', function () {
  324. this.request.get
  325. .calledWith({
  326. url: `${this.settings.apis.docstore.url}/project/${this.project_id}/doc`,
  327. timeout: 30 * 1000,
  328. json: true,
  329. })
  330. .should.equal(true)
  331. })
  332. it('should call the callback with the docs', function () {
  333. this.callback.calledWith(null, this.docs).should.equal(true)
  334. })
  335. })
  336. describe('with a failed response code', function () {
  337. beforeEach(function () {
  338. this.request.get = sinon
  339. .stub()
  340. .callsArgWith(1, null, { statusCode: 500 }, '')
  341. this.DocstoreManager.getAllDocs(this.project_id, this.callback)
  342. })
  343. it('should call the callback with an error', function () {
  344. this.callback
  345. .calledWith(
  346. sinon.match
  347. .instanceOf(Error)
  348. .and(
  349. sinon.match.has(
  350. 'message',
  351. 'docstore api responded with non-success code: 500'
  352. )
  353. )
  354. )
  355. .should.equal(true)
  356. })
  357. })
  358. })
  359. describe('getAllDeletedDocs', function () {
  360. describe('with a successful response code', function () {
  361. beforeEach(function (done) {
  362. this.callback.callsFake(done)
  363. this.docs = [{ _id: 'mock-doc-id', name: 'foo.tex' }]
  364. this.request.get = sinon
  365. .stub()
  366. .callsArgWith(1, null, { statusCode: 200 }, this.docs)
  367. this.DocstoreManager.getAllDeletedDocs(this.project_id, this.callback)
  368. })
  369. it('should get all the project docs in the docstore api', function () {
  370. this.request.get.should.have.been.calledWith({
  371. url: `${this.settings.apis.docstore.url}/project/${this.project_id}/doc-deleted`,
  372. timeout: 30 * 1000,
  373. json: true,
  374. })
  375. })
  376. it('should call the callback with the docs', function () {
  377. this.callback.should.have.been.calledWith(null, this.docs)
  378. })
  379. })
  380. describe('with an error', function () {
  381. beforeEach(function (done) {
  382. this.callback.callsFake(() => done())
  383. this.request.get = sinon
  384. .stub()
  385. .callsArgWith(1, new Error('connect failed'))
  386. this.DocstoreManager.getAllDocs(this.project_id, this.callback)
  387. })
  388. it('should call the callback with an error', function () {
  389. this.callback.should.have.been.calledWith(
  390. sinon.match
  391. .instanceOf(Error)
  392. .and(sinon.match.has('message', 'connect failed'))
  393. )
  394. })
  395. })
  396. describe('with a failed response code', function () {
  397. beforeEach(function (done) {
  398. this.callback.callsFake(() => done())
  399. this.request.get = sinon
  400. .stub()
  401. .callsArgWith(1, null, { statusCode: 500 })
  402. this.DocstoreManager.getAllDocs(this.project_id, this.callback)
  403. })
  404. it('should call the callback with an error', function () {
  405. this.callback.should.have.been.calledWith(
  406. sinon.match
  407. .instanceOf(Error)
  408. .and(
  409. sinon.match.has(
  410. 'message',
  411. 'docstore api responded with non-success code: 500'
  412. )
  413. )
  414. )
  415. })
  416. })
  417. })
  418. describe('getAllRanges', function () {
  419. describe('with a successful response code', function () {
  420. beforeEach(function () {
  421. this.request.get = sinon
  422. .stub()
  423. .callsArgWith(
  424. 1,
  425. null,
  426. { statusCode: 204 },
  427. (this.docs = [{ _id: 'mock-doc-id', ranges: 'mock-ranges' }])
  428. )
  429. this.DocstoreManager.getAllRanges(this.project_id, this.callback)
  430. })
  431. it('should get all the project doc ranges in the docstore api', function () {
  432. this.request.get
  433. .calledWith({
  434. url: `${this.settings.apis.docstore.url}/project/${this.project_id}/ranges`,
  435. timeout: 30 * 1000,
  436. json: true,
  437. })
  438. .should.equal(true)
  439. })
  440. it('should call the callback with the docs', function () {
  441. this.callback.calledWith(null, this.docs).should.equal(true)
  442. })
  443. })
  444. describe('with a failed response code', function () {
  445. beforeEach(function () {
  446. this.request.get = sinon
  447. .stub()
  448. .callsArgWith(1, null, { statusCode: 500 }, '')
  449. this.DocstoreManager.getAllRanges(this.project_id, this.callback)
  450. })
  451. it('should call the callback with an error', function () {
  452. this.callback
  453. .calledWith(
  454. sinon.match
  455. .instanceOf(Error)
  456. .and(
  457. sinon.match.has(
  458. 'message',
  459. 'docstore api responded with non-success code: 500'
  460. )
  461. )
  462. )
  463. .should.equal(true)
  464. })
  465. })
  466. })
  467. describe('archiveProject', function () {
  468. describe('with a successful response code', function () {
  469. beforeEach(function () {
  470. this.request.post = sinon
  471. .stub()
  472. .callsArgWith(1, null, { statusCode: 204 })
  473. this.DocstoreManager.archiveProject(this.project_id, this.callback)
  474. })
  475. it('should call the callback', function () {
  476. this.callback.called.should.equal(true)
  477. })
  478. })
  479. describe('with a failed response code', function () {
  480. beforeEach(function () {
  481. this.request.post = sinon
  482. .stub()
  483. .callsArgWith(1, null, { statusCode: 500 })
  484. this.DocstoreManager.archiveProject(this.project_id, this.callback)
  485. })
  486. it('should call the callback with an error', function () {
  487. this.callback
  488. .calledWith(
  489. sinon.match
  490. .instanceOf(Error)
  491. .and(
  492. sinon.match.has(
  493. 'message',
  494. 'docstore api responded with non-success code: 500'
  495. )
  496. )
  497. )
  498. .should.equal(true)
  499. })
  500. })
  501. })
  502. describe('unarchiveProject', function () {
  503. describe('with a successful response code', function () {
  504. beforeEach(function () {
  505. this.request.post = sinon
  506. .stub()
  507. .callsArgWith(1, null, { statusCode: 204 })
  508. this.DocstoreManager.unarchiveProject(this.project_id, this.callback)
  509. })
  510. it('should call the callback', function () {
  511. this.callback.called.should.equal(true)
  512. })
  513. })
  514. describe('with a failed response code', function () {
  515. beforeEach(function () {
  516. this.request.post = sinon
  517. .stub()
  518. .callsArgWith(1, null, { statusCode: 500 })
  519. this.DocstoreManager.unarchiveProject(this.project_id, this.callback)
  520. })
  521. it('should call the callback with an error', function () {
  522. this.callback
  523. .calledWith(
  524. sinon.match
  525. .instanceOf(Error)
  526. .and(
  527. sinon.match.has(
  528. 'message',
  529. 'docstore api responded with non-success code: 500'
  530. )
  531. )
  532. )
  533. .should.equal(true)
  534. })
  535. })
  536. })
  537. describe('destroyProject', function () {
  538. describe('with a successful response code', function () {
  539. beforeEach(function () {
  540. this.request.post = sinon
  541. .stub()
  542. .callsArgWith(1, null, { statusCode: 204 })
  543. this.DocstoreManager.destroyProject(this.project_id, this.callback)
  544. })
  545. it('should call the callback', function () {
  546. this.callback.called.should.equal(true)
  547. })
  548. })
  549. describe('with a failed response code', function () {
  550. beforeEach(function () {
  551. this.request.post = sinon
  552. .stub()
  553. .callsArgWith(1, null, { statusCode: 500 })
  554. this.DocstoreManager.destroyProject(this.project_id, this.callback)
  555. })
  556. it('should call the callback with an error', function () {
  557. this.callback
  558. .calledWith(
  559. sinon.match
  560. .instanceOf(Error)
  561. .and(
  562. sinon.match.has(
  563. 'message',
  564. 'docstore api responded with non-success code: 500'
  565. )
  566. )
  567. )
  568. .should.equal(true)
  569. })
  570. })
  571. })
  572. })