UserDeleterTests.js 16 KB

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