ProjectDeleterTests.js 23 KB

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