ProjectDeleter.test.mjs 25 KB

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