DocstoreManager.test.mjs 17 KB

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