DocstoreManagerTests.js 18 KB

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