DeletingDocsTests.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. import mongodb from '../../../app/js/mongodb.js'
  2. import { expect } from 'chai'
  3. import DocstoreApp from './helpers/DocstoreApp.js'
  4. import Errors from '../../../app/js/Errors.js'
  5. import Settings from '@overleaf/settings'
  6. import { Storage } from '@google-cloud/storage'
  7. import { setTimeout as sleep } from 'node:timers/promises'
  8. import DocstoreClient from './helpers/DocstoreClient.js'
  9. const { db, ObjectId } = mongodb
  10. function deleteTestSuite(deleteDoc) {
  11. before(async function () {
  12. // Create buckets needed by the archiving part of these tests
  13. const storage = new Storage(Settings.docstore.gcs.endpoint)
  14. await storage.createBucket(Settings.docstore.bucket)
  15. await storage.createBucket(`${Settings.docstore.bucket}-deleted`)
  16. })
  17. after(async function () {
  18. // Tear down the buckets created above
  19. const storage = new Storage(Settings.docstore.gcs.endpoint)
  20. await storage.bucket(Settings.docstore.bucket).deleteFiles()
  21. await storage.bucket(Settings.docstore.bucket).delete()
  22. await storage.bucket(`${Settings.docstore.bucket}-deleted`).deleteFiles()
  23. await storage.bucket(`${Settings.docstore.bucket}-deleted`).delete()
  24. })
  25. beforeEach(async function () {
  26. this.project_id = new ObjectId()
  27. this.doc_id = new ObjectId()
  28. this.lines = ['original', 'lines']
  29. this.version = 42
  30. this.ranges = []
  31. await DocstoreApp.ensureRunning()
  32. await DocstoreClient.createDoc(
  33. this.project_id,
  34. this.doc_id,
  35. this.lines,
  36. this.version,
  37. this.ranges
  38. )
  39. })
  40. it('should show as not deleted on /deleted', async function () {
  41. const { res, body } = await DocstoreClient.isDocDeleted(
  42. this.project_id,
  43. this.doc_id
  44. )
  45. expect(res.status).to.equal(200)
  46. expect(body).to.have.property('deleted').to.equal(false)
  47. })
  48. describe('when the doc exists', function () {
  49. beforeEach(async function () {
  50. this.res = await deleteDoc(this.project_id, this.doc_id)
  51. })
  52. afterEach(async function () {
  53. await db.docs.deleteOne({ _id: this.doc_id })
  54. })
  55. it('should mark the doc as deleted on /deleted', async function () {
  56. const { res, body } = await DocstoreClient.isDocDeleted(
  57. this.project_id,
  58. this.doc_id
  59. )
  60. expect(res.status).to.equal(200)
  61. expect(body).to.have.property('deleted').to.equal(true)
  62. })
  63. it('should insert a deleted doc into the docs collection', async function () {
  64. const docs = await db.docs.find({ _id: this.doc_id }).toArray()
  65. docs[0]._id.should.deep.equal(this.doc_id)
  66. docs[0].lines.should.deep.equal(this.lines)
  67. docs[0].deleted.should.equal(true)
  68. })
  69. it('should not export the doc to s3', async function () {
  70. await sleep(1000)
  71. try {
  72. await DocstoreClient.getS3Doc(this.project_id, this.doc_id)
  73. } catch (error) {
  74. expect(error).to.be.instanceOf(Errors.NotFoundError)
  75. }
  76. })
  77. })
  78. describe('when archiveOnSoftDelete is enabled', function () {
  79. let archiveOnSoftDelete
  80. beforeEach('overwrite settings', function () {
  81. archiveOnSoftDelete = Settings.docstore.archiveOnSoftDelete
  82. Settings.docstore.archiveOnSoftDelete = true
  83. })
  84. afterEach('restore settings', function () {
  85. Settings.docstore.archiveOnSoftDelete = archiveOnSoftDelete
  86. })
  87. beforeEach('delete Doc', async function () {
  88. this.res = await deleteDoc(this.project_id, this.doc_id)
  89. })
  90. beforeEach(function waitForBackgroundFlush(done) {
  91. setTimeout(done, 500)
  92. })
  93. afterEach(function cleanupDoc(done) {
  94. db.docs.deleteOne({ _id: this.doc_id }, done)
  95. })
  96. it('should set the deleted flag in the doc', async function () {
  97. const doc = await db.docs.findOne({ _id: this.doc_id })
  98. expect(doc.deleted).to.equal(true)
  99. })
  100. it('should set inS3 and unset lines and ranges in the doc', async function () {
  101. const doc = await db.docs.findOne({ _id: this.doc_id })
  102. expect(doc.lines).to.not.exist
  103. expect(doc.ranges).to.not.exist
  104. expect(doc.inS3).to.equal(true)
  105. })
  106. it('should set the doc in s3 correctly', async function () {
  107. const s3doc = await DocstoreClient.getS3Doc(this.project_id, this.doc_id)
  108. expect(s3doc.lines).to.deep.equal(this.lines)
  109. expect(s3doc.ranges).to.deep.equal(this.ranges)
  110. })
  111. })
  112. describe('when the doc exists in another project', function () {
  113. const otherProjectId = new ObjectId()
  114. it('should show as not existing on /deleted', async function () {
  115. expect(DocstoreClient.isDocDeleted(otherProjectId, this.doc_id))
  116. .to.eventually.be.rejected.and.have.property('info')
  117. .to.contain({ status: 404 })
  118. })
  119. it('should return a 404 when trying to delete', async function () {
  120. expect(deleteDoc(otherProjectId, this.doc_id))
  121. .to.eventually.be.rejected.and.have.property('info')
  122. .to.contain({ status: 404 })
  123. })
  124. })
  125. describe('when the doc does not exist', function () {
  126. it('should show as not existing on /deleted', async function () {
  127. const missingDocId = new ObjectId()
  128. expect(DocstoreClient.isDocDeleted(this.project_id, missingDocId))
  129. .to.eventually.be.rejected.and.have.property('info')
  130. .to.contain({ status: 404 })
  131. })
  132. it('should return a 404', async function () {
  133. const missingDocId = new ObjectId()
  134. await expect(deleteDoc(this.project_id, missingDocId))
  135. .to.eventually.be.rejected.and.have.property('info')
  136. .to.contain({ status: 404 })
  137. })
  138. })
  139. }
  140. describe('Delete via PATCH', function () {
  141. deleteTestSuite(DocstoreClient.deleteDoc)
  142. describe('when providing a custom doc name in the delete request', function () {
  143. beforeEach(async function () {
  144. await DocstoreClient.deleteDocWithName(
  145. this.project_id,
  146. this.doc_id,
  147. 'wombat.tex'
  148. )
  149. })
  150. it('should insert the doc name into the docs collection', async function () {
  151. const docs = await db.docs.find({ _id: this.doc_id }).toArray()
  152. expect(docs[0].name).to.equal('wombat.tex')
  153. })
  154. })
  155. describe('when providing a custom deletedAt date in the delete request', function () {
  156. beforeEach('record date and delay', function (done) {
  157. this.deletedAt = new Date()
  158. setTimeout(done, 5)
  159. })
  160. beforeEach('perform deletion with past date', async function () {
  161. await DocstoreClient.deleteDocWithDate(
  162. this.project_id,
  163. this.doc_id,
  164. this.deletedAt
  165. )
  166. })
  167. it('should insert the date into the docs collection', async function () {
  168. const docs = await db.docs.find({ _id: this.doc_id }).toArray()
  169. expect(docs[0].deletedAt.toISOString()).to.equal(
  170. this.deletedAt.toISOString()
  171. )
  172. })
  173. })
  174. describe('when providing no doc name in the delete request', function () {
  175. it('should reject the request', function () {
  176. expect(DocstoreClient.deleteDocWithName(this.project_id, this.doc_id))
  177. .to.eventually.be.rejected.and.have.property('info')
  178. .to.contain({ status: 400 })
  179. })
  180. })
  181. describe('when providing no date in the delete request', function () {
  182. it('should reject the request', function () {
  183. expect(DocstoreClient.deleteDocWithDate(this.project_id, this.doc_id))
  184. .to.eventually.be.rejected.and.have.property('info')
  185. .to.contain({ status: 400 })
  186. })
  187. })
  188. describe('before deleting anything', function () {
  189. it('should show nothing in deleted docs response', async function () {
  190. const deletedDocs = await DocstoreClient.getAllDeletedDocs(
  191. this.project_id
  192. )
  193. expect(deletedDocs).to.deep.equal([])
  194. })
  195. })
  196. describe('when the doc gets a name on delete', function () {
  197. beforeEach(async function () {
  198. this.deletedAt = new Date()
  199. await DocstoreClient.deleteDocWithDate(
  200. this.project_id,
  201. this.doc_id,
  202. this.deletedAt
  203. )
  204. })
  205. it('should show the doc in deleted docs response', async function () {
  206. const deletedDocs = await DocstoreClient.getAllDeletedDocs(
  207. this.project_id
  208. )
  209. expect(deletedDocs).to.deep.equal([
  210. {
  211. _id: this.doc_id.toString(),
  212. name: 'main.tex',
  213. deletedAt: this.deletedAt.toISOString(),
  214. },
  215. ])
  216. })
  217. describe('after deleting multiple docs', function () {
  218. beforeEach('create doc2', async function () {
  219. this.doc_id2 = new ObjectId()
  220. await DocstoreClient.createDoc(
  221. this.project_id,
  222. this.doc_id2,
  223. this.lines,
  224. this.version,
  225. this.ranges
  226. )
  227. })
  228. beforeEach('delete doc2', async function () {
  229. this.deletedAt2 = new Date()
  230. await DocstoreClient.deleteDocWithDateAndName(
  231. this.project_id,
  232. this.doc_id2,
  233. this.deletedAt2,
  234. 'two.tex'
  235. )
  236. })
  237. beforeEach('create doc3', async function () {
  238. this.doc_id3 = new ObjectId()
  239. await DocstoreClient.createDoc(
  240. this.project_id,
  241. this.doc_id3,
  242. this.lines,
  243. this.version,
  244. this.ranges
  245. )
  246. })
  247. beforeEach('delete doc3', async function () {
  248. this.deletedAt3 = new Date()
  249. await DocstoreClient.deleteDocWithDateAndName(
  250. this.project_id,
  251. this.doc_id3,
  252. this.deletedAt3,
  253. 'three.tex'
  254. )
  255. })
  256. it('should show all the docs as deleted', async function () {
  257. const deletedDocs = await DocstoreClient.getAllDeletedDocs(
  258. this.project_id
  259. )
  260. expect(deletedDocs).to.deep.equal([
  261. {
  262. _id: this.doc_id3.toString(),
  263. name: 'three.tex',
  264. deletedAt: this.deletedAt3.toISOString(),
  265. },
  266. {
  267. _id: this.doc_id2.toString(),
  268. name: 'two.tex',
  269. deletedAt: this.deletedAt2.toISOString(),
  270. },
  271. {
  272. _id: this.doc_id.toString(),
  273. name: 'main.tex',
  274. deletedAt: this.deletedAt.toISOString(),
  275. },
  276. ])
  277. })
  278. describe('with one more than max_deleted_docs permits', function () {
  279. let maxDeletedDocsBefore
  280. beforeEach(function () {
  281. maxDeletedDocsBefore = Settings.max_deleted_docs
  282. Settings.max_deleted_docs = 2
  283. })
  284. afterEach(function () {
  285. Settings.max_deleted_docs = maxDeletedDocsBefore
  286. })
  287. it('should omit the first deleted doc', async function () {
  288. const deletedDocs = await DocstoreClient.getAllDeletedDocs(
  289. this.project_id
  290. )
  291. expect(deletedDocs).to.deep.equal([
  292. {
  293. _id: this.doc_id3.toString(),
  294. name: 'three.tex',
  295. deletedAt: this.deletedAt3.toISOString(),
  296. },
  297. {
  298. _id: this.doc_id2.toString(),
  299. name: 'two.tex',
  300. deletedAt: this.deletedAt2.toISOString(),
  301. },
  302. // dropped main.tex
  303. ])
  304. })
  305. })
  306. })
  307. })
  308. })
  309. describe("Destroying a project's documents", function () {
  310. beforeEach(async function () {
  311. this.project_id = new ObjectId()
  312. this.doc_id = new ObjectId()
  313. this.lines = ['original', 'lines']
  314. this.version = 42
  315. this.ranges = []
  316. await DocstoreApp.ensureRunning()
  317. await DocstoreClient.createDoc(
  318. this.project_id,
  319. this.doc_id,
  320. this.lines,
  321. this.version,
  322. this.ranges
  323. )
  324. })
  325. describe('when the doc exists', function () {
  326. beforeEach(async function () {
  327. await DocstoreClient.destroyAllDoc(this.project_id)
  328. })
  329. it('should remove the doc from the docs collection', async function () {
  330. const docs = await db.docs.find({ _id: this.doc_id }).toArray()
  331. expect(docs).to.deep.equal([])
  332. })
  333. })
  334. describe('when the doc is archived', function () {
  335. beforeEach(async function () {
  336. try {
  337. await DocstoreClient.archiveAllDoc(this.project_id)
  338. } catch (error) {
  339. // noop
  340. }
  341. await DocstoreClient.destroyAllDoc(this.project_id)
  342. })
  343. it('should remove the doc from the docs collection', async function () {
  344. const docs = await db.docs.find({ _id: this.doc_id }).toArray()
  345. expect(docs).to.deep.equal([])
  346. })
  347. it('should remove the doc contents from s3', async function () {
  348. try {
  349. await DocstoreClient.getS3Doc(this.project_id, this.doc_id)
  350. } catch (error) {
  351. expect(error).to.be.instanceOf(Errors.NotFoundError)
  352. }
  353. })
  354. })
  355. })