[web] Migrate some User and UserMembership files to ESM (#29181)

* Rename files to mjs

* Migrate files to ESM

* Fix imports

* Misc. fixes: Fixup InsititutionsAPI import, ObjectId import, ...

* Rename test files to mjs

* Convert test files to ESM

* Fix tests

* Update UserMembershipErrors imports

* Convert some tests: sinon -> vitest

* Fixup UserMembershipHandler.test.mjs

* Convert UserMembershipErrors to ESM

GitOrigin-RevId: 05d34c7e112a567f9c59398740ae0830ef93d32f
This commit is contained in:
Antoine Clausse
2025-10-20 16:14:31 +02:00
committed by Copybot
parent 37fff74a62
commit 71a267e104
20 changed files with 1055 additions and 872 deletions

View File

@@ -7,8 +7,8 @@ import UserDeleter from './UserDeleter.js'
import UserGetter from './UserGetter.js'
import UserUpdater from './UserUpdater.js'
import Analytics from '../Analytics/AnalyticsManager.js'
import UserOnboardingEmailManager from './UserOnboardingEmailManager.js'
import UserPostRegistrationAnalyticsManager from './UserPostRegistrationAnalyticsManager.js'
import UserOnboardingEmailManager from './UserOnboardingEmailManager.mjs'
import UserPostRegistrationAnalyticsManager from './UserPostRegistrationAnalyticsManager.mjs'
import OError from '@overleaf/o-error'
async function _addAffiliation(user, affiliationOptions) {

View File

@@ -1,8 +1,8 @@
const Queues = require('../../infrastructure/Queues')
const EmailHandler = require('../Email/EmailHandler')
const UserUpdater = require('./UserUpdater')
const UserGetter = require('./UserGetter')
const Settings = require('@overleaf/settings')
import Queues from '../../infrastructure/Queues.js'
import EmailHandler from '../Email/EmailHandler.js'
import UserUpdater from './UserUpdater.js'
import UserGetter from './UserGetter.js'
import Settings from '@overleaf/settings'
const ONE_DAY_MS = 24 * 60 * 60 * 1000
@@ -26,4 +26,4 @@ async function sendOnboardingEmail(userId) {
}
}
module.exports = { scheduleOnboardingEmail, sendOnboardingEmail }
export default { scheduleOnboardingEmail, sendOnboardingEmail }

View File

@@ -1,9 +1,7 @@
const Queues = require('../../infrastructure/Queues')
const UserGetter = require('./UserGetter')
const {
promises: InstitutionsAPIPromises,
} = require('../Institutions/InstitutionsAPI')
const AnalyticsManager = require('../Analytics/AnalyticsManager')
import Queues from '../../infrastructure/Queues.js'
import UserGetter from './UserGetter.js'
import InstitutionsAPI from '../Institutions/InstitutionsAPI.js'
import AnalyticsManager from '../Analytics/AnalyticsManager.js'
const ONE_DAY_MS = 24 * 60 * 60 * 1000
@@ -25,7 +23,7 @@ async function postRegistrationAnalytics(userId) {
async function checkAffiliations(userId) {
const affiliationsData =
await InstitutionsAPIPromises.getUserAffiliations(userId)
await InstitutionsAPI.promises.getUserAffiliations(userId)
const hasCommonsAccountAffiliation = affiliationsData.some(
affiliationData =>
affiliationData.institution && affiliationData.institution.commonsAccount
@@ -40,7 +38,7 @@ async function checkAffiliations(userId) {
}
}
module.exports = {
export default {
schedulePostRegistrationAnalytics,
postRegistrationAnalytics,
}

View File

@@ -1,9 +1,9 @@
const {
import {
hasAdminCapability,
hasAdminAccess,
} = require('../Helpers/AdminAuthorizationHelper')
const SessionManager = require('../Authentication/SessionManager')
const Settings = require('@overleaf/settings')
} from '../Helpers/AdminAuthorizationHelper.js'
import SessionManager from '../Authentication/SessionManager.js'
import Settings from '@overleaf/settings'
const UserMembershipAuthorization = {
hasStaffAccess(requiredStaffAccess) {
@@ -52,4 +52,4 @@ const UserMembershipAuthorization = {
}
},
}
module.exports = UserMembershipAuthorization
export default UserMembershipAuthorization

View File

@@ -1,20 +1,16 @@
import SessionManager from '../Authentication/SessionManager.js'
import UserMembershipHandler from './UserMembershipHandler.js'
import UserMembershipHandler from './UserMembershipHandler.mjs'
import Errors from '../Errors/Errors.js'
import EmailHelper from '../Helpers/EmailHelper.js'
import { csvAttachment } from '../../infrastructure/Response.js'
import {
UserIsManagerError,
UserAlreadyAddedError,
UserNotFoundError,
} from './UserMembershipErrors.js'
import UserMembershipErrors from './UserMembershipErrors.mjs'
import { SSOConfig } from '../../models/SSOConfig.js'
import { Parser as CSVParser } from 'json2csv'
import { expressify } from '@overleaf/promise-utils'
import PlansLocator from '../Subscription/PlansLocator.js'
import RecurlyClient from '../Subscription/RecurlyClient.js'
import Modules from '../../infrastructure/Modules.js'
import UserMembershipAuthorization from './UserMembershipAuthorization.js'
import UserMembershipAuthorization from './UserMembershipAuthorization.mjs'
async function manageGroupMembers(req, res, next) {
const { entity: subscription, entityConfig } = req
@@ -201,7 +197,10 @@ export default {
entityConfig,
email,
function (error, user) {
if (error && error instanceof UserAlreadyAddedError) {
if (
error &&
error instanceof UserMembershipErrors.UserAlreadyAddedError
) {
return res.status(400).json({
error: {
code: 'user_already_added',
@@ -209,7 +208,7 @@ export default {
},
})
}
if (error && error instanceof UserNotFoundError) {
if (error && error instanceof UserMembershipErrors.UserNotFoundError) {
return res.status(404).json({
error: {
code: 'user_not_found',
@@ -247,7 +246,7 @@ export default {
entityConfig,
userId,
function (error, user) {
if (error && error instanceof UserIsManagerError) {
if (error && error instanceof UserMembershipErrors.UserIsManagerError) {
return res.status(400).json({
error: {
code: 'managers_cannot_remove_admin',

View File

@@ -1,11 +1,13 @@
const OError = require('@overleaf/o-error')
import OError from '@overleaf/o-error'
class UserIsManagerError extends OError {}
class UserNotFoundError extends OError {}
class UserAlreadyAddedError extends OError {}
module.exports = {
const UserMembershipErrors = {
UserIsManagerError,
UserNotFoundError,
UserAlreadyAddedError,
}
export default UserMembershipErrors

View File

@@ -1,17 +1,15 @@
const { ObjectId } = require('mongodb-legacy')
const { promisifyAll, callbackify } = require('@overleaf/promise-utils')
const EntityModels = {
Institution: require('../../models/Institution').Institution,
Subscription: require('../../models/Subscription').Subscription,
Publisher: require('../../models/Publisher').Publisher,
}
const UserMembershipViewModel = require('./UserMembershipViewModel')
const UserGetter = require('../User/UserGetter')
const {
UserIsManagerError,
UserNotFoundError,
UserAlreadyAddedError,
} = require('./UserMembershipErrors')
import mongodb from 'mongodb-legacy'
import { promisifyAll, callbackify } from '@overleaf/promise-utils'
import { Institution } from '../../models/Institution.js'
import { Subscription } from '../../models/Subscription.js'
import { Publisher } from '../../models/Publisher.js'
import UserMembershipViewModel from './UserMembershipViewModel.mjs'
import UserGetter from '../User/UserGetter.js'
import UserMembershipErrors from './UserMembershipErrors.mjs'
const { ObjectId } = mongodb
const EntityModels = { Institution, Subscription, Publisher }
const UserMembershipHandler = {
async getEntityWithoutAuthorizationCheck(entityId, entityConfig) {
@@ -34,11 +32,11 @@ const UserMembershipHandler = {
const user = await UserGetter.promises.getUserByAnyEmail(email)
if (!user) {
throw new UserNotFoundError()
throw new UserMembershipErrors.UserNotFoundError()
}
if (entity[attribute].some(managerId => managerId.equals(user._id))) {
throw new UserAlreadyAddedError()
throw new UserMembershipErrors.UserAlreadyAddedError()
}
await addUserToEntity(entity, attribute, user)
@@ -48,14 +46,15 @@ const UserMembershipHandler = {
async removeUser(entity, entityConfig, userId) {
const attribute = entityConfig.fields.write
if (entity.admin_id ? entity.admin_id.equals(userId) : undefined) {
throw new UserIsManagerError()
throw new UserMembershipErrors.UserIsManagerError()
}
return await removeUserFromEntity(entity, attribute, userId)
},
}
UserMembershipHandler.promises = promisifyAll(UserMembershipHandler)
module.exports = {
export default {
getEntityWithoutAuthorizationCheck: callbackify(
UserMembershipHandler.getEntityWithoutAuthorizationCheck
),

View File

@@ -1,16 +1,17 @@
// @ts-check
const { expressify } = require('@overleaf/promise-utils')
const async = require('async')
const UserMembershipAuthorization = require('./UserMembershipAuthorization')
const AuthenticationController = require('../Authentication/AuthenticationController')
const UserMembershipHandler = require('./UserMembershipHandler')
const EntityConfigs = require('./UserMembershipEntityConfigs')
const Errors = require('../Errors/Errors')
const HttpErrorHandler = require('../Errors/HttpErrorHandler')
const TemplatesManager = require('../Templates/TemplatesManager')
const { z, zz, validateReq } = require('../../infrastructure/Validation')
const { useAdminCapabilities } = require('../Helpers/AdminAuthorizationHelper')
import { expressify } from '@overleaf/promise-utils'
import async from 'async'
import UserMembershipAuthorization from './UserMembershipAuthorization.mjs'
import AuthenticationController from '../Authentication/AuthenticationController.js'
import UserMembershipHandler from './UserMembershipHandler.mjs'
import EntityConfigs from './UserMembershipEntityConfigs.js'
import Errors from '../Errors/Errors.js'
import HttpErrorHandler from '../Errors/HttpErrorHandler.js'
import TemplatesManager from '../Templates/TemplatesManager.js'
import { z, zz, validateReq } from '../../infrastructure/Validation.js'
import { useAdminCapabilities } from '../Helpers/AdminAuthorizationHelper.js'
// set of middleware arrays or functions that checks user access to an entity
// (publisher, institution, group, template, etc.)
@@ -234,7 +235,7 @@ const UserMembershipMiddleware = {
},
}
module.exports = UserMembershipMiddleware
export default UserMembershipMiddleware
// fetch entity config and set it in the request
function fetchEntityConfig(entityName) {

View File

@@ -1,4 +1,4 @@
import UserMembershipMiddleware from './UserMembershipMiddleware.js'
import UserMembershipMiddleware from './UserMembershipMiddleware.mjs'
import UserMembershipController from './UserMembershipController.mjs'
import SubscriptionGroupController from '../Subscription/SubscriptionGroupController.mjs'
import TeamInvitesController from '../Subscription/TeamInvitesController.mjs'

View File

@@ -1,5 +1,5 @@
const UserGetter = require('../User/UserGetter')
const { isObjectIdInstance } = require('../Helpers/Mongo')
import UserGetter from '../User/UserGetter.js'
import { isObjectIdInstance } from '../Helpers/Mongo.js'
const UserMembershipViewModel = {
build(userOrEmail) {
@@ -81,4 +81,4 @@ UserMembershipViewModel.promises = {
buildAsync: UserMembershipViewModel.buildAsync,
}
module.exports = UserMembershipViewModel
export default UserMembershipViewModel

View File

@@ -1,7 +1,7 @@
import Features from './Features.js'
import Queues from './Queues.js'
import UserOnboardingEmailManager from '../Features/User/UserOnboardingEmailManager.js'
import UserPostRegistrationAnalyticsManager from '../Features/User/UserPostRegistrationAnalyticsManager.js'
import UserOnboardingEmailManager from '../Features/User/UserOnboardingEmailManager.mjs'
import UserPostRegistrationAnalyticsManager from '../Features/User/UserPostRegistrationAnalyticsManager.mjs'
import FeaturesUpdater from '../Features/Subscription/FeaturesUpdater.js'
import {

View File

@@ -0,0 +1,114 @@
import { vi, expect } from 'vitest'
import sinon from 'sinon'
const MODULE_PATH =
'../../../../app/src/Features/User/UserOnboardingEmailManager'
describe('UserOnboardingEmailManager', function () {
beforeEach(async function (ctx) {
ctx.fakeUserId = '123abc'
ctx.fakeUserEmail = 'frog@overleaf.com'
ctx.onboardingEmailsQueue = {
add: sinon.stub().resolves(),
process: callback => {
ctx.queueProcessFunction = callback
},
}
ctx.Queues = {
createScheduledJob: sinon.stub().resolves(),
}
ctx.UserGetter = {
promises: {
getUser: sinon.stub().resolves(null),
},
}
ctx.UserGetter.promises.getUser.withArgs({ _id: ctx.fakeUserId }).resolves({
_id: ctx.fakeUserId,
email: ctx.fakeUserEmail,
})
ctx.EmailHandler = {
promises: {
sendEmail: sinon.stub().resolves(),
},
}
ctx.UserUpdater = {
promises: {
updateUser: sinon.stub().resolves(),
},
}
vi.doMock('../../../../app/src/infrastructure/Queues', () => ({
default: ctx.Queues,
}))
vi.doMock('../../../../app/src/Features/Email/EmailHandler', () => ({
default: ctx.EmailHandler,
}))
vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({
default: ctx.UserGetter,
}))
vi.doMock('../../../../app/src/Features/User/UserUpdater', () => ({
default: ctx.UserUpdater,
}))
vi.doMock('@overleaf/settings', () => ({
default: (ctx.Settings = {
enableOnboardingEmails: true,
}),
}))
ctx.UserOnboardingEmailManager = (await import(MODULE_PATH)).default
})
describe('scheduleOnboardingEmail', function () {
it('should schedule delayed job on queue', async function (ctx) {
await ctx.UserOnboardingEmailManager.scheduleOnboardingEmail({
_id: ctx.fakeUserId,
})
sinon.assert.calledWith(
ctx.Queues.createScheduledJob,
'emails-onboarding',
{ data: { userId: ctx.fakeUserId } },
24 * 60 * 60 * 1000
)
})
})
describe('sendOnboardingEmail', function () {
describe('when onboarding emails are disabled', function () {
beforeEach(function (ctx) {
ctx.Settings.enableOnboardingEmails = false
})
it('should not send onboarding email', async function (ctx) {
await ctx.UserOnboardingEmailManager.sendOnboardingEmail(ctx.fakeUserId)
expect(ctx.EmailHandler.promises.sendEmail).not.to.have.been.called
expect(ctx.UserUpdater.promises.updateUser).not.to.have.been.called
})
})
describe('when onboarding emails are enabled', function () {
it('should send onboarding email and update user', async function (ctx) {
await ctx.UserOnboardingEmailManager.sendOnboardingEmail(ctx.fakeUserId)
expect(ctx.EmailHandler.promises.sendEmail).to.have.been.calledWith(
'userOnboardingEmail',
{
to: ctx.fakeUserEmail,
}
)
expect(ctx.UserUpdater.promises.updateUser).to.have.been.calledWith(
ctx.fakeUserId,
{ $set: { onboardingEmailSentAt: sinon.match.date } }
)
})
it('should stop if user is not found', async function (ctx) {
await ctx.UserOnboardingEmailManager.sendOnboardingEmail({
data: { userId: 'deleted-user' },
})
expect(ctx.EmailHandler.promises.sendEmail).not.to.have.been.called
expect(ctx.UserUpdater.promises.updateUser).not.to.have.been.called
})
})
})
})

View File

@@ -1,112 +0,0 @@
const SandboxedModule = require('sandboxed-module')
const path = require('path')
const sinon = require('sinon')
const { expect } = require('chai')
const MODULE_PATH = path.join(
__dirname,
'../../../../app/src/Features/User/UserOnboardingEmailManager'
)
describe('UserOnboardingEmailManager', function () {
beforeEach(function () {
this.fakeUserId = '123abc'
this.fakeUserEmail = 'frog@overleaf.com'
this.onboardingEmailsQueue = {
add: sinon.stub().resolves(),
process: callback => {
this.queueProcessFunction = callback
},
}
this.Queues = {
createScheduledJob: sinon.stub().resolves(),
}
this.UserGetter = {
promises: {
getUser: sinon.stub().resolves(null),
},
}
this.UserGetter.promises.getUser
.withArgs({ _id: this.fakeUserId })
.resolves({
_id: this.fakeUserId,
email: this.fakeUserEmail,
})
this.EmailHandler = {
promises: {
sendEmail: sinon.stub().resolves(),
},
}
this.UserUpdater = {
promises: {
updateUser: sinon.stub().resolves(),
},
}
this.UserOnboardingEmailManager = SandboxedModule.require(MODULE_PATH, {
requires: {
'../../infrastructure/Queues': this.Queues,
'../Email/EmailHandler': this.EmailHandler,
'./UserGetter': this.UserGetter,
'./UserUpdater': this.UserUpdater,
'@overleaf/settings': (this.Settings = {
enableOnboardingEmails: true,
}),
},
})
})
describe('scheduleOnboardingEmail', function () {
it('should schedule delayed job on queue', async function () {
await this.UserOnboardingEmailManager.scheduleOnboardingEmail({
_id: this.fakeUserId,
})
sinon.assert.calledWith(
this.Queues.createScheduledJob,
'emails-onboarding',
{ data: { userId: this.fakeUserId } },
24 * 60 * 60 * 1000
)
})
})
describe('sendOnboardingEmail', function () {
describe('when onboarding emails are disabled', function () {
beforeEach(function () {
this.Settings.enableOnboardingEmails = false
})
it('should not send onboarding email', async function () {
await this.UserOnboardingEmailManager.sendOnboardingEmail(
this.fakeUserId
)
expect(this.EmailHandler.promises.sendEmail).not.to.have.been.called
expect(this.UserUpdater.promises.updateUser).not.to.have.been.called
})
})
describe('when onboarding emails are enabled', function () {
it('should send onboarding email and update user', async function () {
await this.UserOnboardingEmailManager.sendOnboardingEmail(
this.fakeUserId
)
expect(this.EmailHandler.promises.sendEmail).to.have.been.calledWith(
'userOnboardingEmail',
{
to: this.fakeUserEmail,
}
)
expect(this.UserUpdater.promises.updateUser).to.have.been.calledWith(
this.fakeUserId,
{ $set: { onboardingEmailSentAt: sinon.match.date } }
)
})
it('should stop if user is not found', async function () {
await this.UserOnboardingEmailManager.sendOnboardingEmail({
data: { userId: 'deleted-user' },
})
expect(this.EmailHandler.promises.sendEmail).not.to.have.been.called
expect(this.UserUpdater.promises.updateUser).not.to.have.been.called
})
})
})
})

View File

@@ -0,0 +1,125 @@
import { vi, expect } from 'vitest'
import sinon from 'sinon'
const MODULE_PATH =
'../../../../app/src/Features/User/UserPostRegistrationAnalyticsManager'
describe('UserPostRegistrationAnalyticsManager', function () {
beforeEach(async function (ctx) {
ctx.fakeUserId = '123abc'
ctx.Queues = {
createScheduledJob: sinon.stub().resolves(),
}
ctx.UserGetter = {
promises: {
getUser: sinon.stub().resolves(),
},
}
ctx.UserGetter.promises.getUser
.withArgs({ _id: ctx.fakeUserId })
.resolves({ _id: ctx.fakeUserId })
ctx.InstitutionsAPI = {
promises: {
getUserAffiliations: sinon.stub().resolves([]),
},
}
ctx.AnalyticsManager = {
setUserPropertyForUser: sinon.stub().resolves(),
}
vi.doMock('../../../../app/src/infrastructure/Queues', () => ({
default: ctx.Queues,
}))
vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({
default: ctx.UserGetter,
}))
vi.doMock(
'../../../../app/src/Features/Institutions/InstitutionsAPI',
() => ({
default: ctx.InstitutionsAPI,
})
)
vi.doMock(
'../../../../app/src/Features/Analytics/AnalyticsManager',
() => ({
default: ctx.AnalyticsManager,
})
)
ctx.UserPostRegistrationAnalyticsManager = (
await import(MODULE_PATH)
).default
})
describe('schedulePostRegistrationAnalytics', function () {
it('should schedule delayed job on queue', async function (ctx) {
await ctx.UserPostRegistrationAnalyticsManager.schedulePostRegistrationAnalytics(
{
_id: ctx.fakeUserId,
}
)
sinon.assert.calledWith(
ctx.Queues.createScheduledJob,
'post-registration-analytics',
{ data: { userId: ctx.fakeUserId } },
24 * 60 * 60 * 1000
)
})
})
describe('postRegistrationAnalytics', function () {
it('stops without errors if user is not found', async function (ctx) {
await ctx.UserPostRegistrationAnalyticsManager.postRegistrationAnalytics(
'not-a-user'
)
expect(ctx.InstitutionsAPI.promises.getUserAffiliations).not.to.have.been
.called
expect(ctx.AnalyticsManager.setUserPropertyForUser).not.to.have.been
.called
})
it('sets user property if user has commons account affiliationd', async function (ctx) {
ctx.InstitutionsAPI.promises.getUserAffiliations.resolves([
{},
{
institution: {
commonsAccount: true,
},
},
{
institution: {
commonsAccount: false,
},
},
])
await ctx.UserPostRegistrationAnalyticsManager.postRegistrationAnalytics(
ctx.fakeUserId
)
expect(
ctx.AnalyticsManager.setUserPropertyForUser
).to.have.been.calledWith(
ctx.fakeUserId,
'registered-from-commons-account',
true
)
})
it('does not set user property if user has no commons account affiliation', async function (ctx) {
ctx.InstitutionsAPI.promises.getUserAffiliations.resolves([
{
institution: {
commonsAccount: false,
},
},
])
await ctx.UserPostRegistrationAnalyticsManager.postRegistrationAnalytics(
ctx.fakeUserId
)
expect(ctx.AnalyticsManager.setUserPropertyForUser).not.to.have.been
.called
})
})
})

View File

@@ -1,114 +0,0 @@
const SandboxedModule = require('sandboxed-module')
const path = require('path')
const sinon = require('sinon')
const { expect } = require('chai')
const MODULE_PATH = path.join(
__dirname,
'../../../../app/src/Features/User/UserPostRegistrationAnalyticsManager'
)
describe('UserPostRegistrationAnalyticsManager', function () {
beforeEach(function () {
this.fakeUserId = '123abc'
this.Queues = {
createScheduledJob: sinon.stub().resolves(),
}
this.UserGetter = {
promises: {
getUser: sinon.stub().resolves(),
},
}
this.UserGetter.promises.getUser
.withArgs({ _id: this.fakeUserId })
.resolves({ _id: this.fakeUserId })
this.InstitutionsAPI = {
promises: {
getUserAffiliations: sinon.stub().resolves([]),
},
}
this.AnalyticsManager = {
setUserPropertyForUser: sinon.stub().resolves(),
}
this.UserPostRegistrationAnalyticsManager = SandboxedModule.require(
MODULE_PATH,
{
requires: {
'../../infrastructure/Queues': this.Queues,
'./UserGetter': this.UserGetter,
'../Institutions/InstitutionsAPI': this.InstitutionsAPI,
'../Analytics/AnalyticsManager': this.AnalyticsManager,
},
}
)
})
describe('schedulePostRegistrationAnalytics', function () {
it('should schedule delayed job on queue', async function () {
await this.UserPostRegistrationAnalyticsManager.schedulePostRegistrationAnalytics(
{
_id: this.fakeUserId,
}
)
sinon.assert.calledWith(
this.Queues.createScheduledJob,
'post-registration-analytics',
{ data: { userId: this.fakeUserId } },
24 * 60 * 60 * 1000
)
})
})
describe('postRegistrationAnalytics', function () {
it('stops without errors if user is not found', async function () {
await this.UserPostRegistrationAnalyticsManager.postRegistrationAnalytics(
'not-a-user'
)
expect(this.InstitutionsAPI.promises.getUserAffiliations).not.to.have.been
.called
expect(this.AnalyticsManager.setUserPropertyForUser).not.to.have.been
.called
})
it('sets user property if user has commons account affiliationd', async function () {
this.InstitutionsAPI.promises.getUserAffiliations.resolves([
{},
{
institution: {
commonsAccount: true,
},
},
{
institution: {
commonsAccount: false,
},
},
])
await this.UserPostRegistrationAnalyticsManager.postRegistrationAnalytics(
this.fakeUserId
)
expect(
this.AnalyticsManager.setUserPropertyForUser
).to.have.been.calledWith(
this.fakeUserId,
'registered-from-commons-account',
true
)
})
it('does not set user property if user has no commons account affiliation', async function () {
this.InstitutionsAPI.promises.getUserAffiliations.resolves([
{
institution: {
commonsAccount: false,
},
},
])
await this.UserPostRegistrationAnalyticsManager.postRegistrationAnalytics(
this.fakeUserId
)
expect(this.AnalyticsManager.setUserPropertyForUser).not.to.have.been
.called
})
})
})

View File

@@ -1,25 +1,18 @@
import { expect, vi } from 'vitest'
import sinon from 'sinon'
import MockRequest from '../helpers/MockRequest.js'
import MockResponse from '../helpers/MockResponse.js'
import { expect, vi, describe, it, beforeEach } from 'vitest'
import MockRequest from '../helpers/MockRequestVitest.mjs'
import MockResponse from '../helpers/MockResponseVitest.mjs'
import EntityConfigs from '../../../../app/src/Features/UserMembership/UserMembershipEntityConfigs.js'
import Errors from '../../../../app/src/Features/Errors/Errors.js'
import {
UserIsManagerError,
UserNotFoundError,
UserAlreadyAddedError,
} from '../../../../app/src/Features/UserMembership/UserMembershipErrors.js'
const assertCalledWith = sinon.assert.calledWith
import UserMembershipErrors from '../../../../app/src/Features/UserMembership/UserMembershipErrors.mjs'
const modulePath =
'../../../../app/src/Features/UserMembership/UserMembershipController.mjs'
vi.mock(
'../../../../app/src/Features/UserMembership/UserMembershipErrors.js',
'../../../../app/src/Features/UserMembership/UserMembershipErrors.mjs',
() =>
vi.importActual(
'../../../../app/src/Features/UserMembership/UserMembershipErrors.js'
'../../../../app/src/Features/UserMembership/UserMembershipErrors.mjs'
)
)
@@ -27,9 +20,9 @@ vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
vi.importActual('../../../../app/src/Features/Errors/Errors.js')
)
describe('UserMembershipController', function () {
beforeEach(async function (ctx) {
ctx.req = new MockRequest()
describe('UserMembershipController', () => {
beforeEach(async ctx => {
ctx.req = new MockRequest(vi)
ctx.req.params.id = 'mock-entity-id'
ctx.user = { _id: 'mock-user-id' }
ctx.newUser = { _id: 'mock-new-user-id', email: 'new-user-email@foo.bar' }
@@ -37,16 +30,16 @@ describe('UserMembershipController', function () {
_id: 'mock-subscription-id',
admin_id: 'mock-admin-id',
manager_ids: ['mock-admin-id'],
fetchV1Data: callback => callback(null, ctx.subscription),
fetchV1Data: vi.fn(callback => callback(null, ctx.subscription)),
}
ctx.institution = {
_id: 'mock-institution-id',
v1Id: 123,
fetchV1Data: callback => {
const institution = Object.assign({}, ctx.institution)
fetchV1Data: vi.fn(callback => {
const institution = { ...ctx.institution }
institution.name = 'Test Institution Name'
callback(null, institution)
},
}),
managerIds: ['mock-member-id-1'],
}
ctx.users = [
@@ -113,45 +106,48 @@ describe('UserMembershipController', function () {
}
ctx.SessionManager = {
getSessionUser: sinon.stub().returns(ctx.user),
getLoggedInUserId: sinon.stub().returns(ctx.user._id),
getSessionUser: vi.fn().mockReturnValue(ctx.user),
getLoggedInUserId: vi.fn().mockReturnValue(ctx.user._id),
}
ctx.SSOConfig = {
findById: sinon
.stub()
.returns({ exec: sinon.stub().resolves({ enabled: true }) }),
findById: vi.fn().mockReturnValue({
exec: vi.fn().mockResolvedValue({ enabled: true }),
}),
}
ctx.UserMembershipHandler = {
getEntity: sinon.stub().yields(null, ctx.subscription),
createEntity: sinon.stub().yields(null, ctx.institution),
getUsers: sinon.stub().yields(null, ctx.users),
addUser: sinon.stub().yields(null, ctx.newUser),
removeUser: sinon.stub().yields(null),
getEntity: vi.fn((_entity, _options, callback) =>
callback(null, ctx.subscription)
),
createEntity: vi.fn((_entity, _options, callback) =>
callback(null, ctx.institution)
),
getUsers: vi.fn((_entity, _options, callback) =>
callback(null, ctx.users)
),
addUser: vi.fn((_entity, _options, _email, callback) =>
callback(null, ctx.newUser)
),
removeUser: vi.fn((_entity, _options, _userId, callback) =>
callback(null)
),
promises: {
getUsers: sinon.stub().resolves(ctx.users),
getUsers: vi.fn().mockResolvedValue(ctx.users),
},
}
ctx.SplitTestHandler = {
promises: {
getAssignment: sinon.stub().resolves({ variant: 'default' }),
getAssignment: vi.fn().mockResolvedValue({ variant: 'default' }),
},
getAssignment: sinon.stub().yields(null, { variant: 'default' }),
getAssignment: vi.fn((_testName, _userId, callback) =>
callback(null, { variant: 'default' })
),
}
ctx.RecurlyClient = {
promises: {
getSubscription: sinon.stub().resolves({}),
getSubscription: vi.fn().mockResolvedValue({}),
},
}
vi.doMock(
'../../../../app/src/Features/UserMembership/UserMembershipErrors',
() => ({
UserIsManagerError,
UserNotFoundError,
UserAlreadyAddedError,
})
)
vi.doMock(
'../../../../app/src/Features/Authentication/SessionManager',
() => ({
@@ -191,7 +187,7 @@ describe('UserMembershipController', function () {
ctx.Modules = {
promises: {
hooks: {
fire: sinon.stub(),
fire: vi.fn(),
},
},
}
@@ -202,55 +198,90 @@ describe('UserMembershipController', function () {
ctx.UserMembershipController = (await import(modulePath)).default
})
describe('index', function () {
beforeEach(function (ctx) {
describe('index', () => {
beforeEach(ctx => {
ctx.req.user = ctx.user
ctx.req.entity = ctx.subscription
ctx.req.entityConfig = EntityConfigs.group
ctx.Modules.promises.hooks.fire.resolves([])
ctx.Modules.promises.hooks.fire.mockResolvedValue([])
})
it('get users', async function (ctx) {
await ctx.UserMembershipController.manageGroupMembers(ctx.req, {
it('get users', async ({
UserMembershipController,
req,
UserMembershipHandler,
subscription,
}) => {
expect.assertions(1)
await UserMembershipController.manageGroupMembers(req, {
render: () => {
sinon.assert.calledWithMatch(
ctx.UserMembershipHandler.promises.getUsers,
ctx.subscription,
{ modelName: 'Subscription' }
expect(UserMembershipHandler.promises.getUsers).toHaveBeenCalledWith(
subscription,
{
modelName: 'Subscription',
baseQuery: { groupPlan: true },
fields: {
access: 'manager_ids',
membership: 'member_ids',
name: 'teamName',
primaryKey: '_id',
read: ['invited_emails', 'teamInvites', 'member_ids'],
write: null,
},
hasMembersLimit: true,
readOnly: true,
}
)
},
})
})
it('render group view', async function (ctx) {
ctx.subscription.managedUsersEnabled = false
await ctx.UserMembershipController.manageGroupMembers(ctx.req, {
it('render group view', async ({
UserMembershipController,
req,
subscription,
users,
}) => {
expect.assertions(4)
subscription.managedUsersEnabled = false
await UserMembershipController.manageGroupMembers(req, {
render: (viewPath, viewParams) => {
expect(viewPath).to.equal('user_membership/group-members-react')
expect(viewParams.users).to.deep.equal(ctx.users)
expect(viewParams.groupSize).to.equal(ctx.subscription.membersLimit)
expect(viewParams.users).to.deep.equal(users)
expect(viewParams.groupSize).to.equal(subscription.membersLimit)
expect(viewParams.managedUsersActive).to.equal(false)
},
})
})
it('render group view with managed users', async function (ctx) {
ctx.subscription.managedUsersEnabled = true
await ctx.UserMembershipController.manageGroupMembers(ctx.req, {
it('render group view with managed users', async ({
UserMembershipController,
req,
subscription,
users,
}) => {
expect.assertions(5)
subscription.managedUsersEnabled = true
await UserMembershipController.manageGroupMembers(req, {
render: (viewPath, viewParams) => {
expect(viewPath).to.equal('user_membership/group-members-react')
expect(viewParams.users).to.deep.equal(ctx.users)
expect(viewParams.groupSize).to.equal(ctx.subscription.membersLimit)
expect(viewParams.users).to.deep.equal(users)
expect(viewParams.groupSize).to.equal(subscription.membersLimit)
expect(viewParams.managedUsersActive).to.equal(true)
expect(viewParams.isUserGroupManager).to.equal(false)
},
})
})
it('render group managers view', async function (ctx) {
ctx.req.user = ctx.user
ctx.req.entityConfig = EntityConfigs.groupManagers
await ctx.UserMembershipController.manageGroupManagers(ctx.req, {
it('render group managers view', async ({
UserMembershipController,
req,
user,
}) => {
expect.assertions(2)
req.user = user
req.entityConfig = EntityConfigs.groupManagers
await UserMembershipController.manageGroupManagers(req, {
render: (viewPath, viewParams) => {
expect(viewPath).to.equal('user_membership/group-managers-react')
expect(viewParams.groupSize).to.equal(undefined)
@@ -258,11 +289,17 @@ describe('UserMembershipController', function () {
})
})
it('render institution view', async function (ctx) {
ctx.req.user = ctx.user
ctx.req.entity = ctx.institution
ctx.req.entityConfig = EntityConfigs.institution
await ctx.UserMembershipController.manageInstitutionManagers(ctx.req, {
it('render institution view', async ({
UserMembershipController,
req,
user,
institution,
}) => {
expect.assertions(3)
req.user = user
req.entity = institution
req.entityConfig = EntityConfigs.institution
await UserMembershipController.manageInstitutionManagers(req, {
render: (viewPath, viewParams) => {
expect(viewPath).to.equal(
'user_membership/institution-managers-react'
@@ -274,22 +311,40 @@ describe('UserMembershipController', function () {
})
})
describe('add', function () {
beforeEach(function (ctx) {
describe('add', () => {
beforeEach(ctx => {
ctx.req.body.email = ctx.newUser.email
ctx.req.entity = ctx.subscription
ctx.req.entityConfig = EntityConfigs.groupManagers
})
it('add user', async function (ctx) {
it('add user', async ({
UserMembershipController,
req,
UserMembershipHandler,
subscription,
newUser,
}) => {
expect.assertions(1)
await new Promise(resolve => {
ctx.UserMembershipController.add(ctx.req, {
UserMembershipController.add(req, {
json: () => {
sinon.assert.calledWithMatch(
ctx.UserMembershipHandler.addUser,
ctx.subscription,
{ modelName: 'Subscription' },
ctx.newUser.email
expect(UserMembershipHandler.addUser).toHaveBeenCalledWith(
subscription,
{
modelName: 'Subscription',
baseQuery: { groupPlan: true },
fields: {
access: 'manager_ids',
membership: 'member_ids',
name: 'teamName',
primaryKey: '_id',
read: ['manager_ids'],
write: 'manager_ids',
},
},
newUser.email,
expect.any(Function)
)
resolve()
},
@@ -297,21 +352,27 @@ describe('UserMembershipController', function () {
})
})
it('return user object', async function (ctx) {
it('return user object', async ({
UserMembershipController,
req,
newUser,
}) => {
expect.assertions(1)
await new Promise(resolve => {
ctx.UserMembershipController.add(ctx.req, {
UserMembershipController.add(req, {
json: payload => {
payload.user.should.equal(ctx.newUser)
expect(payload.user).to.equal(newUser)
resolve()
},
})
})
})
it('handle readOnly entity', async function (ctx) {
it('handle readOnly entity', async ({ UserMembershipController, req }) => {
expect.assertions(2)
req.entityConfig = EntityConfigs.group
await new Promise(resolve => {
ctx.req.entityConfig = EntityConfigs.group
ctx.UserMembershipController.add(ctx.req, null, error => {
UserMembershipController.add(req, null, error => {
expect(error).to.exist
expect(error).to.be.an.instanceof(Errors.NotFoundError)
resolve()
@@ -319,24 +380,47 @@ describe('UserMembershipController', function () {
})
})
it('handle user already added', async function (ctx) {
it('handle user already added', async ({
UserMembershipController,
req,
UserMembershipHandler,
}) => {
expect.assertions(1)
UserMembershipHandler.addUser.mockImplementation(
(_entity, _options, _email, callback) => {
callback(new UserMembershipErrors.UserAlreadyAddedError())
}
)
await new Promise(resolve => {
ctx.UserMembershipHandler.addUser.yields(new UserAlreadyAddedError())
ctx.UserMembershipController.add(ctx.req, {
status: () => ({
json: payload => {
expect(payload.error.code).to.equal('user_already_added')
resolve()
},
}),
})
UserMembershipController.add(
req,
{
status: () => ({
json: payload => {
expect(payload.error.code).to.equal('user_already_added')
resolve()
},
}),
},
() => {}
)
})
})
it('handle user not found', async function (ctx) {
it('handle user not found', async ({
UserMembershipController,
req,
UserMembershipHandler,
}) => {
expect.assertions(1)
UserMembershipHandler.addUser.mockImplementation(
(_entity, _options, _email, callback) => {
callback(new UserMembershipErrors.UserNotFoundError())
}
)
await new Promise(resolve => {
ctx.UserMembershipHandler.addUser.yields(new UserNotFoundError())
ctx.UserMembershipController.add(ctx.req, {
UserMembershipController.add(req, {
status: () => ({
json: payload => {
expect(payload.error.code).to.equal('user_not_found')
@@ -347,10 +431,11 @@ describe('UserMembershipController', function () {
})
})
it('handle invalid email', async function (ctx) {
it('handle invalid email', async ({ UserMembershipController, req }) => {
expect.assertions(1)
req.body.email = 'not_valid_email'
await new Promise(resolve => {
ctx.req.body.email = 'not_valid_email'
ctx.UserMembershipController.add(ctx.req, {
UserMembershipController.add(req, {
status: () => ({
json: payload => {
expect(payload.error.code).to.equal('invalid_email')
@@ -362,33 +447,52 @@ describe('UserMembershipController', function () {
})
})
describe('remove', function () {
beforeEach(function (ctx) {
describe('remove', () => {
beforeEach(ctx => {
ctx.req.params.userId = ctx.newUser._id
ctx.req.entity = ctx.subscription
ctx.req.entityConfig = EntityConfigs.groupManagers
})
it('remove user', async function (ctx) {
await new Promise(resolve => {
ctx.UserMembershipController.remove(ctx.req, {
sendStatus: () => {
sinon.assert.calledWithMatch(
ctx.UserMembershipHandler.removeUser,
ctx.subscription,
{ modelName: 'Subscription' },
ctx.newUser._id
)
resolve()
},
})
it('remove user', async ({
UserMembershipController,
req,
UserMembershipHandler,
subscription,
newUser,
}) => {
expect.assertions(1)
await UserMembershipController.remove(req, {
sendStatus: () => {
expect(UserMembershipHandler.removeUser).toHaveBeenCalledWith(
subscription,
{
modelName: 'Subscription',
baseQuery: {
groupPlan: true,
},
fields: {
access: 'manager_ids',
membership: 'member_ids',
name: 'teamName',
primaryKey: '_id',
read: ['manager_ids'],
write: 'manager_ids',
},
},
newUser._id,
expect.any(Function)
)
},
})
})
it('handle readOnly entity', async function (ctx) {
it('handle readOnly entity', async ({ UserMembershipController, req }) => {
expect.assertions(2)
req.entityConfig = EntityConfigs.group
await new Promise(resolve => {
ctx.req.entityConfig = EntityConfigs.group
ctx.UserMembershipController.remove(ctx.req, null, error => {
UserMembershipController.remove(req, null, error => {
expect(error).to.exist
expect(error).to.be.an.instanceof(Errors.NotFoundError)
resolve()
@@ -396,172 +500,194 @@ describe('UserMembershipController', function () {
})
})
it('prevent self removal', async function (ctx) {
await new Promise(resolve => {
ctx.req.params.userId = ctx.user._id
ctx.UserMembershipController.remove(ctx.req, {
status: () => ({
json: payload => {
expect(payload.error.code).to.equal('managers_cannot_remove_self')
resolve()
},
}),
})
it('prevent self removal', async ({
UserMembershipController,
req,
user,
}) => {
expect.assertions(1)
req.params.userId = user._id
await UserMembershipController.remove(req, {
status: () => ({
json: payload => {
expect(payload.error.code).to.equal('managers_cannot_remove_self')
},
}),
})
})
it('prevent admin removal', async function (ctx) {
await new Promise(resolve => {
ctx.UserMembershipHandler.removeUser.yields(new UserIsManagerError())
ctx.UserMembershipController.remove(ctx.req, {
status: () => ({
json: payload => {
expect(payload.error.code).to.equal(
'managers_cannot_remove_admin'
)
resolve()
},
}),
})
it('prevent admin removal', async ({
UserMembershipController,
req,
UserMembershipHandler,
}) => {
expect.assertions(1)
UserMembershipHandler.removeUser.mockImplementation(
(_entity, _options, _userId, callback) => {
callback(new UserMembershipErrors.UserIsManagerError())
}
)
await UserMembershipController.remove(req, {
status: () => ({
json: payload => {
expect(payload.error.code).to.equal('managers_cannot_remove_admin')
},
}),
})
})
})
describe('exportCsv', function () {
beforeEach(function (ctx) {
describe('exportCsv', () => {
beforeEach(ctx => {
ctx.req.entity = ctx.subscription
ctx.req.entityConfig = EntityConfigs.groupManagers
ctx.res = new MockResponse()
ctx.res = new MockResponse(vi)
ctx.UserMembershipController.exportCsv(ctx.req, ctx.res)
})
it('get users', function (ctx) {
sinon.assert.calledWithMatch(
ctx.UserMembershipHandler.promises.getUsers,
ctx.subscription,
{ modelName: 'Subscription' }
it('get users', ({ UserMembershipHandler, subscription }) => {
expect(UserMembershipHandler.promises.getUsers).toHaveBeenCalledWith(
subscription,
{
modelName: 'Subscription',
baseQuery: { groupPlan: true },
fields: {
access: 'manager_ids',
membership: 'member_ids',
name: 'teamName',
primaryKey: '_id',
read: ['manager_ids'],
write: 'manager_ids',
},
}
)
})
it('should set the correct content type on the request', function (ctx) {
assertCalledWith(ctx.res.contentType, 'text/csv; charset=utf-8')
it('should set the correct content type on the request', ({ res }) => {
expect(res.contentType).toHaveBeenCalledWith('text/csv; charset=utf-8')
})
it('should name the exported csv file', function (ctx) {
assertCalledWith(
ctx.res.header,
it('should name the exported csv file', ({ res }) => {
expect(res.header).toHaveBeenCalledWith(
'Content-Disposition',
'attachment; filename="Group.csv"'
)
})
it('should export the correct csv', function (ctx) {
assertCalledWith(
ctx.res.send,
it('should export the correct csv', ({ res }) => {
expect(res.send).toHaveBeenCalledWith(
'"email","last_logged_in_at","last_active_at"\n"mock-email-1@foo.com","2020-08-09T12:43:11.467Z","2021-08-09T12:43:11.467Z"\n"mock-email-2@foo.com","2020-05-20T10:41:11.407Z","2021-05-20T10:41:11.407Z"\n"mock-email-3@foo.com","2021-08-10T10:41:11.407Z","2021-08-20T10:41:11.407Z"\n"mock-email-4@foo.com","2021-01-01T10:41:11.407Z","2021-01-02T10:41:11.407Z"\n"mock-email-5@foo.com","2023-01-01T10:41:11.407Z","2023-01-02T10:41:11.407Z"\n"mock-email-6@foo.com","2024-01-01T10:41:11.407Z","2024-01-02T10:41:11.407Z"'
)
})
})
describe('exportCsv when group is managed', function () {
beforeEach(function (ctx) {
describe('exportCsv when group is managed', () => {
beforeEach(ctx => {
ctx.req.entity = Object.assign(
{ managedUsersEnabled: true },
ctx.subscription
)
ctx.req.entityConfig = EntityConfigs.groupManagers
ctx.res = new MockResponse()
ctx.res = new MockResponse(vi)
ctx.UserMembershipController.exportCsv(ctx.req, ctx.res)
})
it('should export the correct csv', function (ctx) {
assertCalledWith(
ctx.res.send,
it('should export the correct csv', ({ res }) => {
expect(res.send).toHaveBeenCalledWith(
'"email","last_logged_in_at","last_active_at","managed"\n"mock-email-1@foo.com","2020-08-09T12:43:11.467Z","2021-08-09T12:43:11.467Z",false\n"mock-email-2@foo.com","2020-05-20T10:41:11.407Z","2021-05-20T10:41:11.407Z",false\n"mock-email-3@foo.com","2021-08-10T10:41:11.407Z","2021-08-20T10:41:11.407Z",false\n"mock-email-4@foo.com","2021-01-01T10:41:11.407Z","2021-01-02T10:41:11.407Z",true\n"mock-email-5@foo.com","2023-01-01T10:41:11.407Z","2023-01-02T10:41:11.407Z",false\n"mock-email-6@foo.com","2024-01-01T10:41:11.407Z","2024-01-02T10:41:11.407Z",true'
)
})
})
describe('exportCsv when group has SSO', function () {
beforeEach(function (ctx) {
describe('exportCsv when group has SSO', () => {
beforeEach(ctx => {
ctx.req.entity = Object.assign(
{ ssoConfig: 'sso-config-id' },
ctx.subscription
)
ctx.req.entityConfig = EntityConfigs.groupManagers
ctx.Modules.promises.hooks.fire.resolves([true])
ctx.res = new MockResponse()
ctx.Modules.promises.hooks.fire.mockResolvedValue([true])
ctx.res = new MockResponse(vi)
ctx.UserMembershipController.exportCsv(ctx.req, ctx.res)
})
it('should export the correct csv', function (ctx) {
assertCalledWith(
ctx.res.send,
it('should export the correct csv', ({ res }) => {
expect(res.send).toHaveBeenCalledWith(
'"email","last_logged_in_at","last_active_at","sso"\n"mock-email-1@foo.com","2020-08-09T12:43:11.467Z","2021-08-09T12:43:11.467Z",false\n"mock-email-2@foo.com","2020-05-20T10:41:11.407Z","2021-05-20T10:41:11.407Z",false\n"mock-email-3@foo.com","2021-08-10T10:41:11.407Z","2021-08-20T10:41:11.407Z",false\n"mock-email-4@foo.com","2021-01-01T10:41:11.407Z","2021-01-02T10:41:11.407Z",false\n"mock-email-5@foo.com","2023-01-01T10:41:11.407Z","2023-01-02T10:41:11.407Z",true\n"mock-email-6@foo.com","2024-01-01T10:41:11.407Z","2024-01-02T10:41:11.407Z",true'
)
})
})
describe('exportCsv when group has SSO and managed users enabled', function () {
beforeEach(function (ctx) {
describe('exportCsv when group has SSO and managed users enabled', () => {
beforeEach(ctx => {
ctx.req.entity = Object.assign(
{ managedUsersEnabled: true },
{ ssoConfig: 'sso-config-id' },
ctx.subscription
)
ctx.req.entityConfig = EntityConfigs.groupManagers
ctx.Modules.promises.hooks.fire.resolves([true])
ctx.res = new MockResponse()
ctx.Modules.promises.hooks.fire.mockResolvedValue([true])
ctx.res = new MockResponse(vi)
ctx.UserMembershipController.exportCsv(ctx.req, ctx.res)
})
it('should export the correct csv', function (ctx) {
assertCalledWith(
ctx.res.send,
it('should export the correct csv', ({ res }) => {
expect(res.send).toHaveBeenCalledWith(
'"email","last_logged_in_at","last_active_at","managed","sso"\n"mock-email-1@foo.com","2020-08-09T12:43:11.467Z","2021-08-09T12:43:11.467Z",false,false\n"mock-email-2@foo.com","2020-05-20T10:41:11.407Z","2021-05-20T10:41:11.407Z",false,false\n"mock-email-3@foo.com","2021-08-10T10:41:11.407Z","2021-08-20T10:41:11.407Z",false,false\n"mock-email-4@foo.com","2021-01-01T10:41:11.407Z","2021-01-02T10:41:11.407Z",true,false\n"mock-email-5@foo.com","2023-01-01T10:41:11.407Z","2023-01-02T10:41:11.407Z",false,true\n"mock-email-6@foo.com","2024-01-01T10:41:11.407Z","2024-01-02T10:41:11.407Z",true,true'
)
})
})
describe('new', function () {
beforeEach(function (ctx) {
describe('new', () => {
beforeEach(ctx => {
ctx.req.params.name = 'publisher'
ctx.req.params.id = 'abc'
})
it('renders view', async function (ctx) {
await new Promise(resolve => {
ctx.UserMembershipController.new(ctx.req, {
render: (viewPath, data) => {
expect(data.entityName).to.eq('publisher')
expect(data.entityId).to.eq('abc')
resolve()
},
})
it('renders view', async ({ UserMembershipController, req }) => {
expect.assertions(2)
await UserMembershipController.new(req, {
render: (viewPath, data) => {
expect(data.entityName).to.eq('publisher')
expect(data.entityId).to.eq('abc')
},
})
})
})
describe('create', function () {
beforeEach(function (ctx) {
describe('create', () => {
beforeEach(ctx => {
ctx.req.params.name = 'institution'
ctx.req.entityConfig = EntityConfigs.institution
ctx.req.params.id = 123
})
it('creates institution', async function (ctx) {
await new Promise(resolve => {
ctx.UserMembershipController.create(ctx.req, {
redirect: path => {
expect(path).to.eq(EntityConfigs.institution.pathsFor(123).index)
sinon.assert.calledWithMatch(
ctx.UserMembershipHandler.createEntity,
123,
{ modelName: 'Institution' }
)
resolve()
},
})
it('creates institution', async ({
UserMembershipController,
req,
UserMembershipHandler,
}) => {
expect.assertions(2)
await UserMembershipController.create(req, {
redirect: path => {
expect(path).to.eq(EntityConfigs.institution.pathsFor(123).index)
expect(UserMembershipHandler.createEntity).toHaveBeenCalledWith(
123,
{
fields: {
access: 'managerIds',
membership: 'member_ids',
name: 'name',
primaryKey: 'v1Id',
read: ['managerIds'],
write: 'managerIds',
},
modelName: 'Institution',
pathsFor: EntityConfigs.institution.pathsFor,
},
expect.any(Function)
)
},
})
})
})

View File

@@ -0,0 +1,287 @@
import { vi, expect } from 'vitest'
import mongodb from 'mongodb-legacy'
import EntityConfigs from '../../../../app/src/Features/UserMembership/UserMembershipEntityConfigs.js'
import UserMembershipErrors from '../../../../app/src/Features/UserMembership/UserMembershipErrors.mjs'
const { ObjectId } = mongodb
const modulePath =
'../../../../app/src/Features/UserMembership/UserMembershipHandler'
const serializeIds = ids =>
ids.map(id => (id instanceof ObjectId ? `objectId-${id.toString()}` : id))
vi.mock(
'../../../../app/src/Features/UserMembership/UserMembershipErrors.mjs',
() =>
vi.importActual(
'../../../../app/src/Features/UserMembership/UserMembershipErrors.mjs'
)
)
describe('UserMembershipHandler', function () {
beforeEach(async function (ctx) {
ctx.user = { _id: new ObjectId() }
ctx.newUser = { _id: new ObjectId(), email: 'new-user-email@foo.bar' }
ctx.fakeEntityId = new ObjectId()
ctx.subscription = {
_id: 'mock-subscription-id',
groupPlan: true,
membersLimit: 10,
member_ids: [new ObjectId(), new ObjectId()],
manager_ids: [new ObjectId()],
invited_emails: ['mock-email-1@foo.com'],
teamInvites: [{ email: 'mock-email-1@bar.com' }],
update: vi.fn().mockReturnValue({
exec: vi.fn().mockResolvedValue(),
}),
}
ctx.institution = {
_id: 'mock-institution-id',
v1Id: 123,
managerIds: [new ObjectId(), new ObjectId(), new ObjectId()],
updateOne: vi.fn().mockReturnValue({
exec: vi.fn().mockResolvedValue(),
}),
}
ctx.publisher = {
_id: 'mock-publisher-id',
slug: 'slug',
managerIds: [new ObjectId(), new ObjectId()],
updateOne: vi.fn().mockReturnValue({
exec: vi.fn().mockResolvedValue(),
}),
}
ctx.UserMembershipViewModel = {
promises: {
buildAsync: vi.fn().mockResolvedValue([{ _id: 'mock-member-id' }]),
},
build: vi.fn().mockReturnValue(ctx.newUser),
}
ctx.UserGetter = {
promises: {
getUserByAnyEmail: vi.fn().mockResolvedValue(ctx.newUser),
},
}
ctx.Institution = {
findOne: vi.fn().mockReturnValue({
exec: vi.fn().mockResolvedValue(ctx.institution),
}),
}
ctx.Subscription = {
findOne: vi.fn().mockReturnValue({
exec: vi.fn().mockResolvedValue(ctx.subscription),
}),
}
ctx.Publisher = {
findOne: vi.fn().mockReturnValue({
exec: vi.fn().mockResolvedValue(ctx.publisher),
}),
create: vi.fn().mockReturnValue({
exec: vi.fn().mockResolvedValue(ctx.publisher),
}),
}
vi.doMock('mongodb-legacy', () => ({
default: { ObjectId },
}))
vi.doMock(
'../../../../app/src/Features/UserMembership/UserMembershipViewModel',
() => ({
default: ctx.UserMembershipViewModel,
})
)
vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({
default: ctx.UserGetter,
}))
vi.doMock('../../../../app/src/models/Institution', () => ({
Institution: ctx.Institution,
}))
vi.doMock('../../../../app/src/models/Subscription', () => ({
Subscription: ctx.Subscription,
}))
vi.doMock('../../../../app/src/models/Publisher', () => ({
Publisher: ctx.Publisher,
}))
ctx.UserMembershipHandler = (await import(modulePath)).default
})
describe('getEntityWithoutAuthorizationCheck', function () {
it('get publisher', async function (ctx) {
const subscription =
await ctx.UserMembershipHandler.promises.getEntityWithoutAuthorizationCheck(
ctx.fakeEntityId,
EntityConfigs.publisher
)
const expectedQuery = { slug: ctx.fakeEntityId }
expect(ctx.Publisher.findOne).toHaveBeenCalledWith(expectedQuery)
expect(subscription).to.equal(ctx.publisher)
})
})
describe('getUsers', function () {
describe('group', function () {
it('build view model for all users', async function (ctx) {
await ctx.UserMembershipHandler.promises.getUsers(
ctx.subscription,
EntityConfigs.group
)
expect(
serializeIds(
ctx.UserMembershipViewModel.promises.buildAsync.mock.calls[0][0]
)
).toEqual(
serializeIds(
ctx.subscription.invited_emails.concat(
ctx.subscription.teamInvites[0].email,
ctx.subscription.member_ids
)
)
)
})
})
describe('group managers', function () {
it('build view model for all managers', async function (ctx) {
await ctx.UserMembershipHandler.promises.getUsers(
ctx.subscription,
EntityConfigs.groupManagers
)
expect(
serializeIds(
ctx.UserMembershipViewModel.promises.buildAsync.mock.calls[0][0]
)
).toEqual(serializeIds(ctx.subscription.manager_ids))
})
})
describe('institution', function () {
it('build view model for all managers', async function (ctx) {
await ctx.UserMembershipHandler.promises.getUsers(
ctx.institution,
EntityConfigs.institution
)
expect(
serializeIds(
ctx.UserMembershipViewModel.promises.buildAsync.mock.calls[0][0]
)
).toEqual(serializeIds(ctx.institution.managerIds))
})
})
})
describe('createEntity', function () {
it('creates publisher', async function (ctx) {
await ctx.UserMembershipHandler.promises.createEntity(
ctx.fakeEntityId,
EntityConfigs.publisher
)
expect(ctx.Publisher.create).toHaveBeenCalledWith({
slug: ctx.fakeEntityId,
})
})
})
describe('addUser', function () {
beforeEach(function (ctx) {
ctx.email = ctx.newUser.email
})
describe('institution', function () {
it('get user', async function (ctx) {
await ctx.UserMembershipHandler.promises.addUser(
ctx.institution,
EntityConfigs.institution,
ctx.email
)
expect(ctx.UserGetter.promises.getUserByAnyEmail).toHaveBeenCalledWith(
ctx.email
)
})
it('handle user not found', async function (ctx) {
ctx.UserGetter.promises.getUserByAnyEmail.mockResolvedValue(null)
try {
await ctx.UserMembershipHandler.promises.addUser(
ctx.institution,
EntityConfigs.institution,
ctx.email
)
expect.fail('Expected addUser to throw')
} catch (err) {
expect(err).toBeInstanceOf(UserMembershipErrors.UserNotFoundError)
}
})
it('handle user already added', async function (ctx) {
ctx.institution.managerIds.push(ctx.newUser._id)
try {
await ctx.UserMembershipHandler.promises.addUser(
ctx.institution,
EntityConfigs.institution,
ctx.email
)
expect.fail('Expected addUser to throw')
} catch (err) {
expect(err).toBeInstanceOf(UserMembershipErrors.UserAlreadyAddedError)
}
})
it('add user to institution', async function (ctx) {
await ctx.UserMembershipHandler.promises.addUser(
ctx.institution,
EntityConfigs.institution,
ctx.email
)
expect(ctx.institution.updateOne).toHaveBeenCalledWith({
$addToSet: { managerIds: ctx.newUser._id },
})
})
it('return user view', async function (ctx) {
const user = await ctx.UserMembershipHandler.promises.addUser(
ctx.institution,
EntityConfigs.institution,
ctx.email
)
expect(user).to.equal(ctx.newUser)
})
})
})
describe('removeUser', function () {
describe('institution', function () {
it('remove user from institution', async function (ctx) {
await ctx.UserMembershipHandler.promises.removeUser(
ctx.institution,
EntityConfigs.institution,
ctx.newUser._id
)
expect(ctx.institution.updateOne).toHaveBeenCalledWith({
$pull: { managerIds: ctx.newUser._id },
})
})
it('handle admin', async function (ctx) {
ctx.subscription.admin_id = ctx.newUser._id
try {
await ctx.UserMembershipHandler.promises.removeUser(
ctx.subscription,
EntityConfigs.groupManagers,
ctx.newUser._id
)
expect.fail('Expected removeUser to throw')
} catch (err) {
expect(err).toBeInstanceOf(UserMembershipErrors.UserIsManagerError)
}
})
})
})
})

View File

@@ -1,251 +0,0 @@
const { expect } = require('chai')
const sinon = require('sinon')
const assertCalledWith = sinon.assert.calledWith
const { ObjectId } = require('mongodb-legacy')
const modulePath =
'../../../../app/src/Features/UserMembership/UserMembershipHandler'
const SandboxedModule = require('sandboxed-module')
const EntityConfigs = require('../../../../app/src/Features/UserMembership/UserMembershipEntityConfigs')
const {
UserIsManagerError,
UserNotFoundError,
UserAlreadyAddedError,
} = require('../../../../app/src/Features/UserMembership/UserMembershipErrors')
describe('UserMembershipHandler', function () {
beforeEach(function () {
this.user = { _id: new ObjectId() }
this.newUser = { _id: new ObjectId(), email: 'new-user-email@foo.bar' }
this.fakeEntityId = new ObjectId()
this.subscription = {
_id: 'mock-subscription-id',
groupPlan: true,
membersLimit: 10,
member_ids: [new ObjectId(), new ObjectId()],
manager_ids: [new ObjectId()],
invited_emails: ['mock-email-1@foo.com'],
teamInvites: [{ email: 'mock-email-1@bar.com' }],
update: sinon.stub().returns({
exec: sinon.stub().resolves(),
}),
}
this.institution = {
_id: 'mock-institution-id',
v1Id: 123,
managerIds: [new ObjectId(), new ObjectId(), new ObjectId()],
updateOne: sinon.stub().returns({
exec: sinon.stub().resolves(),
}),
}
this.publisher = {
_id: 'mock-publisher-id',
slug: 'slug',
managerIds: [new ObjectId(), new ObjectId()],
updateOne: sinon.stub().returns({
exec: sinon.stub().resolves(),
}),
}
this.UserMembershipViewModel = {
promises: {
buildAsync: sinon.stub().resolves([{ _id: 'mock-member-id' }]),
},
build: sinon.stub().returns(this.newUser),
}
this.UserGetter = {
promises: {
getUserByAnyEmail: sinon.stub().resolves(this.newUser),
},
}
this.Institution = {
findOne: sinon.stub().returns({
exec: sinon.stub().resolves(this.institution),
}),
}
this.Subscription = {
findOne: sinon.stub().returns({
exec: sinon.stub().resolves(this.subscription),
}),
}
this.Publisher = {
findOne: sinon.stub().returns({
exec: sinon.stub().resolves(this.publisher),
}),
create: sinon.stub().returns({
exec: sinon.stub().resolves(this.publisher),
}),
}
this.UserMembershipHandler = SandboxedModule.require(modulePath, {
requires: {
'mongodb-legacy': { ObjectId },
'./UserMembershipErrors': {
UserIsManagerError,
UserNotFoundError,
UserAlreadyAddedError,
},
'./UserMembershipViewModel': this.UserMembershipViewModel,
'../User/UserGetter': this.UserGetter,
'../../models/Institution': {
Institution: this.Institution,
},
'../../models/Subscription': {
Subscription: this.Subscription,
},
'../../models/Publisher': {
Publisher: this.Publisher,
},
},
})
})
describe('getEntityWithoutAuthorizationCheck', function () {
it('get publisher', async function () {
const subscription =
await this.UserMembershipHandler.promises.getEntityWithoutAuthorizationCheck(
this.fakeEntityId,
EntityConfigs.publisher
)
const expectedQuery = { slug: this.fakeEntityId }
assertCalledWith(this.Publisher.findOne, expectedQuery)
expect(subscription).to.equal(this.publisher)
})
})
describe('getUsers', function () {
describe('group', function () {
it('build view model for all users', async function () {
await this.UserMembershipHandler.promises.getUsers(
this.subscription,
EntityConfigs.group
)
expect(
this.UserMembershipViewModel.promises.buildAsync
).to.be.calledOnceWith(
this.subscription.invited_emails.concat(
this.subscription.teamInvites[0].email,
this.subscription.member_ids
)
)
})
})
describe('group managers', function () {
it('build view model for all managers', async function () {
await this.UserMembershipHandler.promises.getUsers(
this.subscription,
EntityConfigs.groupManagers
)
expect(
this.UserMembershipViewModel.promises.buildAsync
).to.be.calledOnceWith(this.subscription.manager_ids)
})
})
describe('institution', function () {
it('build view model for all managers', async function () {
await this.UserMembershipHandler.promises.getUsers(
this.institution,
EntityConfigs.institution
)
expect(
this.UserMembershipViewModel.promises.buildAsync
).to.be.calledOnceWith(this.institution.managerIds)
})
})
})
describe('createEntity', function () {
it('creates publisher', async function () {
await this.UserMembershipHandler.promises.createEntity(
this.fakeEntityId,
EntityConfigs.publisher
)
assertCalledWith(this.Publisher.create, { slug: this.fakeEntityId })
})
})
describe('addUser', function () {
beforeEach(function () {
this.email = this.newUser.email
})
describe('institution', function () {
it('get user', async function () {
await this.UserMembershipHandler.promises.addUser(
this.institution,
EntityConfigs.institution,
this.email
)
assertCalledWith(this.UserGetter.promises.getUserByAnyEmail, this.email)
})
it('handle user not found', async function () {
this.UserGetter.promises.getUserByAnyEmail.resolves(null)
expect(
this.UserMembershipHandler.promises.addUser(
this.institution,
EntityConfigs.institution,
this.email
)
).to.be.rejectedWith(UserNotFoundError)
})
it('handle user already added', async function () {
this.institution.managerIds.push(this.newUser._id)
expect(
this.UserMembershipHandler.promises.addUser(
this.institution,
EntityConfigs.institution,
this.email
)
).to.be.rejectedWith(UserAlreadyAddedError)
})
it('add user to institution', async function () {
await this.UserMembershipHandler.promises.addUser(
this.institution,
EntityConfigs.institution,
this.email
)
assertCalledWith(this.institution.updateOne, {
$addToSet: { managerIds: this.newUser._id },
})
})
it('return user view', async function () {
const user = await this.UserMembershipHandler.promises.addUser(
this.institution,
EntityConfigs.institution,
this.email
)
user.should.equal(this.newUser)
})
})
})
describe('removeUser', function () {
describe('institution', function () {
it('remove user from institution', async function () {
await this.UserMembershipHandler.promises.removeUser(
this.institution,
EntityConfigs.institution,
this.newUser._id
)
assertCalledWith(this.institution.updateOne, {
$pull: { managerIds: this.newUser._id },
})
})
it('handle admin', async function () {
this.subscription.admin_id = this.newUser._id
expect(
this.UserMembershipHandler.promises.removeUser(
this.subscription,
EntityConfigs.groupManagers,
this.newUser._id
)
).to.be.rejectedWith(UserIsManagerError)
})
})
})
})

View File

@@ -0,0 +1,128 @@
import { vi, expect } from 'vitest'
import sinon from 'sinon'
import mongodb from 'mongodb-legacy'
import {
isObjectIdInstance,
normalizeQuery,
} from '../../../../app/src/Features/Helpers/Mongo.js'
const assertCalledWith = sinon.assert.calledWith
const assertNotCalled = sinon.assert.notCalled
const { ObjectId } = mongodb
const modulePath =
'../../../../app/src/Features/UserMembership/UserMembershipViewModel'
describe('UserMembershipViewModel', function () {
beforeEach(async function (ctx) {
ctx.UserGetter = { promises: { getUsers: sinon.stub() } }
vi.doMock('mongodb-legacy', () => ({
default: { ObjectId },
}))
vi.doMock('../../../../app/src/Features/Helpers/Mongo', () => ({
isObjectIdInstance,
normalizeQuery,
}))
vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({
default: ctx.UserGetter,
}))
ctx.UserMembershipViewModel = (await import(modulePath)).default
ctx.email = 'mock-email@bar.com'
ctx.user = {
_id: 'mock-user-id',
email: 'mock-email@baz.com',
first_name: 'Name',
lastLoggedIn: '2020-05-20T10:41:11.407Z',
enrollment: {
managedBy: 'mock-group-id',
enrolledAt: new Date(),
sso: {
groupId: 'abc123abc123',
linkedAt: new Date(),
primary: true,
},
},
}
})
describe('build', function () {
it('build email', function (ctx) {
const viewModel = ctx.UserMembershipViewModel.build(ctx.email)
expect(viewModel).to.deep.equal({
email: ctx.email,
invite: true,
last_active_at: null,
last_logged_in_at: null,
first_name: null,
last_name: null,
_id: null,
enrollment: undefined,
})
})
it('build user', function (ctx) {
const viewModel = ctx.UserMembershipViewModel.build(ctx.user)
expect(viewModel).to.deep.equal({
email: ctx.user.email,
invite: false,
last_active_at: ctx.user.lastLoggedIn,
last_logged_in_at: ctx.user.lastLoggedIn,
first_name: ctx.user.first_name,
last_name: null,
_id: ctx.user._id,
enrollment: ctx.user.enrollment,
})
})
})
describe('build async', function () {
beforeEach(function (ctx) {
ctx.UserMembershipViewModel.build = sinon.stub()
})
it('build email', async function (ctx) {
ctx.UserGetter.promises.getUsers.resolves([])
await ctx.UserMembershipViewModel.buildAsync([ctx.email])
assertCalledWith(ctx.UserMembershipViewModel.build, ctx.email)
})
it('build user', async function (ctx) {
ctx.UserGetter.promises.getUsers.resolves([])
await ctx.UserMembershipViewModel.buildAsync([ctx.user])
assertCalledWith(ctx.UserMembershipViewModel.build, ctx.user)
})
it('build user id', async function (ctx) {
const user = {
...ctx.user,
_id: new ObjectId(),
}
ctx.UserGetter.promises.getUsers.resolves([user])
const [viewModel] = await ctx.UserMembershipViewModel.buildAsync([
user._id,
])
assertNotCalled(ctx.UserMembershipViewModel.build)
expect(viewModel._id.toString()).to.equal(user._id.toString())
expect(viewModel.email).to.equal(user.email)
expect(viewModel.first_name).to.equal(user.first_name)
expect(viewModel.invite).to.equal(false)
expect(viewModel.email).to.exist
expect(viewModel.enrollment).to.exist
expect(viewModel.enrollment).to.deep.equal(user.enrollment)
})
it('build user id with error', async function (ctx) {
ctx.UserGetter.promises.getUsers.rejects(new Error('nope'))
const userId = new ObjectId()
const [viewModel] = await ctx.UserMembershipViewModel.buildAsync([userId])
assertNotCalled(ctx.UserMembershipViewModel.build)
expect(viewModel._id).to.equal(userId.toString())
expect(viewModel.email).not.to.exist
})
})
})

View File

@@ -1,119 +0,0 @@
const { expect } = require('chai')
const sinon = require('sinon')
const assertCalledWith = sinon.assert.calledWith
const assertNotCalled = sinon.assert.notCalled
const { ObjectId } = require('mongodb-legacy')
const modulePath =
'../../../../app/src/Features/UserMembership/UserMembershipViewModel'
const SandboxedModule = require('sandboxed-module')
const {
isObjectIdInstance,
normalizeQuery,
} = require('../../../../app/src/Features/Helpers/Mongo')
describe('UserMembershipViewModel', function () {
beforeEach(function () {
this.UserGetter = { promises: { getUsers: sinon.stub() } }
this.UserMembershipViewModel = SandboxedModule.require(modulePath, {
requires: {
'mongodb-legacy': { ObjectId },
'../Helpers/Mongo': { isObjectIdInstance, normalizeQuery },
'../User/UserGetter': this.UserGetter,
},
})
this.email = 'mock-email@bar.com'
this.user = {
_id: 'mock-user-id',
email: 'mock-email@baz.com',
first_name: 'Name',
lastLoggedIn: '2020-05-20T10:41:11.407Z',
enrollment: {
managedBy: 'mock-group-id',
enrolledAt: new Date(),
sso: {
groupId: 'abc123abc123',
linkedAt: new Date(),
primary: true,
},
},
}
})
describe('build', function () {
it('build email', function () {
const viewModel = this.UserMembershipViewModel.build(this.email)
expect(viewModel).to.deep.equal({
email: this.email,
invite: true,
last_active_at: null,
last_logged_in_at: null,
first_name: null,
last_name: null,
_id: null,
enrollment: undefined,
})
})
it('build user', function () {
const viewModel = this.UserMembershipViewModel.build(this.user)
expect(viewModel).to.deep.equal({
email: this.user.email,
invite: false,
last_active_at: this.user.lastLoggedIn,
last_logged_in_at: this.user.lastLoggedIn,
first_name: this.user.first_name,
last_name: null,
_id: this.user._id,
enrollment: this.user.enrollment,
})
})
})
describe('build async', function () {
beforeEach(function () {
this.UserMembershipViewModel.build = sinon.stub()
})
it('build email', async function () {
this.UserGetter.promises.getUsers.resolves([])
await this.UserMembershipViewModel.buildAsync([this.email])
assertCalledWith(this.UserMembershipViewModel.build, this.email)
})
it('build user', async function () {
this.UserGetter.promises.getUsers.resolves([])
await this.UserMembershipViewModel.buildAsync([this.user])
assertCalledWith(this.UserMembershipViewModel.build, this.user)
})
it('build user id', async function () {
const user = {
...this.user,
_id: new ObjectId(),
}
this.UserGetter.promises.getUsers.resolves([user])
const [viewModel] = await this.UserMembershipViewModel.buildAsync([
user._id,
])
assertNotCalled(this.UserMembershipViewModel.build)
expect(viewModel._id.toString()).to.equal(user._id.toString())
expect(viewModel.email).to.equal(user.email)
expect(viewModel.first_name).to.equal(user.first_name)
expect(viewModel.invite).to.equal(false)
expect(viewModel.email).to.exist
expect(viewModel.enrollment).to.exist
expect(viewModel.enrollment).to.deep.equal(user.enrollment)
})
it('build user id with error', async function () {
this.UserGetter.promises.getUsers.rejects(new Error('nope'))
const userId = new ObjectId()
const [viewModel] = await this.UserMembershipViewModel.buildAsync([
userId,
])
assertNotCalled(this.UserMembershipViewModel.build)
expect(viewModel._id).to.equal(userId.toString())
expect(viewModel.email).not.to.exist
})
})
})