UserDeleterTests.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667
  1. const { expect } = require('chai')
  2. const sinon = require('sinon')
  3. const tk = require('timekeeper')
  4. const moment = require('moment')
  5. const SandboxedModule = require('sandboxed-module')
  6. const Errors = require('../../../../app/src/Features/Errors/Errors')
  7. const ObjectId = require('mongoose').Types.ObjectId
  8. const { User } = require('../helpers/models/User')
  9. const { DeletedUser } = require('../helpers/models/DeletedUser')
  10. const modulePath = '../../../../app/src/Features/User/UserDeleter.js'
  11. describe('UserDeleter', function () {
  12. beforeEach(function () {
  13. tk.freeze(Date.now())
  14. this.userId = new ObjectId()
  15. this.ipAddress = '1.2.3.4'
  16. this.UserMock = sinon.mock(User)
  17. this.DeletedUserMock = sinon.mock(DeletedUser)
  18. this.mockedUser = sinon.mock(
  19. new User({
  20. _id: this.userId,
  21. email: 'bob@bob.com',
  22. lastLoggedIn: Date.now() + 1000,
  23. signUpDate: Date.now() + 2000,
  24. loginCount: 10,
  25. overleaf: {
  26. id: 1234,
  27. },
  28. refered_users: ['wombat', 'potato'],
  29. refered_user_count: 2,
  30. referal_id: ['giraffe'],
  31. })
  32. )
  33. this.user = this.mockedUser.object
  34. this.NewsletterManager = {
  35. promises: {
  36. unsubscribe: sinon.stub().resolves(),
  37. },
  38. }
  39. this.ProjectDeleter = {
  40. promises: {
  41. deleteUsersProjects: sinon.stub().resolves(),
  42. },
  43. }
  44. this.SubscriptionHandler = {
  45. promises: {
  46. cancelSubscription: sinon.stub().resolves(),
  47. },
  48. }
  49. this.SubscriptionUpdater = {
  50. promises: {
  51. removeUserFromAllGroups: sinon.stub().resolves(),
  52. },
  53. }
  54. this.SubscriptionLocator = {
  55. promises: {
  56. getUsersSubscription: sinon.stub().resolves(),
  57. },
  58. }
  59. this.UserMembershipsHandler = {
  60. promises: {
  61. removeUserFromAllEntities: sinon.stub().resolves(),
  62. },
  63. }
  64. this.UserSessionsManager = {
  65. promises: {
  66. removeSessionsFromRedis: sinon.stub().resolves(),
  67. },
  68. }
  69. this.InstitutionsApi = {
  70. promises: {
  71. deleteAffiliations: sinon.stub().resolves(),
  72. },
  73. }
  74. this.UserAuditLogEntry = {
  75. deleteMany: sinon.stub().returns({ exec: sinon.stub().resolves() }),
  76. }
  77. this.Modules = {
  78. promises: { hooks: { fire: sinon.stub().resolves() } },
  79. }
  80. this.Feedback = {
  81. deleteMany: sinon.stub().returns({ exec: sinon.stub().resolves() }),
  82. }
  83. this.OnboardingDataCollectionManager = {
  84. deleteOnboardingDataCollection: sinon.stub().resolves(),
  85. }
  86. this.EmailHandler = {
  87. promises: {
  88. sendEmail: sinon.stub().resolves(),
  89. },
  90. }
  91. this.UserAuditLogHandler = {
  92. promises: {
  93. addEntry: sinon.stub().resolves(),
  94. },
  95. }
  96. this.UserDeleter = SandboxedModule.require(modulePath, {
  97. requires: {
  98. '../../models/User': { User },
  99. '../../models/DeletedUser': { DeletedUser },
  100. '../../models/Feedback': { Feedback: this.Feedback },
  101. '../Newsletter/NewsletterManager': this.NewsletterManager,
  102. './UserSessionsManager': this.UserSessionsManager,
  103. '../Subscription/SubscriptionHandler': this.SubscriptionHandler,
  104. '../Subscription/SubscriptionUpdater': this.SubscriptionUpdater,
  105. '../Subscription/SubscriptionLocator': this.SubscriptionLocator,
  106. '../UserMembership/UserMembershipsHandler': this.UserMembershipsHandler,
  107. '../Project/ProjectDeleter': this.ProjectDeleter,
  108. '../Institutions/InstitutionsAPI': this.InstitutionsApi,
  109. '../../models/UserAuditLogEntry': {
  110. UserAuditLogEntry: this.UserAuditLogEntry,
  111. },
  112. './UserAuditLogHandler': this.UserAuditLogHandler,
  113. '../../infrastructure/Modules': this.Modules,
  114. '../OnboardingDataCollection/OnboardingDataCollectionManager':
  115. this.OnboardingDataCollectionManager,
  116. '../Email/EmailHandler': this.EmailHandler,
  117. },
  118. })
  119. })
  120. afterEach(function () {
  121. this.DeletedUserMock.restore()
  122. this.UserMock.restore()
  123. this.mockedUser.restore()
  124. tk.reset()
  125. })
  126. describe('deleteUser', function () {
  127. beforeEach(function () {
  128. this.UserMock.expects('findById')
  129. .withArgs(this.userId)
  130. .chain('exec')
  131. .resolves(this.user)
  132. })
  133. describe('when the user can be deleted', function () {
  134. beforeEach(function () {
  135. this.deletedUser = {
  136. user: this.user,
  137. deleterData: {
  138. deletedAt: new Date(),
  139. deletedUserId: this.userId,
  140. deleterIpAddress: this.ipAddress,
  141. deleterId: undefined,
  142. deletedUserLastLoggedIn: this.user.lastLoggedIn,
  143. deletedUserSignUpDate: this.user.signUpDate,
  144. deletedUserLoginCount: this.user.loginCount,
  145. deletedUserReferralId: this.user.referal_id,
  146. deletedUserReferredUsers: this.user.refered_users,
  147. deletedUserReferredUserCount: this.user.refered_user_count,
  148. deletedUserOverleafId: this.user.overleaf.id,
  149. },
  150. }
  151. })
  152. describe('when only the ip address is passed', function () {
  153. beforeEach(function () {
  154. this.DeletedUserMock.expects('updateOne')
  155. .withArgs(
  156. { 'deleterData.deletedUserId': this.userId },
  157. this.deletedUser,
  158. { upsert: true }
  159. )
  160. .chain('exec')
  161. .resolves()
  162. })
  163. describe('when unsubscribing in Mailchimp succeeds', function () {
  164. beforeEach(function () {
  165. this.UserMock.expects('deleteOne')
  166. .withArgs({ _id: this.userId })
  167. .chain('exec')
  168. .resolves()
  169. })
  170. it('should find and the user in mongo by its id', async function () {
  171. await this.UserDeleter.promises.deleteUser(this.userId, {
  172. ipAddress: this.ipAddress,
  173. })
  174. this.UserMock.verify()
  175. })
  176. it('should delete the user from mailchimp', async function () {
  177. await this.UserDeleter.promises.deleteUser(this.userId, {
  178. ipAddress: this.ipAddress,
  179. })
  180. expect(
  181. this.NewsletterManager.promises.unsubscribe
  182. ).to.have.been.calledWith(this.user, { delete: true })
  183. })
  184. it('should delete all the projects of a user', async function () {
  185. await this.UserDeleter.promises.deleteUser(this.userId, {
  186. ipAddress: this.ipAddress,
  187. })
  188. expect(
  189. this.ProjectDeleter.promises.deleteUsersProjects
  190. ).to.have.been.calledWith(this.userId)
  191. })
  192. it("should cancel the user's subscription", async function () {
  193. await this.UserDeleter.promises.deleteUser(this.userId, {
  194. ipAddress: this.ipAddress,
  195. })
  196. expect(
  197. this.SubscriptionHandler.promises.cancelSubscription
  198. ).to.have.been.calledWith(this.user)
  199. })
  200. it('should delete user affiliations', async function () {
  201. await this.UserDeleter.promises.deleteUser(this.userId, {
  202. ipAddress: this.ipAddress,
  203. })
  204. expect(
  205. this.InstitutionsApi.promises.deleteAffiliations
  206. ).to.have.been.calledWith(this.userId)
  207. })
  208. it('should cleanup collabratec access tokens', async function () {
  209. await this.UserDeleter.promises.deleteUser(this.userId, {
  210. ipAddress: this.ipAddress,
  211. })
  212. expect(this.Modules.promises.hooks.fire).to.have.been.calledWith(
  213. 'cleanupPersonalAccessTokens',
  214. this.userId,
  215. ['collabratec', 'git_bridge']
  216. )
  217. })
  218. it('should fire the deleteUser hook for modules', async function () {
  219. await this.UserDeleter.promises.deleteUser(this.userId, {
  220. ipAddress: this.ipAddress,
  221. })
  222. expect(this.Modules.promises.hooks.fire).to.have.been.calledWith(
  223. 'deleteUser',
  224. this.userId
  225. )
  226. })
  227. it('should stop the user sessions', async function () {
  228. await this.UserDeleter.promises.deleteUser(this.userId, {
  229. ipAddress: this.ipAddress,
  230. })
  231. expect(
  232. this.UserSessionsManager.promises.removeSessionsFromRedis
  233. ).to.have.been.calledWith(this.user)
  234. })
  235. it('should remove user from group subscriptions', async function () {
  236. await this.UserDeleter.promises.deleteUser(this.userId, {
  237. ipAddress: this.ipAddress,
  238. })
  239. expect(
  240. this.SubscriptionUpdater.promises.removeUserFromAllGroups
  241. ).to.have.been.calledWith(this.userId)
  242. })
  243. it('should remove user memberships', async function () {
  244. await this.UserDeleter.promises.deleteUser(this.userId, {
  245. ipAddress: this.ipAddress,
  246. })
  247. expect(
  248. this.UserMembershipsHandler.promises.removeUserFromAllEntities
  249. ).to.have.been.calledWith(this.userId)
  250. })
  251. it('rejects if the user is a subscription admin', async function () {
  252. this.SubscriptionLocator.promises.getUsersSubscription.rejects({
  253. _id: 'some-subscription',
  254. })
  255. await expect(this.UserDeleter.promises.deleteUser(this.userId, {}))
  256. .to.be.rejected
  257. })
  258. it('should create a deletedUser', async function () {
  259. await this.UserDeleter.promises.deleteUser(this.userId, {
  260. ipAddress: this.ipAddress,
  261. })
  262. this.DeletedUserMock.verify()
  263. })
  264. describe('email notifications', function () {
  265. it('should email the user', async function () {
  266. await this.UserDeleter.promises.deleteUser(this.userId, {
  267. ipAddress: this.ipAddress,
  268. })
  269. const emailOptions = {
  270. to: 'bob@bob.com',
  271. action: 'account deleted',
  272. actionDescribed: 'your Overleaf account was deleted',
  273. }
  274. expect(
  275. this.EmailHandler.promises.sendEmail
  276. ).to.have.been.calledWith('securityAlert', emailOptions)
  277. })
  278. it('should fail when the email service fails', async function () {
  279. this.EmailHandler.promises.sendEmail = sinon
  280. .stub()
  281. .rejects(new Error('email failed'))
  282. await expect(
  283. this.UserDeleter.promises.deleteUser(this.userId, {
  284. ipAddress: this.ipAddress,
  285. })
  286. ).to.be.rejectedWith('email failed')
  287. })
  288. describe('with "force: true" option', function () {
  289. it('should succeed when the email service fails', async function () {
  290. this.EmailHandler.promises.sendEmail = sinon
  291. .stub()
  292. .rejects(new Error('email failed'))
  293. await expect(
  294. this.UserDeleter.promises.deleteUser(this.userId, {
  295. ipAddress: this.ipAddress,
  296. force: true,
  297. })
  298. ).to.be.fulfilled
  299. })
  300. })
  301. })
  302. it('should add an audit log entry', async function () {
  303. await this.UserDeleter.promises.deleteUser(this.userId, {
  304. ipAddress: this.ipAddress,
  305. })
  306. expect(
  307. this.UserAuditLogHandler.promises.addEntry
  308. ).to.have.been.calledWith(
  309. this.userId,
  310. 'delete-account',
  311. this.userId,
  312. this.ipAddress
  313. )
  314. })
  315. })
  316. describe('when unsubscribing from mailchimp fails', function () {
  317. beforeEach(function () {
  318. this.NewsletterManager.promises.unsubscribe.rejects(
  319. new Error('something went wrong')
  320. )
  321. })
  322. it('should return an error and not delete the user', async function () {
  323. await expect(
  324. this.UserDeleter.promises.deleteUser(this.userId, {
  325. ipAddress: this.ipAddress,
  326. })
  327. ).to.be.rejected
  328. this.UserMock.verify()
  329. })
  330. })
  331. describe('when called as a callback', function () {
  332. beforeEach(function () {
  333. this.UserMock.expects('deleteOne')
  334. .withArgs({ _id: this.userId })
  335. .chain('exec')
  336. .resolves()
  337. })
  338. it('should delete the user', function (done) {
  339. this.UserDeleter.deleteUser(
  340. this.userId,
  341. { ipAddress: this.ipAddress },
  342. err => {
  343. expect(err).not.to.exist
  344. this.UserMock.verify()
  345. this.DeletedUserMock.verify()
  346. done()
  347. }
  348. )
  349. })
  350. })
  351. })
  352. describe('when a user and IP address are specified', function () {
  353. beforeEach(function () {
  354. this.ipAddress = '1.2.3.4'
  355. this.deleterId = new ObjectId()
  356. this.deletedUser.deleterData.deleterIpAddress = this.ipAddress
  357. this.deletedUser.deleterData.deleterId = this.deleterId
  358. this.DeletedUserMock.expects('updateOne')
  359. .withArgs(
  360. { 'deleterData.deletedUserId': this.userId },
  361. this.deletedUser,
  362. { upsert: true }
  363. )
  364. .chain('exec')
  365. .resolves()
  366. this.UserMock.expects('deleteOne')
  367. .withArgs({ _id: this.userId })
  368. .chain('exec')
  369. .resolves()
  370. })
  371. it('should add the deleted user id and ip address to the deletedUser', async function () {
  372. await this.UserDeleter.promises.deleteUser(this.userId, {
  373. deleterUser: { _id: this.deleterId },
  374. ipAddress: this.ipAddress,
  375. })
  376. this.DeletedUserMock.verify()
  377. })
  378. it('should add an audit log entry', async function () {
  379. await this.UserDeleter.promises.deleteUser(this.userId, {
  380. deleterUser: { _id: this.deleterId },
  381. ipAddress: this.ipAddress,
  382. })
  383. expect(
  384. this.UserAuditLogHandler.promises.addEntry
  385. ).to.have.been.calledWith(
  386. this.userId,
  387. 'delete-account',
  388. this.deleterId,
  389. this.ipAddress
  390. )
  391. })
  392. describe('when called as a callback', function () {
  393. it('should delete the user', function (done) {
  394. this.UserDeleter.deleteUser(
  395. this.userId,
  396. {
  397. deleterUser: { _id: this.deleterId },
  398. ipAddress: this.ipAddress,
  399. },
  400. err => {
  401. expect(err).not.to.exist
  402. this.UserMock.verify()
  403. this.DeletedUserMock.verify()
  404. done()
  405. }
  406. )
  407. })
  408. })
  409. })
  410. })
  411. describe('when the user cannot be deleted because they are a subscription admin', function () {
  412. beforeEach(function () {
  413. this.SubscriptionLocator.promises.getUsersSubscription.resolves({
  414. _id: 'some-subscription',
  415. })
  416. })
  417. it('fails with a SubscriptionAdminDeletionError', async function () {
  418. await expect(
  419. this.UserDeleter.promises.deleteUser(this.userId)
  420. ).to.be.rejectedWith(Errors.SubscriptionAdminDeletionError)
  421. })
  422. it('should not create a deletedUser', async function () {
  423. await expect(this.UserDeleter.promises.deleteUser(this.userId)).to.be
  424. .rejected
  425. this.DeletedUserMock.verify()
  426. })
  427. it('should not remove the user from mongo', async function () {
  428. await expect(this.UserDeleter.promises.deleteUser(this.userId)).to.be
  429. .rejected
  430. this.UserMock.verify()
  431. })
  432. })
  433. })
  434. describe('ensureCanDeleteUser', function () {
  435. it('should not return error when user can be deleted', async function () {
  436. this.SubscriptionLocator.promises.getUsersSubscription.resolves(null)
  437. let error
  438. try {
  439. await this.UserDeleter.promises.ensureCanDeleteUser(this.user)
  440. } catch (e) {
  441. error = e
  442. } finally {
  443. expect(error).not.to.exist
  444. }
  445. })
  446. it('should return custom error when user is group admin', async function () {
  447. this.SubscriptionLocator.promises.getUsersSubscription.resolves({
  448. _id: '123abc',
  449. })
  450. let error
  451. try {
  452. await this.UserDeleter.promises.ensureCanDeleteUser(this.user)
  453. } catch (e) {
  454. error = e
  455. } finally {
  456. expect(error).to.be.instanceof(Errors.SubscriptionAdminDeletionError)
  457. }
  458. })
  459. it('propagates errors', async function () {
  460. this.SubscriptionLocator.promises.getUsersSubscription.rejects(
  461. new Error('Some error')
  462. )
  463. let error
  464. try {
  465. await this.UserDeleter.promises.ensureCanDeleteUser(this.user)
  466. } catch (e) {
  467. error = e
  468. } finally {
  469. expect(error).to.be.instanceof(Error)
  470. }
  471. })
  472. })
  473. describe('expireDeletedUsersAfterDuration', function () {
  474. const userId1 = new ObjectId()
  475. const userId2 = new ObjectId()
  476. beforeEach(function () {
  477. this.deletedUsers = [
  478. {
  479. user: { _id: userId1 },
  480. deleterData: { deletedUserId: userId1 },
  481. save: sinon.stub().resolves(),
  482. },
  483. {
  484. user: { _id: userId2 },
  485. deleterData: { deletedUserId: userId2 },
  486. save: sinon.stub().resolves(),
  487. },
  488. ]
  489. this.DeletedUserMock.expects('find')
  490. .withArgs({
  491. 'deleterData.deletedAt': {
  492. $lt: new Date(moment().subtract(90, 'days')),
  493. },
  494. user: {
  495. $ne: null,
  496. },
  497. })
  498. .chain('exec')
  499. .resolves(this.deletedUsers)
  500. for (const deletedUser of this.deletedUsers) {
  501. this.DeletedUserMock.expects('findOne')
  502. .withArgs({
  503. 'deleterData.deletedUserId': deletedUser.deleterData.deletedUserId,
  504. })
  505. .chain('exec')
  506. .resolves(deletedUser)
  507. }
  508. })
  509. it('clears data from all deleted users', async function () {
  510. await this.UserDeleter.promises.expireDeletedUsersAfterDuration()
  511. for (const deletedUser of this.deletedUsers) {
  512. expect(deletedUser.user).to.be.undefined
  513. expect(deletedUser.save.called).to.be.true
  514. }
  515. })
  516. it('deletes audit logs for all deleted users', async function () {
  517. await this.UserDeleter.promises.expireDeletedUsersAfterDuration()
  518. for (const deletedUser of this.deletedUsers) {
  519. expect(this.UserAuditLogEntry.deleteMany).to.have.been.calledWith({
  520. userId: deletedUser.deleterData.deletedUserId,
  521. })
  522. }
  523. })
  524. })
  525. describe('expireDeletedUser', function () {
  526. beforeEach(function () {
  527. this.mockedDeletedUser = sinon.mock(
  528. new DeletedUser({
  529. user: this.user,
  530. deleterData: {
  531. deleterIpAddress: '1.1.1.1',
  532. deletedUserId: this.userId,
  533. },
  534. })
  535. )
  536. this.deletedUser = this.mockedDeletedUser.object
  537. this.mockedDeletedUser.expects('save').resolves()
  538. this.DeletedUserMock.expects('findOne')
  539. .withArgs({ 'deleterData.deletedUserId': this.userId })
  540. .chain('exec')
  541. .resolves(this.deletedUser)
  542. })
  543. afterEach(function () {
  544. this.mockedDeletedUser.restore()
  545. })
  546. it('should find the user by user ID', async function () {
  547. await this.UserDeleter.promises.expireDeletedUser(this.userId)
  548. this.DeletedUserMock.verify()
  549. })
  550. it('should remove the user data from mongo', async function () {
  551. await this.UserDeleter.promises.expireDeletedUser(this.userId)
  552. expect(this.deletedUser.user).not.to.exist
  553. })
  554. it('should remove the IP address from mongo', async function () {
  555. await this.UserDeleter.promises.expireDeletedUser(this.userId)
  556. expect(this.deletedUser.deleterData.ipAddress).not.to.exist
  557. })
  558. it('should not delete other deleterData fields', async function () {
  559. await this.UserDeleter.promises.expireDeletedUser(this.userId)
  560. expect(this.deletedUser.deleterData.deletedUserId).to.equal(this.userId)
  561. })
  562. it('should save the record to mongo', async function () {
  563. await this.UserDeleter.promises.expireDeletedUser(this.userId)
  564. this.mockedDeletedUser.verify()
  565. })
  566. it('should fire the expireDeletedUser hook for modules', async function () {
  567. await this.UserDeleter.promises.expireDeletedUser(this.userId)
  568. expect(this.Modules.promises.hooks.fire).to.have.been.calledWith(
  569. 'expireDeletedUser',
  570. this.userId
  571. )
  572. })
  573. it('should delete Feeback', async function () {
  574. await this.UserDeleter.promises.expireDeletedUser(this.userId)
  575. expect(this.Feedback.deleteMany).to.have.been.calledWith({
  576. userId: this.userId,
  577. })
  578. })
  579. describe('when called as a callback', function () {
  580. it('should expire the user', function (done) {
  581. this.UserDeleter.expireDeletedUser(this.userId, err => {
  582. expect(err).not.to.exist
  583. this.DeletedUserMock.verify()
  584. this.mockedDeletedUser.verify()
  585. expect(this.deletedUser.user).not.to.exist
  586. expect(this.deletedUser.deleterData.ipAddress).not.to.exist
  587. done()
  588. })
  589. })
  590. })
  591. })
  592. })