UserDeleterTests.js 15 KB

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