UserDeleterTests.js 15 KB

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