ProjectDeleterTests.js 27 KB

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