UserDeleterTests.js 17 KB

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