UserDeleterTests.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  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.UserMock = sinon.mock(User)
  16. this.DeletedUserMock = sinon.mock(DeletedUser)
  17. this.mockedUser = sinon.mock(
  18. new User({
  19. _id: this.userId,
  20. email: 'bob@bob.com',
  21. lastLoggedIn: Date.now() + 1000,
  22. signUpDate: Date.now() + 2000,
  23. loginCount: 10,
  24. overleaf: {
  25. id: 1234,
  26. },
  27. refered_users: ['wombat', 'potato'],
  28. refered_user_count: 2,
  29. referal_id: ['giraffe'],
  30. })
  31. )
  32. this.user = this.mockedUser.object
  33. this.NewsletterManager = {
  34. promises: {
  35. unsubscribe: sinon.stub().resolves(),
  36. },
  37. }
  38. this.ProjectDeleter = {
  39. promises: {
  40. deleteUsersProjects: sinon.stub().resolves(),
  41. },
  42. }
  43. this.SubscriptionHandler = {
  44. promises: {
  45. cancelSubscription: sinon.stub().resolves(),
  46. },
  47. }
  48. this.SubscriptionUpdater = {
  49. promises: {
  50. removeUserFromAllGroups: sinon.stub().resolves(),
  51. },
  52. }
  53. this.SubscriptionLocator = {
  54. promises: {
  55. getUsersSubscription: sinon.stub().resolves(),
  56. },
  57. }
  58. this.UserMembershipsHandler = {
  59. promises: {
  60. removeUserFromAllEntities: sinon.stub().resolves(),
  61. },
  62. }
  63. this.UserSessionsManager = {
  64. promises: {
  65. revokeAllUserSessions: sinon.stub().resolves(),
  66. },
  67. }
  68. this.InstitutionsApi = {
  69. promises: {
  70. deleteAffiliations: sinon.stub().resolves(),
  71. },
  72. }
  73. this.UserAuditLogEntry = {
  74. deleteMany: sinon.stub().returns({ exec: sinon.stub().resolves() }),
  75. }
  76. this.Modules = {
  77. promises: { hooks: { fire: sinon.stub().resolves() } },
  78. }
  79. this.Feedback = {
  80. deleteMany: sinon.stub().returns({ exec: sinon.stub().resolves() }),
  81. }
  82. this.OnboardingDataCollectionManager = {
  83. deleteOnboardingDataCollection: sinon.stub().resolves(),
  84. }
  85. this.UserDeleter = SandboxedModule.require(modulePath, {
  86. requires: {
  87. '../../models/User': { User },
  88. '../../models/DeletedUser': { DeletedUser },
  89. '../../models/Feedback': { Feedback: this.Feedback },
  90. '../Newsletter/NewsletterManager': this.NewsletterManager,
  91. './UserSessionsManager': this.UserSessionsManager,
  92. '../Subscription/SubscriptionHandler': this.SubscriptionHandler,
  93. '../Subscription/SubscriptionUpdater': this.SubscriptionUpdater,
  94. '../Subscription/SubscriptionLocator': this.SubscriptionLocator,
  95. '../UserMembership/UserMembershipsHandler': this.UserMembershipsHandler,
  96. '../Project/ProjectDeleter': this.ProjectDeleter,
  97. '../Institutions/InstitutionsAPI': this.InstitutionsApi,
  98. '../../models/UserAuditLogEntry': {
  99. UserAuditLogEntry: this.UserAuditLogEntry,
  100. },
  101. '../../infrastructure/Modules': this.Modules,
  102. '../OnboardingDataCollection/OnboardingDataCollectionManager':
  103. this.OnboardingDataCollectionManager,
  104. },
  105. })
  106. })
  107. afterEach(function () {
  108. this.DeletedUserMock.restore()
  109. this.UserMock.restore()
  110. this.mockedUser.restore()
  111. })
  112. describe('deleteUser', function () {
  113. beforeEach(function () {
  114. this.UserMock.expects('findById')
  115. .withArgs(this.userId)
  116. .chain('exec')
  117. .resolves(this.user)
  118. })
  119. describe('when the user can be deleted', function () {
  120. beforeEach(function () {
  121. this.deletedUser = {
  122. user: this.user,
  123. deleterData: {
  124. deletedAt: new Date(),
  125. deletedUserId: this.userId,
  126. deleterIpAddress: undefined,
  127. deleterId: undefined,
  128. deletedUserLastLoggedIn: this.user.lastLoggedIn,
  129. deletedUserSignUpDate: this.user.signUpDate,
  130. deletedUserLoginCount: this.user.loginCount,
  131. deletedUserReferralId: this.user.referal_id,
  132. deletedUserReferredUsers: this.user.refered_users,
  133. deletedUserReferredUserCount: this.user.refered_user_count,
  134. deletedUserOverleafId: this.user.overleaf.id,
  135. },
  136. }
  137. })
  138. describe('when no options are passed', function () {
  139. beforeEach(function () {
  140. this.DeletedUserMock.expects('updateOne')
  141. .withArgs(
  142. { 'deleterData.deletedUserId': this.userId },
  143. this.deletedUser,
  144. { upsert: true }
  145. )
  146. .chain('exec')
  147. .resolves()
  148. })
  149. describe('when unsubscribing in Mailchimp succeeds', function () {
  150. beforeEach(function () {
  151. this.UserMock.expects('deleteOne')
  152. .withArgs({ _id: this.userId })
  153. .chain('exec')
  154. .resolves()
  155. })
  156. it('should find and the user in mongo by its id', async function () {
  157. await this.UserDeleter.promises.deleteUser(this.userId, {})
  158. this.UserMock.verify()
  159. })
  160. it('should delete the user from mailchimp', async function () {
  161. await this.UserDeleter.promises.deleteUser(this.userId, {})
  162. expect(
  163. this.NewsletterManager.promises.unsubscribe
  164. ).to.have.been.calledWith(this.user, { delete: true })
  165. })
  166. it('should delete all the projects of a user', async function () {
  167. await this.UserDeleter.promises.deleteUser(this.userId, {})
  168. expect(
  169. this.ProjectDeleter.promises.deleteUsersProjects
  170. ).to.have.been.calledWith(this.userId)
  171. })
  172. it("should cancel the user's subscription", async function () {
  173. await this.UserDeleter.promises.deleteUser(this.userId, {})
  174. expect(
  175. this.SubscriptionHandler.promises.cancelSubscription
  176. ).to.have.been.calledWith(this.user)
  177. })
  178. it('should delete user affiliations', async function () {
  179. await this.UserDeleter.promises.deleteUser(this.userId, {})
  180. expect(
  181. this.InstitutionsApi.promises.deleteAffiliations
  182. ).to.have.been.calledWith(this.userId)
  183. })
  184. it('should fire the deleteUser hook for modules', async function () {
  185. await this.UserDeleter.promises.deleteUser(this.userId, {})
  186. expect(this.Modules.promises.hooks.fire).to.have.been.calledWith(
  187. 'deleteUser',
  188. this.userId
  189. )
  190. })
  191. it('should stop the user sessions', async function () {
  192. await this.UserDeleter.promises.deleteUser(this.userId, {})
  193. expect(
  194. this.UserSessionsManager.promises.revokeAllUserSessions
  195. ).to.have.been.calledWith(this.userId, [])
  196. })
  197. it('should remove user from group subscriptions', async function () {
  198. await this.UserDeleter.promises.deleteUser(this.userId, {})
  199. expect(
  200. this.SubscriptionUpdater.promises.removeUserFromAllGroups
  201. ).to.have.been.calledWith(this.userId)
  202. })
  203. it('should remove user memberships', async function () {
  204. await this.UserDeleter.promises.deleteUser(this.userId, {})
  205. expect(
  206. this.UserMembershipsHandler.promises.removeUserFromAllEntities
  207. ).to.have.been.calledWith(this.userId)
  208. })
  209. it('rejects if the user is a subscription admin', async function () {
  210. this.SubscriptionLocator.promises.getUsersSubscription.rejects({
  211. _id: 'some-subscription',
  212. })
  213. await expect(this.UserDeleter.promises.deleteUser(this.userId, {}))
  214. .to.be.rejected
  215. })
  216. it('should create a deletedUser', async function () {
  217. await this.UserDeleter.promises.deleteUser(this.userId, {})
  218. this.DeletedUserMock.verify()
  219. })
  220. })
  221. describe('when unsubscribing from mailchimp fails', function () {
  222. beforeEach(function () {
  223. this.NewsletterManager.promises.unsubscribe.rejects(
  224. new Error('something went wrong')
  225. )
  226. })
  227. it('should return an error and not delete the user', async function () {
  228. await expect(this.UserDeleter.promises.deleteUser(this.userId, {}))
  229. .to.be.rejected
  230. this.UserMock.verify()
  231. })
  232. })
  233. describe('when called as a callback', function () {
  234. beforeEach(function () {
  235. this.UserMock.expects('deleteOne')
  236. .withArgs({ _id: this.userId })
  237. .chain('exec')
  238. .resolves()
  239. })
  240. it('should delete the user', function (done) {
  241. this.UserDeleter.deleteUser(this.userId, {}, err => {
  242. expect(err).not.to.exist
  243. this.UserMock.verify()
  244. this.DeletedUserMock.verify()
  245. done()
  246. })
  247. })
  248. })
  249. })
  250. describe('when a user and IP address are specified', function () {
  251. beforeEach(function () {
  252. this.ipAddress = '1.2.3.4'
  253. this.deleterId = new ObjectId()
  254. this.deletedUser.deleterData.deleterIpAddress = this.ipAddress
  255. this.deletedUser.deleterData.deleterId = this.deleterId
  256. this.DeletedUserMock.expects('updateOne')
  257. .withArgs(
  258. { 'deleterData.deletedUserId': this.userId },
  259. this.deletedUser,
  260. { upsert: true }
  261. )
  262. .chain('exec')
  263. .resolves()
  264. this.UserMock.expects('deleteOne')
  265. .withArgs({ _id: this.userId })
  266. .chain('exec')
  267. .resolves()
  268. })
  269. it('should add the deleted user id and ip address to the deletedUser', async function () {
  270. await this.UserDeleter.promises.deleteUser(this.userId, {
  271. deleterUser: { _id: this.deleterId },
  272. ipAddress: this.ipAddress,
  273. })
  274. this.DeletedUserMock.verify()
  275. })
  276. describe('when called as a callback', function () {
  277. it('should delete the user', function (done) {
  278. this.UserDeleter.deleteUser(
  279. this.userId,
  280. {
  281. deleterUser: { _id: this.deleterId },
  282. ipAddress: this.ipAddress,
  283. },
  284. err => {
  285. expect(err).not.to.exist
  286. this.UserMock.verify()
  287. this.DeletedUserMock.verify()
  288. done()
  289. }
  290. )
  291. })
  292. })
  293. })
  294. })
  295. describe('when the user cannot be deleted because they are a subscription admin', function () {
  296. beforeEach(function () {
  297. this.SubscriptionLocator.promises.getUsersSubscription.resolves({
  298. _id: 'some-subscription',
  299. })
  300. })
  301. it('fails with a SubscriptionAdminDeletionError', async function () {
  302. await expect(
  303. this.UserDeleter.promises.deleteUser(this.userId)
  304. ).to.be.rejectedWith(Errors.SubscriptionAdminDeletionError)
  305. })
  306. it('should not create a deletedUser', async function () {
  307. await expect(this.UserDeleter.promises.deleteUser(this.userId)).to.be
  308. .rejected
  309. this.DeletedUserMock.verify()
  310. })
  311. it('should not remove the user from mongo', async function () {
  312. await expect(this.UserDeleter.promises.deleteUser(this.userId)).to.be
  313. .rejected
  314. this.UserMock.verify()
  315. })
  316. })
  317. })
  318. describe('ensureCanDeleteUser', function () {
  319. it('should not return error when user can be deleted', async function () {
  320. this.SubscriptionLocator.promises.getUsersSubscription.resolves(null)
  321. let error
  322. try {
  323. await this.UserDeleter.promises.ensureCanDeleteUser(this.user)
  324. } catch (e) {
  325. error = e
  326. } finally {
  327. expect(error).not.to.exist
  328. }
  329. })
  330. it('should return custom error when user is group admin', async function () {
  331. this.SubscriptionLocator.promises.getUsersSubscription.resolves({
  332. _id: '123abc',
  333. })
  334. let error
  335. try {
  336. await this.UserDeleter.promises.ensureCanDeleteUser(this.user)
  337. } catch (e) {
  338. error = e
  339. } finally {
  340. expect(error).to.be.instanceof(Errors.SubscriptionAdminDeletionError)
  341. }
  342. })
  343. it('propagates errors', async function () {
  344. this.SubscriptionLocator.promises.getUsersSubscription.rejects(
  345. new Error('Some error')
  346. )
  347. let error
  348. try {
  349. await this.UserDeleter.promises.ensureCanDeleteUser(this.user)
  350. } catch (e) {
  351. error = e
  352. } finally {
  353. expect(error).to.be.instanceof(Error)
  354. }
  355. })
  356. })
  357. describe('expireDeletedUsersAfterDuration', function () {
  358. const userId1 = new ObjectId()
  359. const userId2 = new ObjectId()
  360. beforeEach(function () {
  361. this.deletedUsers = [
  362. {
  363. user: { _id: userId1 },
  364. deleterData: { deletedUserId: userId1 },
  365. save: sinon.stub().resolves(),
  366. },
  367. {
  368. user: { _id: userId2 },
  369. deleterData: { deletedUserId: userId2 },
  370. save: sinon.stub().resolves(),
  371. },
  372. ]
  373. this.DeletedUserMock.expects('find')
  374. .withArgs({
  375. 'deleterData.deletedAt': {
  376. $lt: new Date(moment().subtract(90, 'days')),
  377. },
  378. user: {
  379. $ne: null,
  380. },
  381. })
  382. .chain('exec')
  383. .resolves(this.deletedUsers)
  384. for (const deletedUser of this.deletedUsers) {
  385. this.DeletedUserMock.expects('findOne')
  386. .withArgs({
  387. 'deleterData.deletedUserId': deletedUser.deleterData.deletedUserId,
  388. })
  389. .chain('exec')
  390. .resolves(deletedUser)
  391. }
  392. })
  393. it('clears data from all deleted users', async function () {
  394. await this.UserDeleter.promises.expireDeletedUsersAfterDuration()
  395. for (const deletedUser of this.deletedUsers) {
  396. expect(deletedUser.user).to.be.undefined
  397. expect(deletedUser.save.called).to.be.true
  398. }
  399. })
  400. it('deletes audit logs for all deleted users', async function () {
  401. await this.UserDeleter.promises.expireDeletedUsersAfterDuration()
  402. for (const deletedUser of this.deletedUsers) {
  403. expect(this.UserAuditLogEntry.deleteMany).to.have.been.calledWith({
  404. userId: deletedUser.deleterData.deletedUserId,
  405. })
  406. }
  407. })
  408. })
  409. describe('expireDeletedUser', function () {
  410. beforeEach(function () {
  411. this.mockedDeletedUser = sinon.mock(
  412. new DeletedUser({
  413. user: this.user,
  414. deleterData: {
  415. deleterIpAddress: '1.1.1.1',
  416. deletedUserId: this.userId,
  417. },
  418. })
  419. )
  420. this.deletedUser = this.mockedDeletedUser.object
  421. this.mockedDeletedUser.expects('save').resolves()
  422. this.DeletedUserMock.expects('findOne')
  423. .withArgs({ 'deleterData.deletedUserId': this.userId })
  424. .chain('exec')
  425. .resolves(this.deletedUser)
  426. })
  427. afterEach(function () {
  428. this.mockedDeletedUser.restore()
  429. })
  430. it('should find the user by user ID', async function () {
  431. await this.UserDeleter.promises.expireDeletedUser(this.userId)
  432. this.DeletedUserMock.verify()
  433. })
  434. it('should remove the user data from mongo', async function () {
  435. await this.UserDeleter.promises.expireDeletedUser(this.userId)
  436. expect(this.deletedUser.user).not.to.exist
  437. })
  438. it('should remove the IP address from mongo', async function () {
  439. await this.UserDeleter.promises.expireDeletedUser(this.userId)
  440. expect(this.deletedUser.deleterData.ipAddress).not.to.exist
  441. })
  442. it('should not delete other deleterData fields', async function () {
  443. await this.UserDeleter.promises.expireDeletedUser(this.userId)
  444. expect(this.deletedUser.deleterData.deletedUserId).to.equal(this.userId)
  445. })
  446. it('should save the record to mongo', async function () {
  447. await this.UserDeleter.promises.expireDeletedUser(this.userId)
  448. this.mockedDeletedUser.verify()
  449. })
  450. it('should fire the expireDeletedUser hook for modules', async function () {
  451. await this.UserDeleter.promises.expireDeletedUser(this.userId)
  452. expect(this.Modules.promises.hooks.fire).to.have.been.calledWith(
  453. 'expireDeletedUser',
  454. this.userId
  455. )
  456. })
  457. it('should delete Feeback', async function () {
  458. await this.UserDeleter.promises.expireDeletedUser(this.userId)
  459. expect(this.Feedback.deleteMany).to.have.been.calledWith({
  460. userId: this.userId,
  461. })
  462. })
  463. describe('when called as a callback', function () {
  464. it('should expire the user', function (done) {
  465. this.UserDeleter.expireDeletedUser(this.userId, err => {
  466. expect(err).not.to.exist
  467. this.DeletedUserMock.verify()
  468. this.mockedDeletedUser.verify()
  469. expect(this.deletedUser.user).not.to.exist
  470. expect(this.deletedUser.deleterData.ipAddress).not.to.exist
  471. done()
  472. })
  473. })
  474. })
  475. })
  476. })