| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537 |
- const { expect } = require('chai')
- const sinon = require('sinon')
- const tk = require('timekeeper')
- const moment = require('moment')
- const SandboxedModule = require('sandboxed-module')
- const Errors = require('../../../../app/src/Features/Errors/Errors')
- const ObjectId = require('mongoose').Types.ObjectId
- const { User } = require('../helpers/models/User')
- const { DeletedUser } = require('../helpers/models/DeletedUser')
- const modulePath = '../../../../app/src/Features/User/UserDeleter.js'
- describe('UserDeleter', function () {
- beforeEach(function () {
- tk.freeze(Date.now())
- this.userId = ObjectId()
- this.UserMock = sinon.mock(User)
- this.DeletedUserMock = sinon.mock(DeletedUser)
- this.mockedUser = sinon.mock(
- new User({
- _id: this.userId,
- email: 'bob@bob.com',
- lastLoggedIn: Date.now() + 1000,
- signUpDate: Date.now() + 2000,
- loginCount: 10,
- overleaf: {
- id: 1234,
- },
- refered_users: ['wombat', 'potato'],
- refered_user_count: 2,
- referal_id: ['giraffe'],
- })
- )
- this.user = this.mockedUser.object
- this.NewsletterManager = {
- promises: {
- unsubscribe: sinon.stub().resolves(),
- },
- }
- this.ProjectDeleter = {
- promises: {
- deleteUsersProjects: sinon.stub().resolves(),
- },
- }
- this.SubscriptionHandler = {
- promises: {
- cancelSubscription: sinon.stub().resolves(),
- },
- }
- this.SubscriptionUpdater = {
- promises: {
- removeUserFromAllGroups: sinon.stub().resolves(),
- },
- }
- this.SubscriptionLocator = {
- promises: {
- getUsersSubscription: sinon.stub().resolves(),
- },
- }
- this.UserMembershipsHandler = {
- promises: {
- removeUserFromAllEntities: sinon.stub().resolves(),
- },
- }
- this.UserSessionsManager = {
- promises: {
- revokeAllUserSessions: sinon.stub().resolves(),
- },
- }
- this.InstitutionsApi = {
- promises: {
- deleteAffiliations: sinon.stub().resolves(),
- },
- }
- this.UserAuditLogEntry = {
- deleteMany: sinon.stub().returns({ exec: sinon.stub().resolves() }),
- }
- this.Modules = {
- promises: { hooks: { fire: sinon.stub().resolves() } },
- }
- this.Feedback = {
- deleteMany: sinon.stub().returns({ exec: sinon.stub().resolves() }),
- }
- this.UserDeleter = SandboxedModule.require(modulePath, {
- requires: {
- '../../models/User': { User },
- '../../models/DeletedUser': { DeletedUser },
- '../../models/Feedback': { Feedback: this.Feedback },
- '../Newsletter/NewsletterManager': this.NewsletterManager,
- './UserSessionsManager': this.UserSessionsManager,
- '../Subscription/SubscriptionHandler': this.SubscriptionHandler,
- '../Subscription/SubscriptionUpdater': this.SubscriptionUpdater,
- '../Subscription/SubscriptionLocator': this.SubscriptionLocator,
- '../UserMembership/UserMembershipsHandler': this.UserMembershipsHandler,
- '../Project/ProjectDeleter': this.ProjectDeleter,
- '../Institutions/InstitutionsAPI': this.InstitutionsApi,
- '../../models/UserAuditLogEntry': {
- UserAuditLogEntry: this.UserAuditLogEntry,
- },
- '../../infrastructure/Modules': this.Modules,
- },
- })
- })
- afterEach(function () {
- this.DeletedUserMock.restore()
- this.UserMock.restore()
- this.mockedUser.restore()
- })
- describe('deleteUser', function () {
- beforeEach(function () {
- this.UserMock.expects('findById')
- .withArgs(this.userId)
- .chain('exec')
- .resolves(this.user)
- })
- describe('when the user can be deleted', function () {
- beforeEach(function () {
- this.deletedUser = {
- user: this.user,
- deleterData: {
- deletedAt: new Date(),
- deletedUserId: this.userId,
- deleterIpAddress: undefined,
- deleterId: undefined,
- deletedUserLastLoggedIn: this.user.lastLoggedIn,
- deletedUserSignUpDate: this.user.signUpDate,
- deletedUserLoginCount: this.user.loginCount,
- deletedUserReferralId: this.user.referal_id,
- deletedUserReferredUsers: this.user.refered_users,
- deletedUserReferredUserCount: this.user.refered_user_count,
- deletedUserOverleafId: this.user.overleaf.id,
- },
- }
- })
- describe('when no options are passed', function () {
- beforeEach(function () {
- this.DeletedUserMock.expects('updateOne')
- .withArgs(
- { 'deleterData.deletedUserId': this.userId },
- this.deletedUser,
- { upsert: true }
- )
- .chain('exec')
- .resolves()
- })
- describe('when unsubscribing in Mailchimp succeeds', function () {
- beforeEach(function () {
- this.UserMock.expects('deleteOne')
- .withArgs({ _id: this.userId })
- .chain('exec')
- .resolves()
- })
- it('should find and the user in mongo by its id', async function () {
- await this.UserDeleter.promises.deleteUser(this.userId)
- this.UserMock.verify()
- })
- it('should delete the user from mailchimp', async function () {
- await this.UserDeleter.promises.deleteUser(this.userId)
- expect(
- this.NewsletterManager.promises.unsubscribe
- ).to.have.been.calledWith(this.user, { delete: true })
- })
- it('should delete all the projects of a user', async function () {
- await this.UserDeleter.promises.deleteUser(this.userId)
- expect(
- this.ProjectDeleter.promises.deleteUsersProjects
- ).to.have.been.calledWith(this.userId)
- })
- it("should cancel the user's subscription", async function () {
- await this.UserDeleter.promises.deleteUser(this.userId)
- expect(
- this.SubscriptionHandler.promises.cancelSubscription
- ).to.have.been.calledWith(this.user)
- })
- it('should delete user affiliations', async function () {
- await this.UserDeleter.promises.deleteUser(this.userId)
- expect(
- this.InstitutionsApi.promises.deleteAffiliations
- ).to.have.been.calledWith(this.userId)
- })
- it('should fire the deleteUser hook for modules', async function () {
- await this.UserDeleter.promises.deleteUser(this.userId)
- expect(this.Modules.promises.hooks.fire).to.have.been.calledWith(
- 'deleteUser',
- this.userId
- )
- })
- it('should stop the user sessions', async function () {
- await this.UserDeleter.promises.deleteUser(this.userId)
- expect(
- this.UserSessionsManager.promises.revokeAllUserSessions
- ).to.have.been.calledWith(this.userId, [])
- })
- it('should remove user from group subscriptions', async function () {
- await this.UserDeleter.promises.deleteUser(this.userId)
- expect(
- this.SubscriptionUpdater.promises.removeUserFromAllGroups
- ).to.have.been.calledWith(this.userId)
- })
- it('should remove user memberships', async function () {
- await this.UserDeleter.promises.deleteUser(this.userId)
- expect(
- this.UserMembershipsHandler.promises.removeUserFromAllEntities
- ).to.have.been.calledWith(this.userId)
- })
- it('rejects if the user is a subscription admin', async function () {
- this.SubscriptionLocator.promises.getUsersSubscription.rejects({
- _id: 'some-subscription',
- })
- await expect(this.UserDeleter.promises.deleteUser(this.userId)).to
- .be.rejected
- })
- it('should create a deletedUser', async function () {
- await this.UserDeleter.promises.deleteUser(this.userId)
- this.DeletedUserMock.verify()
- })
- })
- describe('when unsubscribing from mailchimp fails', function () {
- beforeEach(function () {
- this.NewsletterManager.promises.unsubscribe.rejects(
- new Error('something went wrong')
- )
- })
- it('should return an error and not delete the user', async function () {
- await expect(this.UserDeleter.promises.deleteUser(this.userId)).to
- .be.rejected
- this.UserMock.verify()
- })
- })
- describe('when called as a callback', function () {
- beforeEach(function () {
- this.UserMock.expects('deleteOne')
- .withArgs({ _id: this.userId })
- .chain('exec')
- .resolves()
- })
- it('should delete the user', function (done) {
- this.UserDeleter.deleteUser(this.userId, err => {
- expect(err).not.to.exist
- this.UserMock.verify()
- this.DeletedUserMock.verify()
- done()
- })
- })
- })
- })
- describe('when a user and IP address are specified', function () {
- beforeEach(function () {
- this.ipAddress = '1.2.3.4'
- this.deleterId = ObjectId()
- this.deletedUser.deleterData.deleterIpAddress = this.ipAddress
- this.deletedUser.deleterData.deleterId = this.deleterId
- this.DeletedUserMock.expects('updateOne')
- .withArgs(
- { 'deleterData.deletedUserId': this.userId },
- this.deletedUser,
- { upsert: true }
- )
- .chain('exec')
- .resolves()
- this.UserMock.expects('deleteOne')
- .withArgs({ _id: this.userId })
- .chain('exec')
- .resolves()
- })
- it('should add the deleted user id and ip address to the deletedUser', async function () {
- await this.UserDeleter.promises.deleteUser(this.userId, {
- deleterUser: { _id: this.deleterId },
- ipAddress: this.ipAddress,
- })
- this.DeletedUserMock.verify()
- })
- describe('when called as a callback', function () {
- it('should delete the user', function (done) {
- this.UserDeleter.deleteUser(
- this.userId,
- {
- deleterUser: { _id: this.deleterId },
- ipAddress: this.ipAddress,
- },
- err => {
- expect(err).not.to.exist
- this.UserMock.verify()
- this.DeletedUserMock.verify()
- done()
- }
- )
- })
- })
- })
- })
- describe('when the user cannot be deleted because they are a subscription admin', function () {
- beforeEach(function () {
- this.SubscriptionLocator.promises.getUsersSubscription.resolves({
- _id: 'some-subscription',
- })
- })
- it('fails with a SubscriptionAdminDeletionError', async function () {
- await expect(
- this.UserDeleter.promises.deleteUser(this.userId)
- ).to.be.rejectedWith(Errors.SubscriptionAdminDeletionError)
- })
- it('should not create a deletedUser', async function () {
- await expect(this.UserDeleter.promises.deleteUser(this.userId)).to.be
- .rejected
- this.DeletedUserMock.verify()
- })
- it('should not remove the user from mongo', async function () {
- await expect(this.UserDeleter.promises.deleteUser(this.userId)).to.be
- .rejected
- this.UserMock.verify()
- })
- })
- })
- describe('ensureCanDeleteUser', function () {
- it('should not return error when user can be deleted', async function () {
- this.SubscriptionLocator.promises.getUsersSubscription.resolves(null)
- let error
- try {
- await this.UserDeleter.promises.ensureCanDeleteUser(this.user)
- } catch (e) {
- error = e
- } finally {
- expect(error).not.to.exist
- }
- })
- it('should return custom error when user is group admin', async function () {
- this.SubscriptionLocator.promises.getUsersSubscription.resolves({
- _id: '123abc',
- })
- let error
- try {
- await this.UserDeleter.promises.ensureCanDeleteUser(this.user)
- } catch (e) {
- error = e
- } finally {
- expect(error).to.be.instanceof(Errors.SubscriptionAdminDeletionError)
- }
- })
- it('propagates errors', async function () {
- this.SubscriptionLocator.promises.getUsersSubscription.rejects(
- new Error('Some error')
- )
- let error
- try {
- await this.UserDeleter.promises.ensureCanDeleteUser(this.user)
- } catch (e) {
- error = e
- } finally {
- expect(error).to.be.instanceof(Error)
- }
- })
- })
- describe('expireDeletedUsersAfterDuration', function () {
- const userId1 = new ObjectId()
- const userId2 = new ObjectId()
- beforeEach(function () {
- this.deletedUsers = [
- {
- user: { _id: userId1 },
- deleterData: { deletedUserId: userId1 },
- save: sinon.stub().resolves(),
- },
- {
- user: { _id: userId2 },
- deleterData: { deletedUserId: userId2 },
- save: sinon.stub().resolves(),
- },
- ]
- this.DeletedUserMock.expects('find')
- .withArgs({
- 'deleterData.deletedAt': {
- $lt: new Date(moment().subtract(90, 'days')),
- },
- user: {
- $ne: null,
- },
- })
- .chain('exec')
- .resolves(this.deletedUsers)
- for (const deletedUser of this.deletedUsers) {
- this.DeletedUserMock.expects('findOne')
- .withArgs({
- 'deleterData.deletedUserId': deletedUser.deleterData.deletedUserId,
- })
- .chain('exec')
- .resolves(deletedUser)
- }
- })
- it('clears data from all deleted users', async function () {
- await this.UserDeleter.promises.expireDeletedUsersAfterDuration()
- for (const deletedUser of this.deletedUsers) {
- expect(deletedUser.user).to.be.undefined
- expect(deletedUser.save.called).to.be.true
- }
- })
- it('deletes audit logs for all deleted users', async function () {
- await this.UserDeleter.promises.expireDeletedUsersAfterDuration()
- for (const deletedUser of this.deletedUsers) {
- expect(this.UserAuditLogEntry.deleteMany).to.have.been.calledWith({
- userId: deletedUser.deleterData.deletedUserId,
- })
- }
- })
- })
- describe('expireDeletedUser', function () {
- beforeEach(function () {
- this.mockedDeletedUser = sinon.mock(
- new DeletedUser({
- user: this.user,
- deleterData: {
- deleterIpAddress: '1.1.1.1',
- deletedUserId: this.userId,
- },
- })
- )
- this.deletedUser = this.mockedDeletedUser.object
- this.mockedDeletedUser.expects('save').resolves()
- this.DeletedUserMock.expects('findOne')
- .withArgs({ 'deleterData.deletedUserId': this.userId })
- .chain('exec')
- .resolves(this.deletedUser)
- })
- afterEach(function () {
- this.mockedDeletedUser.restore()
- })
- it('should find the user by user ID', async function () {
- await this.UserDeleter.promises.expireDeletedUser(this.userId)
- this.DeletedUserMock.verify()
- })
- it('should remove the user data from mongo', async function () {
- await this.UserDeleter.promises.expireDeletedUser(this.userId)
- expect(this.deletedUser.user).not.to.exist
- })
- it('should remove the IP address from mongo', async function () {
- await this.UserDeleter.promises.expireDeletedUser(this.userId)
- expect(this.deletedUser.deleterData.ipAddress).not.to.exist
- })
- it('should not delete other deleterData fields', async function () {
- await this.UserDeleter.promises.expireDeletedUser(this.userId)
- expect(this.deletedUser.deleterData.deletedUserId).to.equal(this.userId)
- })
- it('should save the record to mongo', async function () {
- await this.UserDeleter.promises.expireDeletedUser(this.userId)
- this.mockedDeletedUser.verify()
- })
- it('should fire the expireDeletedUser hook for modules', async function () {
- await this.UserDeleter.promises.expireDeletedUser(this.userId)
- expect(this.Modules.promises.hooks.fire).to.have.been.calledWith(
- 'expireDeletedUser',
- this.userId
- )
- })
- it('should delete Feeback', async function () {
- await this.UserDeleter.promises.expireDeletedUser(this.userId)
- expect(this.Feedback.deleteMany).to.have.been.calledWith({
- userId: this.userId,
- })
- })
- describe('when called as a callback', function () {
- it('should expire the user', function (done) {
- this.UserDeleter.expireDeletedUser(this.userId, err => {
- expect(err).not.to.exist
- this.DeletedUserMock.verify()
- this.mockedDeletedUser.verify()
- expect(this.deletedUser.user).not.to.exist
- expect(this.deletedUser.deleterData.ipAddress).not.to.exist
- done()
- })
- })
- })
- })
- })
|