ProjectDeleterTests.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862
  1. const modulePath = '../../../../app/src/Features/Project/ProjectDeleter'
  2. const SandboxedModule = require('sandboxed-module')
  3. const sinon = require('sinon')
  4. const { expect } = require('chai')
  5. const tk = require('timekeeper')
  6. const moment = require('moment')
  7. const { Project } = require('../helpers/models/Project')
  8. const { DeletedProject } = require('../helpers/models/DeletedProject')
  9. const { ObjectId, ReadPreference } = require('mongodb')
  10. const Errors = require('../../../../app/src/Features/Errors/Errors')
  11. describe('ProjectDeleter', function () {
  12. beforeEach(function () {
  13. tk.freeze(Date.now())
  14. this.ip = '192.170.18.1'
  15. this.project = dummyProject()
  16. this.user = {
  17. _id: '588f3ddae8ebc1bac07c9fa4',
  18. first_name: 'bjkdsjfk',
  19. features: {},
  20. }
  21. this.doc = {
  22. _id: '5bd975f54f62e803cb8a8fec',
  23. lines: ['a bunch of lines', 'for a sunny day', 'in London town'],
  24. ranges: {},
  25. project_id: '5cf9270b4eff6e186cf8b05e',
  26. }
  27. this.deletedProjects = [
  28. {
  29. _id: '5cf7f145c1401f0ca0eb1aaa',
  30. deleterData: {
  31. _id: '5cf7f145c1401f0ca0eb1aac',
  32. deletedAt: moment().subtract(95, 'days').toDate(),
  33. deleterId: '588f3ddae8ebc1bac07c9fa4',
  34. deleterIpAddress: '172.19.0.1',
  35. deletedProjectId: '5cf9270b4eff6e186cf8b05e',
  36. },
  37. project: {
  38. _id: '5cf9270b4eff6e186cf8b05e',
  39. overleaf: {
  40. history: {
  41. id: new ObjectId(),
  42. },
  43. },
  44. },
  45. },
  46. {
  47. _id: '5cf8eb11c1401f0ca0eb1ad7',
  48. deleterData: {
  49. _id: '5b74360c0fbe57011ae9938f',
  50. deletedAt: moment().subtract(95, 'days').toDate(),
  51. deleterId: '588f3ddae8ebc1bac07c9fa4',
  52. deleterIpAddress: '172.20.0.1',
  53. deletedProjectId: '5cf8f95a0c87371362c23919',
  54. },
  55. project: {
  56. _id: '5cf8f95a0c87371362c23919',
  57. },
  58. },
  59. ]
  60. this.DocumentUpdaterHandler = {
  61. promises: {
  62. flushProjectToMongoAndDelete: sinon.stub().resolves(),
  63. },
  64. }
  65. this.EditorRealTimeController = {
  66. emitToRoom: sinon.stub(),
  67. }
  68. this.TagsHandler = {
  69. promises: {
  70. removeProjectFromAllTags: sinon.stub().resolves(),
  71. },
  72. }
  73. this.CollaboratorsHandler = {
  74. promises: {
  75. removeUserFromAllProjects: sinon.stub().resolves(),
  76. },
  77. }
  78. this.CollaboratorsGetter = {
  79. promises: {
  80. getMemberIds: sinon
  81. .stub()
  82. .withArgs(this.project._id)
  83. .resolves(['member-id-1', 'member-id-2']),
  84. },
  85. }
  86. this.ProjectDetailsHandler = {
  87. promises: {
  88. generateUniqueName: sinon.stub().resolves(this.project.name),
  89. },
  90. }
  91. this.ProjectHelper = {
  92. calculateArchivedArray: sinon.stub(),
  93. }
  94. this.db = {
  95. deletedFiles: {
  96. indexExists: sinon.stub().resolves(false),
  97. deleteMany: sinon.stub(),
  98. },
  99. projects: {
  100. insertOne: sinon.stub().resolves(),
  101. },
  102. }
  103. this.DocstoreManager = {
  104. promises: {
  105. archiveProject: sinon.stub().resolves(),
  106. destroyProject: sinon.stub().resolves(),
  107. },
  108. }
  109. this.HistoryManager = {
  110. promises: {
  111. deleteProject: sinon.stub().resolves(),
  112. },
  113. }
  114. this.ProjectMock = sinon.mock(Project)
  115. this.DeletedProjectMock = sinon.mock(DeletedProject)
  116. this.FileStoreHandler = {
  117. promises: {
  118. deleteProject: sinon.stub().resolves(),
  119. },
  120. }
  121. this.TpdsUpdateSender = {
  122. promises: {
  123. deleteProject: sinon.stub().resolves(),
  124. },
  125. }
  126. this.Features = {
  127. hasFeature: sinon.stub().returns(true),
  128. }
  129. this.ChatApiHandler = {
  130. promises: {
  131. destroyProject: sinon.stub().resolves(),
  132. },
  133. }
  134. this.ProjectAuditLogEntry = {
  135. deleteMany: sinon.stub().returns({ exec: sinon.stub().resolves() }),
  136. }
  137. this.ProjectDeleter = SandboxedModule.require(modulePath, {
  138. requires: {
  139. '../../infrastructure/Features': this.Features,
  140. '../Editor/EditorRealTimeController': this.EditorRealTimeController,
  141. '../../models/Project': { Project },
  142. './ProjectHelper': this.ProjectHelper,
  143. '../../models/DeletedProject': { DeletedProject },
  144. '../DocumentUpdater/DocumentUpdaterHandler':
  145. this.DocumentUpdaterHandler,
  146. '../Tags/TagsHandler': this.TagsHandler,
  147. '../FileStore/FileStoreHandler': this.FileStoreHandler,
  148. '../ThirdPartyDataStore/TpdsUpdateSender': this.TpdsUpdateSender,
  149. '../Chat/ChatApiHandler': this.ChatApiHandler,
  150. '../Collaborators/CollaboratorsHandler': this.CollaboratorsHandler,
  151. '../Collaborators/CollaboratorsGetter': this.CollaboratorsGetter,
  152. '../Docstore/DocstoreManager': this.DocstoreManager,
  153. './ProjectDetailsHandler': this.ProjectDetailsHandler,
  154. '../../infrastructure/mongodb': {
  155. db: this.db,
  156. ObjectId,
  157. READ_PREFERENCE_SECONDARY: ReadPreference.secondaryPreferred.mode,
  158. },
  159. '../History/HistoryManager': this.HistoryManager,
  160. '../../models/ProjectAuditLogEntry': {
  161. ProjectAuditLogEntry: this.ProjectAuditLogEntry,
  162. },
  163. },
  164. })
  165. })
  166. afterEach(function () {
  167. tk.reset()
  168. this.DeletedProjectMock.restore()
  169. this.ProjectMock.restore()
  170. })
  171. describe('mark as deleted by external source', function () {
  172. beforeEach(function () {
  173. this.ProjectMock.expects('updateOne')
  174. .withArgs(
  175. { _id: this.project._id },
  176. { deletedByExternalDataSource: true }
  177. )
  178. .chain('exec')
  179. .resolves()
  180. })
  181. it('should update the project with the flag set to true', async function () {
  182. await this.ProjectDeleter.promises.markAsDeletedByExternalSource(
  183. this.project._id
  184. )
  185. this.ProjectMock.verify()
  186. })
  187. it('should tell the editor controler so users are notified', async function () {
  188. await this.ProjectDeleter.promises.markAsDeletedByExternalSource(
  189. this.project._id
  190. )
  191. expect(this.EditorRealTimeController.emitToRoom).to.have.been.calledWith(
  192. this.project._id,
  193. 'projectRenamedOrDeletedByExternalSource'
  194. )
  195. })
  196. })
  197. describe('unmarkAsDeletedByExternalSource', function () {
  198. beforeEach(async function () {
  199. this.ProjectMock.expects('updateOne')
  200. .withArgs(
  201. { _id: this.project._id },
  202. { deletedByExternalDataSource: false }
  203. )
  204. .chain('exec')
  205. .resolves()
  206. await this.ProjectDeleter.promises.unmarkAsDeletedByExternalSource(
  207. this.project._id
  208. )
  209. })
  210. it('should remove the flag from the project', function () {
  211. this.ProjectMock.verify()
  212. })
  213. })
  214. describe('deleteUsersProjects', function () {
  215. beforeEach(function () {
  216. this.projects = [dummyProject(), dummyProject()]
  217. this.ProjectMock.expects('find')
  218. .withArgs({ owner_ref: this.user._id })
  219. .chain('exec')
  220. .resolves(this.projects)
  221. for (const project of this.projects) {
  222. this.ProjectMock.expects('findOne')
  223. .withArgs({ _id: project._id })
  224. .chain('exec')
  225. .resolves(project)
  226. this.ProjectMock.expects('deleteOne')
  227. .withArgs({ _id: project._id })
  228. .chain('exec')
  229. .resolves()
  230. this.DeletedProjectMock.expects('updateOne')
  231. .withArgs(
  232. { 'deleterData.deletedProjectId': project._id },
  233. {
  234. project,
  235. deleterData: sinon.match.object,
  236. },
  237. { upsert: true }
  238. )
  239. .resolves()
  240. }
  241. })
  242. it('should delete all projects owned by the user', async function () {
  243. await this.ProjectDeleter.promises.deleteUsersProjects(this.user._id)
  244. this.ProjectMock.verify()
  245. this.DeletedProjectMock.verify()
  246. })
  247. it('should remove any collaboration from this user', async function () {
  248. await this.ProjectDeleter.promises.deleteUsersProjects(this.user._id)
  249. sinon.assert.calledWith(
  250. this.CollaboratorsHandler.promises.removeUserFromAllProjects,
  251. this.user._id
  252. )
  253. sinon.assert.calledOnce(
  254. this.CollaboratorsHandler.promises.removeUserFromAllProjects
  255. )
  256. })
  257. })
  258. describe('deleteProject', function () {
  259. beforeEach(function () {
  260. this.deleterData = {
  261. deletedAt: new Date(),
  262. deletedProjectId: this.project._id,
  263. deletedProjectOwnerId: this.project.owner_ref,
  264. deletedProjectCollaboratorIds: this.project.collaberator_refs,
  265. deletedProjectReadOnlyIds: this.project.readOnly_refs,
  266. deletedProjectReadWriteTokenAccessIds:
  267. this.project.tokenAccessReadAndWrite_refs,
  268. deletedProjectReadOnlyTokenAccessIds:
  269. this.project.tokenAccessReadOnly_refs,
  270. deletedProjectReadWriteToken: this.project.tokens.readAndWrite,
  271. deletedProjectReadOnlyToken: this.project.tokens.readOnly,
  272. deletedProjectOverleafId: this.project.overleaf.id,
  273. deletedProjectOverleafHistoryId: this.project.overleaf.history.id,
  274. deletedProjectLastUpdatedAt: this.project.lastUpdated,
  275. }
  276. this.ProjectMock.expects('findOne')
  277. .withArgs({ _id: this.project._id })
  278. .chain('exec')
  279. .resolves(this.project)
  280. })
  281. it('should save a DeletedProject with additional deleterData', async function () {
  282. this.deleterData.deleterIpAddress = this.ip
  283. this.deleterData.deleterId = this.user._id
  284. this.ProjectMock.expects('deleteOne').chain('exec').resolves()
  285. this.DeletedProjectMock.expects('updateOne')
  286. .withArgs(
  287. { 'deleterData.deletedProjectId': this.project._id },
  288. {
  289. project: this.project,
  290. deleterData: this.deleterData,
  291. },
  292. { upsert: true }
  293. )
  294. .resolves()
  295. await this.ProjectDeleter.promises.deleteProject(this.project._id, {
  296. deleterUser: this.user,
  297. ipAddress: this.ip,
  298. })
  299. this.DeletedProjectMock.verify()
  300. })
  301. it('should flushProjectToMongoAndDelete in doc updater', async function () {
  302. this.ProjectMock.expects('deleteOne').chain('exec').resolves()
  303. this.DeletedProjectMock.expects('updateOne').resolves()
  304. await this.ProjectDeleter.promises.deleteProject(this.project._id, {
  305. deleterUser: this.user,
  306. ipAddress: this.ip,
  307. })
  308. this.DocumentUpdaterHandler.promises.flushProjectToMongoAndDelete
  309. .calledWith(this.project._id)
  310. .should.equal(true)
  311. })
  312. it('should flush docs out of mongo', async function () {
  313. this.ProjectMock.expects('deleteOne').chain('exec').resolves()
  314. this.DeletedProjectMock.expects('updateOne').resolves()
  315. await this.ProjectDeleter.promises.deleteProject(this.project._id, {
  316. deleterUser: this.user,
  317. ipAddress: this.ip,
  318. })
  319. expect(
  320. this.DocstoreManager.promises.archiveProject
  321. ).to.have.been.calledWith(this.project._id)
  322. })
  323. it('should flush docs out of mongo and ignore errors', async function () {
  324. this.ProjectMock.expects('deleteOne').chain('exec').resolves()
  325. this.DeletedProjectMock.expects('updateOne').resolves()
  326. this.DocstoreManager.promises.archiveProject.rejects(new Error('foo'))
  327. await this.ProjectDeleter.promises.deleteProject(this.project._id, {
  328. deleterUser: this.user,
  329. ipAddress: this.ip,
  330. })
  331. })
  332. it('should removeProjectFromAllTags', async function () {
  333. this.ProjectMock.expects('deleteOne').chain('exec').resolves()
  334. this.DeletedProjectMock.expects('updateOne').resolves()
  335. await this.ProjectDeleter.promises.deleteProject(this.project._id)
  336. sinon.assert.calledWith(
  337. this.TagsHandler.promises.removeProjectFromAllTags,
  338. 'member-id-1',
  339. this.project._id
  340. )
  341. sinon.assert.calledWith(
  342. this.TagsHandler.promises.removeProjectFromAllTags,
  343. 'member-id-2',
  344. this.project._id
  345. )
  346. })
  347. it('should remove the project from Mongo', async function () {
  348. this.ProjectMock.expects('deleteOne')
  349. .withArgs({ _id: this.project._id })
  350. .chain('exec')
  351. .resolves()
  352. this.DeletedProjectMock.expects('updateOne').resolves()
  353. await this.ProjectDeleter.promises.deleteProject(this.project._id)
  354. this.ProjectMock.verify()
  355. })
  356. })
  357. describe('expireDeletedProjectsAfterDuration', function () {
  358. beforeEach(async function () {
  359. for (const deletedProject of this.deletedProjects) {
  360. this.ProjectMock.expects('findById')
  361. .withArgs(deletedProject.deleterData.deletedProjectId)
  362. .chain('exec')
  363. .resolves(null)
  364. }
  365. this.DeletedProjectMock.expects('find')
  366. .withArgs({
  367. 'deleterData.deletedAt': {
  368. $lt: new Date(moment().subtract(90, 'days')),
  369. },
  370. project: {
  371. $ne: null,
  372. },
  373. })
  374. .chain('exec')
  375. .resolves(this.deletedProjects)
  376. for (const deletedProject of this.deletedProjects) {
  377. this.DeletedProjectMock.expects('findOne')
  378. .withArgs({
  379. 'deleterData.deletedProjectId': deletedProject.project._id,
  380. })
  381. .chain('exec')
  382. .resolves(deletedProject)
  383. this.DeletedProjectMock.expects('updateOne')
  384. .withArgs(
  385. {
  386. _id: deletedProject._id,
  387. },
  388. {
  389. $set: {
  390. 'deleterData.deleterIpAddress': null,
  391. project: null,
  392. },
  393. }
  394. )
  395. .chain('exec')
  396. .resolves()
  397. }
  398. await this.ProjectDeleter.promises.expireDeletedProjectsAfterDuration()
  399. })
  400. it('should expire projects older than 90 days', function () {
  401. this.DeletedProjectMock.verify()
  402. })
  403. })
  404. describe('expireDeletedProject', function () {
  405. describe('on an inactive project', function () {
  406. beforeEach(async function () {
  407. this.ProjectMock.expects('findById')
  408. .withArgs(this.deletedProjects[0].deleterData.deletedProjectId)
  409. .chain('exec')
  410. .resolves(null)
  411. this.DeletedProjectMock.expects('updateOne')
  412. .withArgs(
  413. {
  414. _id: this.deletedProjects[0]._id,
  415. },
  416. {
  417. $set: {
  418. 'deleterData.deleterIpAddress': null,
  419. project: null,
  420. },
  421. }
  422. )
  423. .chain('exec')
  424. .resolves()
  425. this.DeletedProjectMock.expects('findOne')
  426. .withArgs({
  427. 'deleterData.deletedProjectId': this.deletedProjects[0].project._id,
  428. })
  429. .chain('exec')
  430. .resolves(this.deletedProjects[0])
  431. await this.ProjectDeleter.promises.expireDeletedProject(
  432. this.deletedProjects[0].project._id
  433. )
  434. })
  435. it('should find the specified deletedProject and remove its project and ip address', function () {
  436. this.DeletedProjectMock.verify()
  437. })
  438. it('should destroy the docs in docstore', function () {
  439. expect(
  440. this.DocstoreManager.promises.destroyProject
  441. ).to.have.been.calledWith(this.deletedProjects[0].project._id)
  442. })
  443. it('should delete the project in history', function () {
  444. expect(
  445. this.HistoryManager.promises.deleteProject
  446. ).to.have.been.calledWith(
  447. this.deletedProjects[0].project._id,
  448. this.deletedProjects[0].project.overleaf.history.id
  449. )
  450. })
  451. it('should destroy the files in filestore', function () {
  452. expect(
  453. this.FileStoreHandler.promises.deleteProject
  454. ).to.have.been.calledWith(this.deletedProjects[0].project._id)
  455. })
  456. it('should destroy the files in project-archiver', function () {
  457. expect(
  458. this.TpdsUpdateSender.promises.deleteProject
  459. ).to.have.been.calledWith({
  460. projectId: this.deletedProjects[0].project._id,
  461. })
  462. })
  463. it('should destroy the chat threads and messages', function () {
  464. expect(
  465. this.ChatApiHandler.promises.destroyProject
  466. ).to.have.been.calledWith(this.deletedProjects[0].project._id)
  467. })
  468. it('should delete audit logs', async function () {
  469. expect(this.ProjectAuditLogEntry.deleteMany).to.have.been.calledWith({
  470. projectId: this.deletedProjects[0].project._id,
  471. })
  472. })
  473. })
  474. describe('on an active project (from an incomplete delete)', function () {
  475. beforeEach(async function () {
  476. this.ProjectMock.expects('findById')
  477. .withArgs(this.deletedProjects[0].deleterData.deletedProjectId)
  478. .chain('exec')
  479. .resolves(this.deletedProjects[0].project)
  480. this.DeletedProjectMock.expects('deleteOne')
  481. .withArgs({
  482. 'deleterData.deletedProjectId': this.deletedProjects[0].project._id,
  483. })
  484. .chain('exec')
  485. .resolves()
  486. await this.ProjectDeleter.promises.expireDeletedProject(
  487. this.deletedProjects[0].project._id
  488. )
  489. })
  490. it('should delete the spurious deleted project record', function () {
  491. this.DeletedProjectMock.verify()
  492. })
  493. it('should not destroy the docs in docstore', function () {
  494. expect(this.DocstoreManager.promises.destroyProject).to.not.have.been
  495. .called
  496. })
  497. it('should not delete the project in history', function () {
  498. expect(this.HistoryManager.promises.deleteProject).to.not.have.been
  499. .called
  500. })
  501. it('should not destroy the files in filestore', function () {
  502. expect(this.FileStoreHandler.promises.deleteProject).to.not.have.been
  503. .called
  504. })
  505. it('should not destroy the files in project-archiver', function () {
  506. expect(this.TpdsUpdateSender.promises.deleteProject).to.not.have.been
  507. .called
  508. })
  509. it('should not destroy the chat threads and messages', function () {
  510. expect(this.ChatApiHandler.promises.destroyProject).to.not.have.been
  511. .called
  512. })
  513. })
  514. })
  515. describe('archiveProject', function () {
  516. beforeEach(function () {
  517. const archived = [ObjectId(this.user._id)]
  518. this.ProjectHelper.calculateArchivedArray.returns(archived)
  519. this.ProjectMock.expects('findOne')
  520. .withArgs({ _id: this.project._id })
  521. .chain('exec')
  522. .resolves(this.project)
  523. this.ProjectMock.expects('updateOne')
  524. .withArgs(
  525. { _id: this.project._id },
  526. {
  527. $set: { archived },
  528. $pull: { trashed: ObjectId(this.user._id) },
  529. }
  530. )
  531. .resolves()
  532. })
  533. it('should update the project', async function () {
  534. await this.ProjectDeleter.promises.archiveProject(
  535. this.project._id,
  536. this.user._id
  537. )
  538. this.ProjectMock.verify()
  539. })
  540. it('calculates the archived array', async function () {
  541. await this.ProjectDeleter.promises.archiveProject(
  542. this.project._id,
  543. this.user._id
  544. )
  545. expect(this.ProjectHelper.calculateArchivedArray).to.have.been.calledWith(
  546. this.project,
  547. this.user._id,
  548. 'ARCHIVE'
  549. )
  550. })
  551. })
  552. describe('unarchiveProject', function () {
  553. beforeEach(function () {
  554. const archived = [ObjectId(this.user._id)]
  555. this.ProjectHelper.calculateArchivedArray.returns(archived)
  556. this.ProjectMock.expects('findOne')
  557. .withArgs({ _id: this.project._id })
  558. .chain('exec')
  559. .resolves(this.project)
  560. this.ProjectMock.expects('updateOne')
  561. .withArgs({ _id: this.project._id }, { $set: { archived } })
  562. .resolves()
  563. })
  564. it('should update the project', async function () {
  565. await this.ProjectDeleter.promises.unarchiveProject(
  566. this.project._id,
  567. this.user._id
  568. )
  569. this.ProjectMock.verify()
  570. })
  571. it('calculates the archived array', async function () {
  572. await this.ProjectDeleter.promises.unarchiveProject(
  573. this.project._id,
  574. this.user._id
  575. )
  576. expect(this.ProjectHelper.calculateArchivedArray).to.have.been.calledWith(
  577. this.project,
  578. this.user._id,
  579. 'UNARCHIVE'
  580. )
  581. })
  582. })
  583. describe('trashProject', function () {
  584. beforeEach(function () {
  585. const archived = [ObjectId(this.user._id)]
  586. this.ProjectHelper.calculateArchivedArray.returns(archived)
  587. this.ProjectMock.expects('findOne')
  588. .withArgs({ _id: this.project._id })
  589. .chain('exec')
  590. .resolves(this.project)
  591. this.ProjectMock.expects('updateOne')
  592. .withArgs(
  593. { _id: this.project._id },
  594. {
  595. $addToSet: { trashed: ObjectId(this.user._id) },
  596. $set: { archived },
  597. }
  598. )
  599. .resolves()
  600. })
  601. it('should update the project', async function () {
  602. await this.ProjectDeleter.promises.trashProject(
  603. this.project._id,
  604. this.user._id
  605. )
  606. this.ProjectMock.verify()
  607. })
  608. it('unarchives the project', async function () {
  609. await this.ProjectDeleter.promises.trashProject(
  610. this.project._id,
  611. this.user._id
  612. )
  613. expect(this.ProjectHelper.calculateArchivedArray).to.have.been.calledWith(
  614. this.project,
  615. this.user._id,
  616. 'UNARCHIVE'
  617. )
  618. })
  619. })
  620. describe('untrashProject', function () {
  621. beforeEach(function () {
  622. this.ProjectMock.expects('findOne')
  623. .withArgs({ _id: this.project._id })
  624. .chain('exec')
  625. .resolves(this.project)
  626. this.ProjectMock.expects('updateOne')
  627. .withArgs(
  628. { _id: this.project._id },
  629. { $pull: { trashed: ObjectId(this.user._id) } }
  630. )
  631. .resolves()
  632. })
  633. it('should update the project', async function () {
  634. await this.ProjectDeleter.promises.untrashProject(
  635. this.project._id,
  636. this.user._id
  637. )
  638. this.ProjectMock.verify()
  639. })
  640. })
  641. describe('restoreProject', function () {
  642. beforeEach(function () {
  643. this.ProjectMock.expects('updateOne')
  644. .withArgs(
  645. {
  646. _id: this.project._id,
  647. },
  648. {
  649. $unset: { archived: true },
  650. }
  651. )
  652. .chain('exec')
  653. .resolves()
  654. })
  655. it('should unset the archive attribute', async function () {
  656. await this.ProjectDeleter.promises.restoreProject(this.project._id)
  657. })
  658. })
  659. describe('undeleteProject', function () {
  660. beforeEach(function () {
  661. this.unknownProjectId = ObjectId()
  662. this.purgedProjectId = ObjectId()
  663. this.deletedProject = {
  664. _id: 'deleted',
  665. project: this.project,
  666. deleterData: {
  667. deletedProjectId: this.project._id,
  668. deletedProjectOwnerId: this.project.owner_ref,
  669. },
  670. }
  671. this.purgedProject = {
  672. _id: 'purged',
  673. deleterData: {
  674. deletedProjectId: this.purgedProjectId,
  675. deletedProjectOwnerId: 'potato',
  676. },
  677. }
  678. this.DeletedProjectMock.expects('findOne')
  679. .withArgs({ 'deleterData.deletedProjectId': this.project._id })
  680. .chain('exec')
  681. .resolves(this.deletedProject)
  682. this.DeletedProjectMock.expects('findOne')
  683. .withArgs({ 'deleterData.deletedProjectId': this.purgedProjectId })
  684. .chain('exec')
  685. .resolves(this.purgedProject)
  686. this.DeletedProjectMock.expects('findOne')
  687. .withArgs({ 'deleterData.deletedProjectId': this.unknownProjectId })
  688. .chain('exec')
  689. .resolves(null)
  690. this.DeletedProjectMock.expects('deleteOne').chain('exec').resolves()
  691. })
  692. it('should return not found if the project does not exist', async function () {
  693. await expect(
  694. this.ProjectDeleter.promises.undeleteProject(
  695. this.unknownProjectId.toString()
  696. )
  697. ).to.be.rejectedWith(Errors.NotFoundError, 'project_not_found')
  698. })
  699. it('should return not found if the project has been expired', async function () {
  700. await expect(
  701. this.ProjectDeleter.promises.undeleteProject(
  702. this.purgedProjectId.toString()
  703. )
  704. ).to.be.rejectedWith(Errors.NotFoundError, 'project_too_old_to_restore')
  705. })
  706. it('should insert the project into the collection', async function () {
  707. await this.ProjectDeleter.promises.undeleteProject(this.project._id)
  708. sinon.assert.calledWith(
  709. this.db.projects.insertOne,
  710. sinon.match({
  711. _id: this.project._id,
  712. name: this.project.name,
  713. })
  714. )
  715. })
  716. it('should clear the archive bit', async function () {
  717. this.project.archived = true
  718. await this.ProjectDeleter.promises.undeleteProject(this.project._id)
  719. sinon.assert.calledWith(
  720. this.db.projects.insertOne,
  721. sinon.match({ archived: undefined })
  722. )
  723. })
  724. it('should generate a unique name for the project', async function () {
  725. await this.ProjectDeleter.promises.undeleteProject(this.project._id)
  726. sinon.assert.calledWith(
  727. this.ProjectDetailsHandler.promises.generateUniqueName,
  728. this.project.owner_ref
  729. )
  730. })
  731. it('should add a suffix to the project name', async function () {
  732. await this.ProjectDeleter.promises.undeleteProject(this.project._id)
  733. sinon.assert.calledWith(
  734. this.ProjectDetailsHandler.promises.generateUniqueName,
  735. this.project.owner_ref,
  736. this.project.name + ' (Restored)'
  737. )
  738. })
  739. it('should remove the DeletedProject', async function () {
  740. // need to change the mock just to include the methods we want
  741. this.DeletedProjectMock.restore()
  742. this.DeletedProjectMock = sinon.mock(DeletedProject)
  743. this.DeletedProjectMock.expects('findOne')
  744. .withArgs({ 'deleterData.deletedProjectId': this.project._id })
  745. .chain('exec')
  746. .resolves(this.deletedProject)
  747. this.DeletedProjectMock.expects('deleteOne')
  748. .withArgs({ _id: 'deleted' })
  749. .chain('exec')
  750. .resolves()
  751. await this.ProjectDeleter.promises.undeleteProject(this.project._id)
  752. this.DeletedProjectMock.verify()
  753. })
  754. })
  755. })
  756. function dummyProject() {
  757. return {
  758. _id: new ObjectId(),
  759. lastUpdated: new Date(),
  760. rootFolder: [],
  761. collaberator_refs: [new ObjectId(), new ObjectId()],
  762. readOnly_refs: [new ObjectId(), new ObjectId()],
  763. tokenAccessReadAndWrite_refs: [new ObjectId(), new ObjectId()],
  764. tokenAccessReadOnly_refs: [new ObjectId(), new ObjectId()],
  765. owner_ref: new ObjectId(),
  766. tokens: {
  767. readOnly: 'wombat',
  768. readAndWrite: 'potato',
  769. },
  770. overleaf: {
  771. id: 1234,
  772. history: {
  773. id: 5678,
  774. },
  775. },
  776. name: 'a very scientific analysis of spooky ghosts',
  777. }
  778. }