UserDeleterTests.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628
  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. revokeAllUserSessions: 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. })
  125. describe('deleteUser', function () {
  126. beforeEach(function () {
  127. this.UserMock.expects('findById')
  128. .withArgs(this.userId)
  129. .chain('exec')
  130. .resolves(this.user)
  131. })
  132. describe('when the user can be deleted', function () {
  133. beforeEach(function () {
  134. this.deletedUser = {
  135. user: this.user,
  136. deleterData: {
  137. deletedAt: new Date(),
  138. deletedUserId: this.userId,
  139. deleterIpAddress: this.ipAddress,
  140. deleterId: undefined,
  141. deletedUserLastLoggedIn: this.user.lastLoggedIn,
  142. deletedUserSignUpDate: this.user.signUpDate,
  143. deletedUserLoginCount: this.user.loginCount,
  144. deletedUserReferralId: this.user.referal_id,
  145. deletedUserReferredUsers: this.user.refered_users,
  146. deletedUserReferredUserCount: this.user.refered_user_count,
  147. deletedUserOverleafId: this.user.overleaf.id,
  148. },
  149. }
  150. })
  151. describe('when only the ip address is passed', function () {
  152. beforeEach(function () {
  153. this.DeletedUserMock.expects('updateOne')
  154. .withArgs(
  155. { 'deleterData.deletedUserId': this.userId },
  156. this.deletedUser,
  157. { upsert: true }
  158. )
  159. .chain('exec')
  160. .resolves()
  161. })
  162. describe('when unsubscribing in Mailchimp succeeds', function () {
  163. beforeEach(function () {
  164. this.UserMock.expects('deleteOne')
  165. .withArgs({ _id: this.userId })
  166. .chain('exec')
  167. .resolves()
  168. })
  169. it('should find and the user in mongo by its id', async function () {
  170. await this.UserDeleter.promises.deleteUser(this.userId, {
  171. ipAddress: this.ipAddress,
  172. })
  173. this.UserMock.verify()
  174. })
  175. it('should delete the user from mailchimp', async function () {
  176. await this.UserDeleter.promises.deleteUser(this.userId, {
  177. ipAddress: this.ipAddress,
  178. })
  179. expect(
  180. this.NewsletterManager.promises.unsubscribe
  181. ).to.have.been.calledWith(this.user, { delete: true })
  182. })
  183. it('should delete all the projects of a user', async function () {
  184. await this.UserDeleter.promises.deleteUser(this.userId, {
  185. ipAddress: this.ipAddress,
  186. })
  187. expect(
  188. this.ProjectDeleter.promises.deleteUsersProjects
  189. ).to.have.been.calledWith(this.userId)
  190. })
  191. it("should cancel the user's subscription", async function () {
  192. await this.UserDeleter.promises.deleteUser(this.userId, {
  193. ipAddress: this.ipAddress,
  194. })
  195. expect(
  196. this.SubscriptionHandler.promises.cancelSubscription
  197. ).to.have.been.calledWith(this.user)
  198. })
  199. it('should delete user affiliations', async function () {
  200. await this.UserDeleter.promises.deleteUser(this.userId, {
  201. ipAddress: this.ipAddress,
  202. })
  203. expect(
  204. this.InstitutionsApi.promises.deleteAffiliations
  205. ).to.have.been.calledWith(this.userId)
  206. })
  207. it('should fire the deleteUser hook for modules', async function () {
  208. await this.UserDeleter.promises.deleteUser(this.userId, {
  209. ipAddress: this.ipAddress,
  210. })
  211. expect(this.Modules.promises.hooks.fire).to.have.been.calledWith(
  212. 'deleteUser',
  213. this.userId
  214. )
  215. })
  216. it('should stop the user sessions', async function () {
  217. await this.UserDeleter.promises.deleteUser(this.userId, {
  218. ipAddress: this.ipAddress,
  219. })
  220. expect(
  221. this.UserSessionsManager.promises.revokeAllUserSessions
  222. ).to.have.been.calledWith(this.userId, [])
  223. })
  224. it('should remove user from group subscriptions', async function () {
  225. await this.UserDeleter.promises.deleteUser(this.userId, {
  226. ipAddress: this.ipAddress,
  227. })
  228. expect(
  229. this.SubscriptionUpdater.promises.removeUserFromAllGroups
  230. ).to.have.been.calledWith(this.userId)
  231. })
  232. it('should remove user memberships', async function () {
  233. await this.UserDeleter.promises.deleteUser(this.userId, {
  234. ipAddress: this.ipAddress,
  235. })
  236. expect(
  237. this.UserMembershipsHandler.promises.removeUserFromAllEntities
  238. ).to.have.been.calledWith(this.userId)
  239. })
  240. it('rejects if the user is a subscription admin', async function () {
  241. this.SubscriptionLocator.promises.getUsersSubscription.rejects({
  242. _id: 'some-subscription',
  243. })
  244. await expect(this.UserDeleter.promises.deleteUser(this.userId, {}))
  245. .to.be.rejected
  246. })
  247. it('should create a deletedUser', async function () {
  248. await this.UserDeleter.promises.deleteUser(this.userId, {
  249. ipAddress: this.ipAddress,
  250. })
  251. this.DeletedUserMock.verify()
  252. })
  253. it('should email the user', async function () {
  254. await this.UserDeleter.promises.deleteUser(this.userId, {
  255. ipAddress: this.ipAddress,
  256. })
  257. const emailOptions = {
  258. to: 'bob@bob.com',
  259. action: 'account deleted',
  260. actionDescribed: 'your Overleaf account was deleted',
  261. }
  262. expect(
  263. this.EmailHandler.promises.sendEmail
  264. ).to.have.been.calledWith('securityAlert', emailOptions)
  265. })
  266. it('should add an audit log entry', async function () {
  267. await this.UserDeleter.promises.deleteUser(this.userId, {
  268. ipAddress: this.ipAddress,
  269. })
  270. expect(
  271. this.UserAuditLogHandler.promises.addEntry
  272. ).to.have.been.calledWith(
  273. this.userId,
  274. 'delete-account',
  275. this.userId,
  276. this.ipAddress
  277. )
  278. })
  279. })
  280. describe('when unsubscribing from mailchimp fails', function () {
  281. beforeEach(function () {
  282. this.NewsletterManager.promises.unsubscribe.rejects(
  283. new Error('something went wrong')
  284. )
  285. })
  286. it('should return an error and not delete the user', async function () {
  287. await expect(
  288. this.UserDeleter.promises.deleteUser(this.userId, {
  289. ipAddress: this.ipAddress,
  290. })
  291. ).to.be.rejected
  292. this.UserMock.verify()
  293. })
  294. })
  295. describe('when called as a callback', function () {
  296. beforeEach(function () {
  297. this.UserMock.expects('deleteOne')
  298. .withArgs({ _id: this.userId })
  299. .chain('exec')
  300. .resolves()
  301. })
  302. it('should delete the user', function (done) {
  303. this.UserDeleter.deleteUser(
  304. this.userId,
  305. { ipAddress: this.ipAddress },
  306. err => {
  307. expect(err).not.to.exist
  308. this.UserMock.verify()
  309. this.DeletedUserMock.verify()
  310. done()
  311. }
  312. )
  313. })
  314. })
  315. })
  316. describe('when a user and IP address are specified', function () {
  317. beforeEach(function () {
  318. this.ipAddress = '1.2.3.4'
  319. this.deleterId = new ObjectId()
  320. this.deletedUser.deleterData.deleterIpAddress = this.ipAddress
  321. this.deletedUser.deleterData.deleterId = this.deleterId
  322. this.DeletedUserMock.expects('updateOne')
  323. .withArgs(
  324. { 'deleterData.deletedUserId': this.userId },
  325. this.deletedUser,
  326. { upsert: true }
  327. )
  328. .chain('exec')
  329. .resolves()
  330. this.UserMock.expects('deleteOne')
  331. .withArgs({ _id: this.userId })
  332. .chain('exec')
  333. .resolves()
  334. })
  335. it('should add the deleted user id and ip address to the deletedUser', async function () {
  336. await this.UserDeleter.promises.deleteUser(this.userId, {
  337. deleterUser: { _id: this.deleterId },
  338. ipAddress: this.ipAddress,
  339. })
  340. this.DeletedUserMock.verify()
  341. })
  342. it('should add an audit log entry', async function () {
  343. await this.UserDeleter.promises.deleteUser(this.userId, {
  344. deleterUser: { _id: this.deleterId },
  345. ipAddress: this.ipAddress,
  346. })
  347. expect(
  348. this.UserAuditLogHandler.promises.addEntry
  349. ).to.have.been.calledWith(
  350. this.userId,
  351. 'delete-account',
  352. this.deleterId,
  353. this.ipAddress
  354. )
  355. })
  356. describe('when called as a callback', function () {
  357. it('should delete the user', function (done) {
  358. this.UserDeleter.deleteUser(
  359. this.userId,
  360. {
  361. deleterUser: { _id: this.deleterId },
  362. ipAddress: this.ipAddress,
  363. },
  364. err => {
  365. expect(err).not.to.exist
  366. this.UserMock.verify()
  367. this.DeletedUserMock.verify()
  368. done()
  369. }
  370. )
  371. })
  372. })
  373. })
  374. })
  375. describe('when the user cannot be deleted because they are a subscription admin', function () {
  376. beforeEach(function () {
  377. this.SubscriptionLocator.promises.getUsersSubscription.resolves({
  378. _id: 'some-subscription',
  379. })
  380. })
  381. it('fails with a SubscriptionAdminDeletionError', async function () {
  382. await expect(
  383. this.UserDeleter.promises.deleteUser(this.userId)
  384. ).to.be.rejectedWith(Errors.SubscriptionAdminDeletionError)
  385. })
  386. it('should not create a deletedUser', async function () {
  387. await expect(this.UserDeleter.promises.deleteUser(this.userId)).to.be
  388. .rejected
  389. this.DeletedUserMock.verify()
  390. })
  391. it('should not remove the user from mongo', async function () {
  392. await expect(this.UserDeleter.promises.deleteUser(this.userId)).to.be
  393. .rejected
  394. this.UserMock.verify()
  395. })
  396. })
  397. })
  398. describe('ensureCanDeleteUser', function () {
  399. it('should not return error when user can be deleted', async function () {
  400. this.SubscriptionLocator.promises.getUsersSubscription.resolves(null)
  401. let error
  402. try {
  403. await this.UserDeleter.promises.ensureCanDeleteUser(this.user)
  404. } catch (e) {
  405. error = e
  406. } finally {
  407. expect(error).not.to.exist
  408. }
  409. })
  410. it('should return custom error when user is group admin', async function () {
  411. this.SubscriptionLocator.promises.getUsersSubscription.resolves({
  412. _id: '123abc',
  413. })
  414. let error
  415. try {
  416. await this.UserDeleter.promises.ensureCanDeleteUser(this.user)
  417. } catch (e) {
  418. error = e
  419. } finally {
  420. expect(error).to.be.instanceof(Errors.SubscriptionAdminDeletionError)
  421. }
  422. })
  423. it('propagates errors', async function () {
  424. this.SubscriptionLocator.promises.getUsersSubscription.rejects(
  425. new Error('Some error')
  426. )
  427. let error
  428. try {
  429. await this.UserDeleter.promises.ensureCanDeleteUser(this.user)
  430. } catch (e) {
  431. error = e
  432. } finally {
  433. expect(error).to.be.instanceof(Error)
  434. }
  435. })
  436. })
  437. describe('expireDeletedUsersAfterDuration', function () {
  438. const userId1 = new ObjectId()
  439. const userId2 = new ObjectId()
  440. beforeEach(function () {
  441. this.deletedUsers = [
  442. {
  443. user: { _id: userId1 },
  444. deleterData: { deletedUserId: userId1 },
  445. save: sinon.stub().resolves(),
  446. },
  447. {
  448. user: { _id: userId2 },
  449. deleterData: { deletedUserId: userId2 },
  450. save: sinon.stub().resolves(),
  451. },
  452. ]
  453. this.DeletedUserMock.expects('find')
  454. .withArgs({
  455. 'deleterData.deletedAt': {
  456. $lt: new Date(moment().subtract(90, 'days')),
  457. },
  458. user: {
  459. $ne: null,
  460. },
  461. })
  462. .chain('exec')
  463. .resolves(this.deletedUsers)
  464. for (const deletedUser of this.deletedUsers) {
  465. this.DeletedUserMock.expects('findOne')
  466. .withArgs({
  467. 'deleterData.deletedUserId': deletedUser.deleterData.deletedUserId,
  468. })
  469. .chain('exec')
  470. .resolves(deletedUser)
  471. }
  472. })
  473. it('clears data from all deleted users', async function () {
  474. await this.UserDeleter.promises.expireDeletedUsersAfterDuration()
  475. for (const deletedUser of this.deletedUsers) {
  476. expect(deletedUser.user).to.be.undefined
  477. expect(deletedUser.save.called).to.be.true
  478. }
  479. })
  480. it('deletes audit logs for all deleted users', async function () {
  481. await this.UserDeleter.promises.expireDeletedUsersAfterDuration()
  482. for (const deletedUser of this.deletedUsers) {
  483. expect(this.UserAuditLogEntry.deleteMany).to.have.been.calledWith({
  484. userId: deletedUser.deleterData.deletedUserId,
  485. })
  486. }
  487. })
  488. })
  489. describe('expireDeletedUser', function () {
  490. beforeEach(function () {
  491. this.mockedDeletedUser = sinon.mock(
  492. new DeletedUser({
  493. user: this.user,
  494. deleterData: {
  495. deleterIpAddress: '1.1.1.1',
  496. deletedUserId: this.userId,
  497. },
  498. })
  499. )
  500. this.deletedUser = this.mockedDeletedUser.object
  501. this.mockedDeletedUser.expects('save').resolves()
  502. this.DeletedUserMock.expects('findOne')
  503. .withArgs({ 'deleterData.deletedUserId': this.userId })
  504. .chain('exec')
  505. .resolves(this.deletedUser)
  506. })
  507. afterEach(function () {
  508. this.mockedDeletedUser.restore()
  509. })
  510. it('should find the user by user ID', async function () {
  511. await this.UserDeleter.promises.expireDeletedUser(this.userId)
  512. this.DeletedUserMock.verify()
  513. })
  514. it('should remove the user data from mongo', async function () {
  515. await this.UserDeleter.promises.expireDeletedUser(this.userId)
  516. expect(this.deletedUser.user).not.to.exist
  517. })
  518. it('should remove the IP address from mongo', async function () {
  519. await this.UserDeleter.promises.expireDeletedUser(this.userId)
  520. expect(this.deletedUser.deleterData.ipAddress).not.to.exist
  521. })
  522. it('should not delete other deleterData fields', async function () {
  523. await this.UserDeleter.promises.expireDeletedUser(this.userId)
  524. expect(this.deletedUser.deleterData.deletedUserId).to.equal(this.userId)
  525. })
  526. it('should save the record to mongo', async function () {
  527. await this.UserDeleter.promises.expireDeletedUser(this.userId)
  528. this.mockedDeletedUser.verify()
  529. })
  530. it('should fire the expireDeletedUser hook for modules', async function () {
  531. await this.UserDeleter.promises.expireDeletedUser(this.userId)
  532. expect(this.Modules.promises.hooks.fire).to.have.been.calledWith(
  533. 'expireDeletedUser',
  534. this.userId
  535. )
  536. })
  537. it('should delete Feeback', async function () {
  538. await this.UserDeleter.promises.expireDeletedUser(this.userId)
  539. expect(this.Feedback.deleteMany).to.have.been.calledWith({
  540. userId: this.userId,
  541. })
  542. })
  543. describe('when called as a callback', function () {
  544. it('should expire the user', function (done) {
  545. this.UserDeleter.expireDeletedUser(this.userId, err => {
  546. expect(err).not.to.exist
  547. this.DeletedUserMock.verify()
  548. this.mockedDeletedUser.verify()
  549. expect(this.deletedUser.user).not.to.exist
  550. expect(this.deletedUser.deleterData.ipAddress).not.to.exist
  551. done()
  552. })
  553. })
  554. })
  555. })
  556. })