mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-06-08 16:50:44 +02:00
Merge pull request #25151 from overleaf/dk-use-user-features
UserFeaturesContext with cross-tab syncing via BroadcastChannel GitOrigin-RevId: 4262719f5018f5717211851ce28b3255af65461a
This commit is contained in:
committed by
Copybot
parent
2f3166aa54
commit
82e5b2c5d7
@@ -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 =
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
module.exports = {}
|
||||
module.exports = {
|
||||
addEventListener: () => {},
|
||||
removeEventListener: () => {},
|
||||
}
|
||||
|
||||
@@ -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<
|
||||
<Providers.ReferencesProvider>
|
||||
<Providers.DetachProvider>
|
||||
<Providers.EditorProvider>
|
||||
<Providers.PermissionsProvider>
|
||||
<Providers.RailProvider>
|
||||
<Providers.LayoutProvider>
|
||||
<Providers.ProjectSettingsProvider>
|
||||
<Providers.EditorManagerProvider>
|
||||
<Providers.LocalCompileProvider>
|
||||
<Providers.DetachCompileProvider>
|
||||
<Providers.ChatProvider>
|
||||
<Providers.FileTreeOpenProvider>
|
||||
<Providers.OnlineUsersProvider>
|
||||
<Providers.MetadataProvider>
|
||||
<Providers.OutlineProvider>
|
||||
<Providers.IdeRedesignSwitcherProvider>
|
||||
<Providers.CommandRegistryProvider>
|
||||
{children}
|
||||
</Providers.CommandRegistryProvider>
|
||||
</Providers.IdeRedesignSwitcherProvider>
|
||||
</Providers.OutlineProvider>
|
||||
</Providers.MetadataProvider>
|
||||
</Providers.OnlineUsersProvider>
|
||||
</Providers.FileTreeOpenProvider>
|
||||
</Providers.ChatProvider>
|
||||
</Providers.DetachCompileProvider>
|
||||
</Providers.LocalCompileProvider>
|
||||
</Providers.EditorManagerProvider>
|
||||
</Providers.ProjectSettingsProvider>
|
||||
</Providers.LayoutProvider>
|
||||
</Providers.RailProvider>
|
||||
</Providers.PermissionsProvider>
|
||||
<Providers.UserFeaturesProvider>
|
||||
<Providers.PermissionsProvider>
|
||||
<Providers.RailProvider>
|
||||
<Providers.LayoutProvider>
|
||||
<Providers.ProjectSettingsProvider>
|
||||
<Providers.EditorManagerProvider>
|
||||
<Providers.LocalCompileProvider>
|
||||
<Providers.DetachCompileProvider>
|
||||
<Providers.ChatProvider>
|
||||
<Providers.FileTreeOpenProvider>
|
||||
<Providers.OnlineUsersProvider>
|
||||
<Providers.MetadataProvider>
|
||||
<Providers.OutlineProvider>
|
||||
<Providers.IdeRedesignSwitcherProvider>
|
||||
<Providers.CommandRegistryProvider>
|
||||
{children}
|
||||
</Providers.CommandRegistryProvider>
|
||||
</Providers.IdeRedesignSwitcherProvider>
|
||||
</Providers.OutlineProvider>
|
||||
</Providers.MetadataProvider>
|
||||
</Providers.OnlineUsersProvider>
|
||||
</Providers.FileTreeOpenProvider>
|
||||
</Providers.ChatProvider>
|
||||
</Providers.DetachCompileProvider>
|
||||
</Providers.LocalCompileProvider>
|
||||
</Providers.EditorManagerProvider>
|
||||
</Providers.ProjectSettingsProvider>
|
||||
</Providers.LayoutProvider>
|
||||
</Providers.RailProvider>
|
||||
</Providers.PermissionsProvider>
|
||||
</Providers.UserFeaturesProvider>
|
||||
</Providers.EditorProvider>
|
||||
</Providers.DetachProvider>
|
||||
</Providers.ReferencesProvider>
|
||||
|
||||
+4
-1
@@ -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 (
|
||||
<SplitTestProvider>
|
||||
<SubscriptionDashboardProvider>
|
||||
<SuccessfulSubscription />
|
||||
<UserProvider>
|
||||
<SuccessfulSubscription />
|
||||
</UserProvider>
|
||||
</SubscriptionDashboardProvider>
|
||||
</SplitTestProvider>
|
||||
)
|
||||
|
||||
+2
@@ -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
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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<User['features']>(undefined)
|
||||
|
||||
export const UserFeaturesProvider: FC<React.PropsWithChildren> = ({
|
||||
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 (
|
||||
<UserFeaturesContext.Provider value={features}>
|
||||
{children}
|
||||
</UserFeaturesContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
+18
-11
@@ -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(<SuccessfulSubscription />, {
|
||||
metaTags: [
|
||||
{
|
||||
name: 'ol-ExposedSettings',
|
||||
value: {
|
||||
adminEmail,
|
||||
} as ExposedSettings,
|
||||
},
|
||||
{ name: 'ol-subscription', value: annualActiveSubscription },
|
||||
],
|
||||
})
|
||||
renderWithSubscriptionDashContext(
|
||||
<UserProvider>
|
||||
<SuccessfulSubscription />
|
||||
</UserProvider>,
|
||||
|
||||
{
|
||||
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')
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user