[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-21 08:06:30 +00:00
committed by Copybot
parent 93526bec96
commit 3a1f3af6a4
20 changed files with 1055 additions and 872 deletions
@@ -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
})
})
})
})
@@ -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
})
})
})
})
@@ -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
})
})
})
@@ -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
})
})
})