diff --git a/services/web/app/src/Features/Project/ProjectController.js b/services/web/app/src/Features/Project/ProjectController.js
index f34e2522ec..1e92a55519 100644
--- a/services/web/app/src/Features/Project/ProjectController.js
+++ b/services/web/app/src/Features/Project/ProjectController.js
@@ -15,7 +15,6 @@ const metrics = require('@overleaf/metrics')
const { User } = require('../../models/User')
const SubscriptionLocator = require('../Subscription/SubscriptionLocator')
const LimitationsManager = require('../Subscription/LimitationsManager')
-const FeaturesHelper = require('../Subscription/FeaturesHelper')
const Settings = require('@overleaf/settings')
const AuthorizationManager = require('../Authorization/AuthorizationManager')
const InactiveProjectManager = require('../InactiveData/InactiveProjectManager')
@@ -753,16 +752,7 @@ const _ProjectController = {
let fullFeatureSet = user?.features
if (!anonymous) {
- // generate users feature set including features added, or overriden via modules
- const moduleFeatures =
- (await Modules.promises.hooks.fire(
- 'getModuleProvidedFeatures',
- userId
- )) || []
- fullFeatureSet = FeaturesHelper.computeFeatureSet([
- user.features,
- ...moduleFeatures,
- ])
+ fullFeatureSet = await UserGetter.promises.getUserFeatures(userId)
}
const isPaywallChangeCompileTimeoutEnabled =
diff --git a/services/web/app/src/Features/Subscription/SubscriptionController.js b/services/web/app/src/Features/Subscription/SubscriptionController.js
index 72efe77980..fe23512901 100644
--- a/services/web/app/src/Features/Subscription/SubscriptionController.js
+++ b/services/web/app/src/Features/Subscription/SubscriptionController.js
@@ -25,6 +25,7 @@ const RecurlyClient = require('./RecurlyClient')
const { AI_ADD_ON_CODE } = require('./PaymentProviderEntities')
const PlansLocator = require('./PlansLocator')
const PaymentProviderEntities = require('./PaymentProviderEntities')
+const { User } = require('../../models/User')
/**
* @import { SubscriptionChangeDescription } from '../../../../types/subscription/subscription-change-preview'
@@ -186,7 +187,9 @@ async function userSubscriptionPage(req, res) {
async function successfulSubscription(req, res) {
const user = SessionManager.getSessionUser(req.session)
-
+ if (!user) {
+ throw new Error('User is not logged in')
+ }
const { personalSubscription } =
await SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
user,
@@ -198,11 +201,23 @@ async function successfulSubscription(req, res) {
if (!personalSubscription) {
res.redirect('/user/subscription/plans')
} else {
+ const userInDb = await User.findById(user._id, {
+ _id: 1,
+ features: 1,
+ })
+
+ if (!userInDb) {
+ throw new Error('User not found')
+ }
+
res.render('subscriptions/successful-subscription-react', {
title: 'thank_you',
personalSubscription,
postCheckoutRedirect,
- user,
+ user: {
+ _id: user._id,
+ features: userInDb.features,
+ },
})
}
}
@@ -387,7 +402,6 @@ async function purchaseAddon(req, res, next) {
addOnCode,
quantity
)
- return res.sendStatus(200)
} catch (err) {
if (err instanceof DuplicateAddOnError) {
HttpErrorHandler.badRequest(
@@ -406,6 +420,14 @@ async function purchaseAddon(req, res, next) {
return next(err)
}
}
+
+ try {
+ await FeaturesUpdater.promises.refreshFeatures(user._id, 'add-on-purchase')
+ } catch (err) {
+ logger.error({ err }, 'Failed to refresh features after add-on purchase')
+ }
+
+ return res.sendStatus(200)
}
async function removeAddon(req, res, next) {
diff --git a/services/web/app/src/Features/User/UserGetter.js b/services/web/app/src/Features/User/UserGetter.js
index 34d758add6..9be0c64b9a 100644
--- a/services/web/app/src/Features/User/UserGetter.js
+++ b/services/web/app/src/Features/User/UserGetter.js
@@ -11,6 +11,8 @@ const Errors = require('../Errors/Errors')
const Features = require('../../infrastructure/Features')
const { User } = require('../../models/User')
const { normalizeQuery, normalizeMultiQuery } = require('../Helpers/Mongo')
+const Modules = require('../../infrastructure/Modules')
+const FeaturesHelper = require('../Subscription/FeaturesHelper')
function _lastDayToReconfirm(emailData, institutionData) {
const globalReconfirmPeriod = settings.reconfirmNotificationDays
@@ -95,6 +97,21 @@ async function getUserFullEmails(userId) {
)
}
+async function getUserFeatures(userId) {
+ const user = await UserGetter.promises.getUser(userId, {
+ features: 1,
+ })
+ if (!user) {
+ throw new Error('User not Found')
+ }
+
+ const moduleFeatures =
+ (await Modules.promises.hooks.fire('getModuleProvidedFeatures', userId)) ||
+ []
+
+ return FeaturesHelper.computeFeatureSet([user.features, ...moduleFeatures])
+}
+
async function getUserConfirmedEmails(userId) {
const user = await UserGetter.promises.getUser(userId, {
emails: 1,
@@ -136,13 +153,7 @@ const UserGetter = {
}
},
- getUserFeatures(userId, callback) {
- this.getUser(userId, { features: 1 }, (error, user) => {
- if (error) return callback(error)
- if (!user) return callback(new Errors.NotFoundError('user not found'))
- callback(null, user.features)
- })
- },
+ getUserFeatures: callbackify(getUserFeatures),
getUserEmail(userId, callback) {
this.getUser(userId, { email: 1 }, (error, user) =>
@@ -335,9 +346,10 @@ const decorateFullEmails = (
}
UserGetter.promises = promisifyAll(UserGetter, {
- without: ['getSsoUsersAtInstitution', 'getUserFullEmails'],
+ without: ['getSsoUsersAtInstitution', 'getUserFullEmails', 'getUserFeatures'],
})
UserGetter.promises.getUserFullEmails = getUserFullEmails
UserGetter.promises.getSsoUsersAtInstitution = getSsoUsersAtInstitution
+UserGetter.promises.getUserFeatures = getUserFeatures
module.exports = UserGetter
diff --git a/services/web/app/src/Features/User/UserInfoController.js b/services/web/app/src/Features/User/UserInfoController.js
index 4eeea4f5e9..c95bc45af5 100644
--- a/services/web/app/src/Features/User/UserInfoController.js
+++ b/services/web/app/src/Features/User/UserInfoController.js
@@ -1,6 +1,7 @@
const UserGetter = require('./UserGetter')
const SessionManager = require('../Authentication/SessionManager')
const { ObjectId } = require('mongodb-legacy')
+const { expressify } = require('@overleaf/promise-utils')
function getLoggedInUsersPersonalInfo(req, res, next) {
const userId = SessionManager.getLoggedInUserId(req.session)
@@ -78,9 +79,19 @@ function formatPersonalInfo(user) {
return formattedUser
}
+async function getUserFeatures(req, res, next) {
+ const userId = SessionManager.getLoggedInUserId(req.session)
+ if (!userId) {
+ throw new Error('User is not logged in')
+ }
+ const features = await UserGetter.promises.getUserFeatures(userId)
+ return res.json(features)
+}
+
module.exports = {
getLoggedInUsersPersonalInfo,
getPersonalInfo,
sendFormattedPersonalInfo,
formatPersonalInfo,
+ getUserFeatures: expressify(getUserFeatures),
}
diff --git a/services/web/app/src/router.mjs b/services/web/app/src/router.mjs
index 9201ad4c55..f87297c35c 100644
--- a/services/web/app/src/router.mjs
+++ b/services/web/app/src/router.mjs
@@ -484,6 +484,11 @@ async function initialize(webRouter, privateApiRouter, publicApiRouter) {
AuthenticationController.requirePrivateApiAuth(),
UserInfoController.getPersonalInfo
)
+ webRouter.get(
+ '/user/features',
+ AuthenticationController.requireLogin(),
+ UserInfoController.getUserFeatures
+ )
webRouter.get(
'/user/reconfirm',
diff --git a/services/web/cypress/fixtures/build/mock-writefull-api.js b/services/web/cypress/fixtures/build/mock-writefull-api.js
index 4ba52ba2c8..a70a28653e 100644
--- a/services/web/cypress/fixtures/build/mock-writefull-api.js
+++ b/services/web/cypress/fixtures/build/mock-writefull-api.js
@@ -1 +1,4 @@
-module.exports = {}
+module.exports = {
+ addEventListener: () => {},
+ removeEventListener: () => {},
+}
diff --git a/services/web/frontend/js/features/ide-react/context/react-context-root.tsx b/services/web/frontend/js/features/ide-react/context/react-context-root.tsx
index f8b094f821..0eeb870a2b 100644
--- a/services/web/frontend/js/features/ide-react/context/react-context-root.tsx
+++ b/services/web/frontend/js/features/ide-react/context/react-context-root.tsx
@@ -23,6 +23,7 @@ import { ReferencesProvider } from '@/features/ide-react/context/references-cont
import { SnapshotProvider } from '@/features/ide-react/context/snapshot-context'
import { SplitTestProvider } from '@/shared/context/split-test-context'
import { UserProvider } from '@/shared/context/user-context'
+import { UserFeaturesProvider } from '@/shared/context/user-features-context'
import { UserSettingsProvider } from '@/shared/context/user-settings-context'
import { IdeRedesignSwitcherProvider } from './ide-redesign-switcher-context'
import { CommandRegistryProvider } from './command-registry-context'
@@ -60,6 +61,7 @@ export const ReactContextRoot: FC<
UserSettingsProvider,
IdeRedesignSwitcherProvider,
CommandRegistryProvider,
+ UserFeaturesProvider,
...providers,
}
@@ -77,35 +79,37 @@ export const ReactContextRoot: FC<
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {children}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/services/web/frontend/js/features/subscription/components/successful-subscription/root.tsx b/services/web/frontend/js/features/subscription/components/successful-subscription/root.tsx
index f7ef400ded..437ac2928e 100644
--- a/services/web/frontend/js/features/subscription/components/successful-subscription/root.tsx
+++ b/services/web/frontend/js/features/subscription/components/successful-subscription/root.tsx
@@ -1,3 +1,4 @@
+import { UserProvider } from '@/shared/context/user-context'
import useWaitForI18n from '../../../../shared/hooks/use-wait-for-i18n'
import { SubscriptionDashboardProvider } from '../../context/subscription-dashboard-context'
import SuccessfulSubscription from './successful-subscription'
@@ -13,7 +14,9 @@ function Root() {
return (
-
+
+
+
)
diff --git a/services/web/frontend/js/features/subscription/components/successful-subscription/successful-subscription.tsx b/services/web/frontend/js/features/subscription/components/successful-subscription/successful-subscription.tsx
index f211c85f10..e48ce0053f 100644
--- a/services/web/frontend/js/features/subscription/components/successful-subscription/successful-subscription.tsx
+++ b/services/web/frontend/js/features/subscription/components/successful-subscription/successful-subscription.tsx
@@ -13,6 +13,7 @@ import {
isStandaloneAiPlanCode,
} from '../../data/add-on-codes'
import { PaidSubscription } from '../../../../../../types/subscription/dashboard/subscription'
+import { useBroadcastUser } from '@/shared/hooks/user-channel/use-broadcast-user'
function SuccessfulSubscription() {
const { t } = useTranslation()
@@ -20,6 +21,7 @@ function SuccessfulSubscription() {
useSubscriptionDashboardContext()
const postCheckoutRedirect = getMeta('ol-postCheckoutRedirect')
const { appName, adminEmail } = getMeta('ol-ExposedSettings')
+ useBroadcastUser()
if (!subscription || !('payment' in subscription)) return null
diff --git a/services/web/frontend/js/shared/context/types/writefull-instance.ts b/services/web/frontend/js/shared/context/types/writefull-instance.ts
index 213ab67f18..120590e668 100644
--- a/services/web/frontend/js/shared/context/types/writefull-instance.ts
+++ b/services/web/frontend/js/shared/context/types/writefull-instance.ts
@@ -1,6 +1,7 @@
export interface WritefullEvents {
'writefull-login-complete': {
method: 'email-password' | 'login-with-overleaf'
+ isPremium: boolean
}
'writefull-received-suggestions': { numberOfSuggestions: number }
'writefull-register-as-auto-account': { email: string }
diff --git a/services/web/frontend/js/shared/context/user-features-context.tsx b/services/web/frontend/js/shared/context/user-features-context.tsx
new file mode 100644
index 0000000000..2cc9ba932d
--- /dev/null
+++ b/services/web/frontend/js/shared/context/user-features-context.tsx
@@ -0,0 +1,69 @@
+import {
+ createContext,
+ FC,
+ useCallback,
+ useContext,
+ useEffect,
+ useState,
+} from 'react'
+import { User } from '../../../../types/user'
+import { useUserContext } from './user-context'
+import { useReceiveUser } from '../hooks/user-channel/use-receive-user'
+import { getJSON } from '@/infrastructure/fetch-json'
+import { useEditorContext } from './editor-context'
+
+export const UserFeaturesContext = createContext(undefined)
+
+export const UserFeaturesProvider: FC = ({
+ children,
+}) => {
+ const user = useUserContext()
+ const { writefullInstance } = useEditorContext()
+ const [features, setFeatures] = useState(user.features)
+
+ useReceiveUser(
+ useCallback(data => {
+ if (data?.features) {
+ setFeatures(data.features)
+ }
+ }, [])
+ )
+
+ useEffect(() => {
+ const listener = async ({ isPremium }: { isPremium: boolean }) => {
+ if (features?.aiErrorAssistant === isPremium) {
+ // the user is premium on writefull and has the AI assist, no need to refresh the features
+ return
+ }
+ const newFeatures = await getJSON('/user/features')
+ setFeatures(newFeatures)
+ }
+
+ writefullInstance?.addEventListener('writefull-login-complete', listener)
+
+ return () => {
+ writefullInstance?.removeEventListener(
+ 'writefull-login-complete',
+ listener
+ )
+ }
+ }, [features?.aiErrorAssistant, writefullInstance])
+
+ return (
+
+ {children}
+
+ )
+}
+
+export function useUserFeaturesContext() {
+ const context = useContext(UserFeaturesContext)
+
+ if (!context) {
+ throw new Error(
+ 'useUserFeaturesContext is only available inside UserFeaturesContext, or `ol-user` meta is not defined'
+ )
+ }
+
+ return context
+}
diff --git a/services/web/test/frontend/features/subscription/components/successful-subscription/successful-subscription.test.tsx b/services/web/test/frontend/features/subscription/components/successful-subscription/successful-subscription.test.tsx
index 33979e6b9e..90453c9349 100644
--- a/services/web/test/frontend/features/subscription/components/successful-subscription/successful-subscription.test.tsx
+++ b/services/web/test/frontend/features/subscription/components/successful-subscription/successful-subscription.test.tsx
@@ -4,21 +4,28 @@ import SuccessfulSubscription from '../../../../../../frontend/js/features/subsc
import { renderWithSubscriptionDashContext } from '../../helpers/render-with-subscription-dash-context'
import { annualActiveSubscription } from '../../fixtures/subscriptions'
import { ExposedSettings } from '../../../../../../types/exposed-settings'
+import { UserProvider } from '@/shared/context/user-context'
describe('successful subscription page', function () {
it('renders the invoices link', function () {
const adminEmail = 'foo@example.com'
- renderWithSubscriptionDashContext(, {
- metaTags: [
- {
- name: 'ol-ExposedSettings',
- value: {
- adminEmail,
- } as ExposedSettings,
- },
- { name: 'ol-subscription', value: annualActiveSubscription },
- ],
- })
+ renderWithSubscriptionDashContext(
+
+
+ ,
+
+ {
+ metaTags: [
+ {
+ name: 'ol-ExposedSettings',
+ value: {
+ adminEmail,
+ } as ExposedSettings,
+ },
+ { name: 'ol-subscription', value: annualActiveSubscription },
+ ],
+ }
+ )
screen.getByRole('heading', { name: /thanks for subscribing/i })
const alert = screen.getByRole('alert')
diff --git a/services/web/test/unit/src/Project/ProjectControllerTests.js b/services/web/test/unit/src/Project/ProjectControllerTests.js
index 71de5023b1..46427171da 100644
--- a/services/web/test/unit/src/Project/ProjectControllerTests.js
+++ b/services/web/test/unit/src/Project/ProjectControllerTests.js
@@ -1121,44 +1121,6 @@ describe('ProjectController', function () {
this.ProjectController.loadEditor(this.req, this.res)
})
})
-
- describe('when fetching the users featureSet', function () {
- beforeEach(function () {
- this.Modules.promises.hooks.fire = sinon.stub().resolves()
- this.user.features = {}
- })
-
- it('should take into account features overrides from modules', function (done) {
- // this case occurs when the user has bought the ai bundle on WF, which should include our error assistant
- const bundleFeatures = { aiErrorAssistant: true }
- this.user.features = { aiErrorAssistant: false }
- this.Modules.promises.hooks.fire = sinon
- .stub()
- .resolves([bundleFeatures])
- this.res.render = (pageName, opts) => {
- expect(opts.user.features).to.deep.equal(bundleFeatures)
- this.Modules.promises.hooks.fire.should.have.been.calledWith(
- 'getModuleProvidedFeatures',
- this.user._id
- )
- done()
- }
- this.ProjectController.loadEditor(this.req, this.res)
- })
-
- it('should handle modules not returning any features', function (done) {
- this.Modules.promises.hooks.fire = sinon.stub().resolves([])
- this.res.render = (pageName, opts) => {
- expect(opts.user.features).to.deep.equal({})
- this.Modules.promises.hooks.fire.should.have.been.calledWith(
- 'getModuleProvidedFeatures',
- this.user._id
- )
- done()
- }
- this.ProjectController.loadEditor(this.req, this.res)
- })
- })
})
describe('userProjectsJson', function () {
diff --git a/services/web/test/unit/src/Subscription/SubscriptionControllerTests.js b/services/web/test/unit/src/Subscription/SubscriptionControllerTests.js
index 27ba5bf85b..5b6e86663e 100644
--- a/services/web/test/unit/src/Subscription/SubscriptionControllerTests.js
+++ b/services/web/test/unit/src/Subscription/SubscriptionControllerTests.js
@@ -158,6 +158,7 @@ describe('SubscriptionController', function () {
'./FeaturesUpdater': (this.FeaturesUpdater = {
promises: {
hasFeaturesViaWritefull: sinon.stub().resolves(false),
+ refreshFeatures: sinon.stub().resolves({ features: {} }),
},
}),
'./GroupPlansData': (this.GroupPlansData = {}),
@@ -186,6 +187,11 @@ describe('SubscriptionController', function () {
'../../util/currency': (this.currency = {
formatCurrency: sinon.stub(),
}),
+ '../../models/User': {
+ User: {
+ findById: sinon.stub().resolves(this.user),
+ },
+ },
},
})
@@ -221,7 +227,10 @@ describe('SubscriptionController', function () {
title: 'thank_you',
personalSubscription: 'foo',
postCheckoutRedirect: undefined,
- user: this.user,
+ user: {
+ _id: this.user._id,
+ features: this.user.features,
+ },
})
done()
}
diff --git a/services/web/test/unit/src/User/UserGetterTests.js b/services/web/test/unit/src/User/UserGetterTests.js
index 91df5d8c6d..0e0c170fd6 100644
--- a/services/web/test/unit/src/User/UserGetterTests.js
+++ b/services/web/test/unit/src/User/UserGetterTests.js
@@ -19,7 +19,7 @@ describe('UserGetter', function () {
beforeEach(function () {
const confirmedAt = new Date()
this.fakeUser = {
- _id: '12390i',
+ _id: new ObjectId(),
email: 'email2@foo.bar',
emails: [
{
@@ -45,6 +45,10 @@ describe('UserGetter', function () {
}
this.getUserAffiliations = sinon.stub().resolves([])
+ this.Modules = {
+ promises: { hooks: { fire: sinon.stub().resolves() } },
+ }
+
this.UserGetter = SandboxedModule.require(modulePath, {
requires: {
'../Helpers/Mongo': { normalizeQuery, normalizeMultiQuery },
@@ -63,6 +67,7 @@ describe('UserGetter', function () {
'../../models/User': {
User: (this.User = {}),
},
+ '../../infrastructure/Modules': this.Modules,
},
})
})
@@ -1259,4 +1264,56 @@ describe('UserGetter', function () {
})
})
})
+
+ describe('getUserFeatures', function () {
+ beforeEach(function () {
+ this.Modules.promises.hooks.fire = sinon.stub().resolves()
+ this.fakeUser.features = {}
+ })
+
+ it('should return user features', function (done) {
+ this.fakeUser.features = { feature1: true, feature2: false }
+ this.UserGetter.getUserFeatures(new ObjectId(), (error, features) => {
+ expect(error).to.not.exist
+ expect(features).to.deep.equal(this.fakeUser.features)
+ done()
+ })
+ })
+
+ it('should return user features when using promises', async function () {
+ this.fakeUser.features = { feature1: true, feature2: false }
+ const features = await this.UserGetter.promises.getUserFeatures(
+ this.fakeUser._id
+ )
+ expect(features).to.deep.equal(this.fakeUser.features)
+ })
+
+ it('should take into account features overrides from modules', async function () {
+ // this case occurs when the user has bought the ai bundle on WF, which should include our error assistant
+ const bundleFeatures = { aiErrorAssistant: true }
+ this.fakeUser.features = { aiErrorAssistant: false }
+ this.Modules.promises.hooks.fire = sinon.stub().resolves([bundleFeatures])
+ const features = await this.UserGetter.promises.getUserFeatures(
+ this.fakeUser._id
+ )
+ expect(features).to.deep.equal(bundleFeatures)
+ this.Modules.promises.hooks.fire.should.have.been.calledWith(
+ 'getModuleProvidedFeatures',
+ this.fakeUser._id
+ )
+ })
+
+ it('should handle modules not returning any features', async function () {
+ this.Modules.promises.hooks.fire = sinon.stub().resolves([])
+ this.fakeUser.features = { test: true }
+ const features = await this.UserGetter.promises.getUserFeatures(
+ this.fakeUser._id
+ )
+ expect(features).to.deep.equal({ test: true })
+ this.Modules.promises.hooks.fire.should.have.been.calledWith(
+ 'getModuleProvidedFeatures',
+ this.fakeUser._id
+ )
+ })
+ })
})
diff --git a/services/web/test/unit/src/User/UserInfoControllerTests.js b/services/web/test/unit/src/User/UserInfoControllerTests.js
index 022dbd2d7f..dd90ac3b00 100644
--- a/services/web/test/unit/src/User/UserInfoControllerTests.js
+++ b/services/web/test/unit/src/User/UserInfoControllerTests.js
@@ -10,7 +10,11 @@ describe('UserInfoController', function () {
beforeEach(function () {
this.UserDeleter = { deleteUser: sinon.stub().callsArgWith(1) }
this.UserUpdater = { updatePersonalInfo: sinon.stub() }
- this.UserGetter = {}
+ this.UserGetter = {
+ promises: {
+ getUserFeatures: sinon.stub(),
+ },
+ }
this.UserInfoController = SandboxedModule.require(modulePath, {
requires: {
@@ -148,4 +152,74 @@ describe('UserInfoController', function () {
})
})
})
+
+ describe('getUserFeatures', function () {
+ describe('when the user is logged in', function () {
+ beforeEach(async function () {
+ this.user_id = new ObjectId().toString()
+ this.features = {
+ collaborators: 10,
+ trackChanges: true,
+ references: true,
+ }
+ this.SessionManager.getLoggedInUserId.returns(this.user_id)
+ this.UserGetter.promises.getUserFeatures.resolves(this.features)
+ await this.UserInfoController.getUserFeatures(
+ this.req,
+ this.res,
+ this.next
+ )
+ })
+
+ it('should fetch the user features', function () {
+ expect(this.UserGetter.promises.getUserFeatures.callCount).to.equal(1)
+ expect(
+ this.UserGetter.promises.getUserFeatures.calledWith(this.user_id)
+ ).to.equal(true)
+ })
+
+ it('should return the features as JSON', function () {
+ expect(this.res.json.callCount).to.equal(1)
+ expect(this.res.json.calledWith(this.features)).to.equal(true)
+ })
+ })
+
+ describe('when the user is not logged in', function () {
+ beforeEach(async function () {
+ this.SessionManager.getLoggedInUserId.returns(null)
+ await this.UserInfoController.getUserFeatures(
+ this.req,
+ this.res,
+ this.next
+ )
+ })
+
+ it('should call next with an error', function () {
+ expect(this.next.callCount).to.equal(1)
+ expect(this.next.firstCall.args[0]).to.be.an.instanceof(Error)
+ expect(this.next.firstCall.args[0].message).to.equal(
+ 'User is not logged in'
+ )
+ })
+ })
+
+ describe('when fetching features fails', function () {
+ beforeEach(async function () {
+ this.user_id = new ObjectId().toString()
+ this.error = new Error('something went wrong')
+ this.SessionManager.getLoggedInUserId.returns(this.user_id)
+ this.UserGetter.promises.getUserFeatures.rejects(this.error)
+ await this.UserInfoController.getUserFeatures(
+ this.req,
+ this.res,
+ this.next
+ )
+ })
+
+ it('should call next with the error', function () {
+ expect(this.next.callCount).to.equal(1)
+ expect(this.next.firstCall.args[0]).to.equal(this.error)
+ })
+ })
+ })
})