ProjectDeleterTests.js 26 KB

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