Remove node controller and dead backend code of the old project list (#12222)
GitOrigin-RevId: 31f92b1df484db9e44ea17cd258fdc5fe467a379
This commit is contained in:
@@ -11,9 +11,7 @@ const EditorController = require('../Editor/EditorController')
|
||||
const ProjectHelper = require('./ProjectHelper')
|
||||
const metrics = require('@overleaf/metrics')
|
||||
const { User } = require('../../models/User')
|
||||
const TagsHandler = require('../Tags/TagsHandler')
|
||||
const SubscriptionLocator = require('../Subscription/SubscriptionLocator')
|
||||
const NotificationsHandler = require('../Notifications/NotificationsHandler')
|
||||
const LimitationsManager = require('../Subscription/LimitationsManager')
|
||||
const Settings = require('@overleaf/settings')
|
||||
const AuthorizationManager = require('../Authorization/AuthorizationManager')
|
||||
@@ -28,21 +26,15 @@ const CollaboratorsGetter = require('../Collaborators/CollaboratorsGetter')
|
||||
const ProjectEntityHandler = require('./ProjectEntityHandler')
|
||||
const TpdsProjectFlusher = require('../ThirdPartyDataStore/TpdsProjectFlusher')
|
||||
const UserGetter = require('../User/UserGetter')
|
||||
const NotificationsBuilder = require('../Notifications/NotificationsBuilder')
|
||||
const { V1ConnectionError } = require('../Errors/Errors')
|
||||
const Features = require('../../infrastructure/Features')
|
||||
const BrandVariationsHandler = require('../BrandVariations/BrandVariationsHandler')
|
||||
const UserController = require('../User/UserController')
|
||||
const AnalyticsManager = require('../Analytics/AnalyticsManager')
|
||||
const Modules = require('../../infrastructure/Modules')
|
||||
const SplitTestHandler = require('../SplitTests/SplitTestHandler')
|
||||
const FeaturesUpdater = require('../Subscription/FeaturesUpdater')
|
||||
const SpellingHandler = require('../Spelling/SpellingHandler')
|
||||
const UserPrimaryEmailCheckHandler = require('../User/UserPrimaryEmailCheckHandler')
|
||||
const { hasAdminAccess } = require('../Helpers/AdminAuthorizationHelper')
|
||||
const InstitutionsFeatures = require('../Institutions/InstitutionsFeatures')
|
||||
const SubscriptionViewModelBuilder = require('../Subscription/SubscriptionViewModelBuilder')
|
||||
const SurveyHandler = require('../Survey/SurveyHandler')
|
||||
const ProjectAuditLogHandler = require('./ProjectAuditLogHandler')
|
||||
const PublicAccessLevels = require('../Authorization/PublicAccessLevels')
|
||||
|
||||
@@ -54,25 +46,6 @@ const VISUAL_EDITOR_NAMING_SPLIT_TEST_MIN_SIGNUP_DATE = new Date('2023-04-17')
|
||||
* @typedef {import("./types").Project} Project
|
||||
*/
|
||||
|
||||
const _ssoAvailable = (affiliation, session, linkedInstitutionIds) => {
|
||||
if (!affiliation.institution) return false
|
||||
|
||||
// institution.confirmed is for the domain being confirmed, not the email
|
||||
// Do not show SSO UI for unconfirmed domains
|
||||
if (!affiliation.institution.confirmed) return false
|
||||
|
||||
// Could have multiple emails at the same institution, and if any are
|
||||
// linked to the institution then do not show notification for others
|
||||
if (
|
||||
linkedInstitutionIds.indexOf(affiliation.institution.id.toString()) === -1
|
||||
) {
|
||||
if (affiliation.institution.ssoEnabled) return true
|
||||
if (affiliation.institution.ssoBeta && session.samlBeta) return true
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const ProjectController = {
|
||||
_isInPercentageRollout(rolloutName, objectId, percentage) {
|
||||
if (Settings.bypassPercentageRollouts === true) {
|
||||
@@ -413,331 +386,6 @@ const ProjectController = {
|
||||
})
|
||||
},
|
||||
|
||||
projectListPage(req, res, next) {
|
||||
const timer = new metrics.Timer('project-list')
|
||||
const userId = SessionManager.getLoggedInUserId(req.session)
|
||||
const currentUser = SessionManager.getSessionUser(req.session)
|
||||
async.parallel(
|
||||
{
|
||||
tags(cb) {
|
||||
TagsHandler.getAllTags(userId, cb)
|
||||
},
|
||||
notifications(cb) {
|
||||
NotificationsHandler.getUserNotifications(userId, cb)
|
||||
},
|
||||
projects(cb) {
|
||||
ProjectGetter.findAllUsersProjects(
|
||||
userId,
|
||||
'name lastUpdated lastUpdatedBy publicAccesLevel archived trashed owner_ref tokens',
|
||||
cb
|
||||
)
|
||||
},
|
||||
hasSubscription(cb) {
|
||||
LimitationsManager.hasPaidSubscription(
|
||||
currentUser,
|
||||
(error, hasPaidSubscription) => {
|
||||
if (error != null && error instanceof V1ConnectionError) {
|
||||
return cb(null, true)
|
||||
}
|
||||
cb(error, hasPaidSubscription)
|
||||
}
|
||||
)
|
||||
},
|
||||
user(cb) {
|
||||
User.findById(
|
||||
userId,
|
||||
'email emails featureSwitches overleaf awareOfV2 features lastLoginIp lastPrimaryEmailCheck signUpDate',
|
||||
cb
|
||||
)
|
||||
},
|
||||
userEmailsData(cb) {
|
||||
const result = { list: [], allInReconfirmNotificationPeriods: [] }
|
||||
|
||||
UserGetter.getUserFullEmails(userId, (error, fullEmails) => {
|
||||
if (error && error instanceof V1ConnectionError) {
|
||||
return cb(null, result)
|
||||
}
|
||||
|
||||
if (!Features.hasFeature('affiliations')) {
|
||||
result.list = fullEmails
|
||||
return cb(null, result)
|
||||
}
|
||||
Modules.hooks.fire(
|
||||
'allInReconfirmNotificationPeriodsForUser',
|
||||
fullEmails,
|
||||
(error, results) => {
|
||||
if (error != null) {
|
||||
return cb(error)
|
||||
}
|
||||
|
||||
// Module.hooks.fire accepts multiple methods
|
||||
// and does async.series
|
||||
const allInReconfirmNotificationPeriods =
|
||||
(results && results[0]) || []
|
||||
cb(null, {
|
||||
list: fullEmails,
|
||||
allInReconfirmNotificationPeriods,
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
},
|
||||
usersBestSubscription(cb) {
|
||||
if (!Features.hasFeature('saas')) {
|
||||
return cb()
|
||||
}
|
||||
SubscriptionViewModelBuilder.getBestSubscription(
|
||||
{ _id: userId },
|
||||
(err, subscription) => {
|
||||
if (err) {
|
||||
// do not fail loading the project list when fetching the best subscription fails
|
||||
logger.error(
|
||||
{ userId, err },
|
||||
'Could not get usersBestSubscription'
|
||||
)
|
||||
return cb(null, { type: 'error' })
|
||||
}
|
||||
cb(null, subscription)
|
||||
}
|
||||
)
|
||||
},
|
||||
userIsMemberOfGroupSubscription(cb) {
|
||||
LimitationsManager.userIsMemberOfGroupSubscription(
|
||||
currentUser,
|
||||
(error, isMember) => {
|
||||
if (error) {
|
||||
logger.error(
|
||||
{ err: error },
|
||||
'Failed to check whether user is a member of group subscription'
|
||||
)
|
||||
return cb(null, false)
|
||||
}
|
||||
cb(null, isMember)
|
||||
}
|
||||
)
|
||||
},
|
||||
survey(cb) {
|
||||
SurveyHandler.getSurvey(userId, (err, survey) => {
|
||||
if (err) {
|
||||
logger.warn({ err }, 'failed to get survey')
|
||||
// do not fail loading the project list if we fail to load the survey
|
||||
cb(null, null)
|
||||
} else {
|
||||
cb(null, survey)
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
(err, results) => {
|
||||
if (err != null) {
|
||||
OError.tag(err, 'error getting data for project list page')
|
||||
return next(err)
|
||||
}
|
||||
const {
|
||||
notifications,
|
||||
user,
|
||||
userEmailsData,
|
||||
userIsMemberOfGroupSubscription,
|
||||
} = results
|
||||
|
||||
if (
|
||||
user &&
|
||||
Features.hasFeature('saas') &&
|
||||
UserPrimaryEmailCheckHandler.requiresPrimaryEmailCheck(user)
|
||||
) {
|
||||
return res.redirect('/user/emails/primary-email-check')
|
||||
}
|
||||
|
||||
const userEmails = userEmailsData.list || []
|
||||
|
||||
const userAffiliations = userEmails
|
||||
.filter(emailData => !!emailData.affiliation)
|
||||
.map(emailData => {
|
||||
const result = emailData.affiliation
|
||||
result.email = emailData.email
|
||||
return result
|
||||
})
|
||||
|
||||
const { allInReconfirmNotificationPeriods } = userEmailsData
|
||||
|
||||
// Handle case of deleted user
|
||||
if (user == null) {
|
||||
UserController.logout(req, res, next)
|
||||
return
|
||||
}
|
||||
const tags = results.tags
|
||||
const notificationsInstitution = []
|
||||
for (const notification of notifications) {
|
||||
notification.html = req.i18n.translate(
|
||||
notification.templateKey,
|
||||
notification.messageOpts
|
||||
)
|
||||
}
|
||||
|
||||
// Institution SSO Notifications
|
||||
let reconfirmedViaSAML
|
||||
if (Features.hasFeature('saml')) {
|
||||
reconfirmedViaSAML = _.get(req.session, ['saml', 'reconfirmed'])
|
||||
const samlSession = req.session.saml
|
||||
// Notification: SSO Available
|
||||
const linkedInstitutionIds = []
|
||||
user.emails.forEach(email => {
|
||||
if (email.samlProviderId) {
|
||||
linkedInstitutionIds.push(email.samlProviderId)
|
||||
}
|
||||
})
|
||||
if (Array.isArray(userAffiliations)) {
|
||||
userAffiliations.forEach(affiliation => {
|
||||
if (
|
||||
_ssoAvailable(affiliation, req.session, linkedInstitutionIds)
|
||||
) {
|
||||
notificationsInstitution.push({
|
||||
email: affiliation.email,
|
||||
institutionId: affiliation.institution.id,
|
||||
institutionName: affiliation.institution.name,
|
||||
templateKey: 'notification_institution_sso_available',
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (samlSession) {
|
||||
// Notification: After SSO Linked
|
||||
if (samlSession.linked) {
|
||||
notificationsInstitution.push({
|
||||
email: samlSession.institutionEmail,
|
||||
institutionName: samlSession.linked.universityName,
|
||||
templateKey: 'notification_institution_sso_linked',
|
||||
})
|
||||
}
|
||||
|
||||
// Notification: After SSO Linked or Logging in
|
||||
// The requested email does not match primary email returned from
|
||||
// the institution
|
||||
if (
|
||||
samlSession.requestedEmail &&
|
||||
samlSession.emailNonCanonical &&
|
||||
!samlSession.error
|
||||
) {
|
||||
notificationsInstitution.push({
|
||||
institutionEmail: samlSession.emailNonCanonical,
|
||||
requestedEmail: samlSession.requestedEmail,
|
||||
templateKey: 'notification_institution_sso_non_canonical',
|
||||
})
|
||||
}
|
||||
|
||||
// Notification: Tried to register, but account already existed
|
||||
// registerIntercept is set before the institution callback.
|
||||
// institutionEmail is set after institution callback.
|
||||
// Check for both in case SSO flow was abandoned
|
||||
if (
|
||||
samlSession.registerIntercept &&
|
||||
samlSession.institutionEmail &&
|
||||
!samlSession.error
|
||||
) {
|
||||
notificationsInstitution.push({
|
||||
email: samlSession.institutionEmail,
|
||||
templateKey: 'notification_institution_sso_already_registered',
|
||||
})
|
||||
}
|
||||
|
||||
// Notification: When there is a session error
|
||||
if (samlSession.error) {
|
||||
notificationsInstitution.push({
|
||||
templateKey: 'notification_institution_sso_error',
|
||||
error: samlSession.error,
|
||||
})
|
||||
}
|
||||
}
|
||||
delete req.session.saml
|
||||
}
|
||||
|
||||
const portalTemplates =
|
||||
ProjectController._buildPortalTemplatesList(userAffiliations)
|
||||
const projects = ProjectController._buildProjectList(
|
||||
results.projects,
|
||||
userId
|
||||
)
|
||||
|
||||
// in v2 add notifications for matching university IPs
|
||||
if (Settings.overleaf != null && req.ip !== user.lastLoginIp) {
|
||||
NotificationsBuilder.ipMatcherAffiliation(user._id).create(
|
||||
req.ip,
|
||||
() => {}
|
||||
)
|
||||
}
|
||||
|
||||
const hasPaidAffiliation = userAffiliations.some(
|
||||
affiliation => affiliation.licence && affiliation.licence !== 'free'
|
||||
)
|
||||
|
||||
const showGroupsAndEnterpriseBanner =
|
||||
Features.hasFeature('saas') &&
|
||||
!userIsMemberOfGroupSubscription &&
|
||||
!hasPaidAffiliation
|
||||
|
||||
const groupsAndEnterpriseBannerVariant =
|
||||
showGroupsAndEnterpriseBanner &&
|
||||
_.sample(['did-you-know', 'on-premise', 'people', 'FOMO'])
|
||||
|
||||
ProjectController._injectProjectUsers(projects, (error, projects) => {
|
||||
if (error != null) {
|
||||
return next(error)
|
||||
}
|
||||
const viewModel = {
|
||||
title: 'your_projects',
|
||||
priority_title: true,
|
||||
projects,
|
||||
tags,
|
||||
notifications: notifications || [],
|
||||
notificationsInstitution,
|
||||
allInReconfirmNotificationPeriods,
|
||||
portalTemplates,
|
||||
user,
|
||||
userAffiliations,
|
||||
userEmails,
|
||||
hasSubscription: results.hasSubscription,
|
||||
reconfirmedViaSAML,
|
||||
zipFileSizeLimit: Settings.maxUploadSize,
|
||||
isOverleaf: !!Settings.overleaf,
|
||||
metadata: { viewport: false },
|
||||
showThinFooter: true, // don't show the fat footer on the projects dashboard, as there's a fixed space available
|
||||
usersBestSubscription: results.usersBestSubscription,
|
||||
survey: results.survey,
|
||||
showGroupsAndEnterpriseBanner,
|
||||
groupsAndEnterpriseBannerVariant,
|
||||
}
|
||||
|
||||
const paidUser =
|
||||
(user.features != null ? user.features.github : undefined) &&
|
||||
(user.features != null ? user.features.dropbox : undefined) // use a heuristic for paid account
|
||||
const freeUserProportion = 0.1
|
||||
const sampleFreeUser =
|
||||
parseInt(user._id.toString().slice(-2), 16) <
|
||||
freeUserProportion * 255
|
||||
const showFrontWidget = paidUser || sampleFreeUser
|
||||
|
||||
if (showFrontWidget) {
|
||||
viewModel.frontChatWidgetRoomId =
|
||||
Settings.overleaf != null
|
||||
? Settings.overleaf.front_chat_widget_room_id
|
||||
: undefined
|
||||
}
|
||||
|
||||
// null test targeting logged in users
|
||||
SplitTestHandler.promises.getAssignment(
|
||||
req,
|
||||
res,
|
||||
'null-test-dashboard'
|
||||
)
|
||||
|
||||
res.render('project/list', viewModel)
|
||||
timer.done()
|
||||
})
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
loadEditor(req, res, next) {
|
||||
const timer = new metrics.Timer('load-editor')
|
||||
if (!Settings.editorIsOpen) {
|
||||
|
||||
@@ -3,7 +3,6 @@ const path = require('path')
|
||||
const sinon = require('sinon')
|
||||
const { expect } = require('chai')
|
||||
const { ObjectId } = require('mongodb')
|
||||
const Errors = require('../../../../app/src/Features/Errors/Errors')
|
||||
|
||||
const MODULE_PATH = path.join(
|
||||
__dirname,
|
||||
@@ -62,7 +61,6 @@ describe('ProjectController', function () {
|
||||
.callsArgWith(1, null, false),
|
||||
}
|
||||
this.TagsHandler = { getAllTags: sinon.stub() }
|
||||
this.NotificationsHandler = { getUserNotifications: sinon.stub() }
|
||||
this.UserModel = { findById: sinon.stub(), updateOne: sinon.stub() }
|
||||
this.AuthorizationManager = {
|
||||
getPrivilegeLevelForProject: sinon.stub(),
|
||||
@@ -71,9 +69,6 @@ describe('ProjectController', function () {
|
||||
this.EditorController = { renameProject: sinon.stub() }
|
||||
this.InactiveProjectManager = { reactivateProjectIfRequired: sinon.stub() }
|
||||
this.ProjectUpdateHandler = { markAsOpened: sinon.stub() }
|
||||
this.UserPrimaryEmailCheckHandler = {
|
||||
requiresPrimaryEmailCheck: sinon.stub().returns(false),
|
||||
}
|
||||
this.ProjectGetter = {
|
||||
findAllUsersProjects: sinon.stub(),
|
||||
getProject: sinon.stub(),
|
||||
@@ -102,9 +97,6 @@ describe('ProjectController', function () {
|
||||
isUserInvitedMemberOfProject: sinon.stub().callsArgWith(2, null, true),
|
||||
}
|
||||
this.ProjectEntityHandler = {}
|
||||
this.NotificationBuilder = {
|
||||
ipMatcherAffiliation: sinon.stub().returns({ create: sinon.stub() }),
|
||||
}
|
||||
this.UserGetter = {
|
||||
getUserFullEmails: sinon.stub().yields(null, []),
|
||||
getUser: sinon
|
||||
@@ -166,7 +158,6 @@ describe('ProjectController', function () {
|
||||
'../Subscription/SubscriptionLocator': this.SubscriptionLocator,
|
||||
'../Subscription/LimitationsManager': this.LimitationsManager,
|
||||
'../Tags/TagsHandler': this.TagsHandler,
|
||||
'../Notifications/NotificationsHandler': this.NotificationsHandler,
|
||||
'../../models/User': { User: this.UserModel },
|
||||
'../Authorization/AuthorizationManager': this.AuthorizationManager,
|
||||
'../InactiveData/InactiveProjectManager': this.InactiveProjectManager,
|
||||
@@ -179,7 +170,6 @@ describe('ProjectController', function () {
|
||||
'./ProjectEntityHandler': this.ProjectEntityHandler,
|
||||
'../../infrastructure/Features': this.Features,
|
||||
'../Subscription/FeaturesUpdater': this.FeaturesUpdater,
|
||||
'../Notifications/NotificationsBuilder': this.NotificationBuilder,
|
||||
'../User/UserGetter': this.UserGetter,
|
||||
'../BrandVariations/BrandVariationsHandler':
|
||||
this.BrandVariationsHandler,
|
||||
@@ -188,16 +178,11 @@ describe('ProjectController', function () {
|
||||
'../Analytics/AnalyticsManager': { recordEventForUser: () => {} },
|
||||
'../Subscription/SubscriptionViewModelBuilder':
|
||||
this.SubscriptionViewModelBuilder,
|
||||
'../../infrastructure/Modules': {
|
||||
hooks: { fire: sinon.stub().yields(null, []) },
|
||||
},
|
||||
'../Spelling/SpellingHandler': {
|
||||
getUserDictionary: sinon.stub().yields(null, []),
|
||||
},
|
||||
'../Institutions/InstitutionsFeatures': this.InstitutionsFeatures,
|
||||
'../Survey/SurveyHandler': this.SurveyHandler,
|
||||
'../User/UserPrimaryEmailCheckHandler':
|
||||
this.UserPrimaryEmailCheckHandler,
|
||||
'./ProjectAuditLogHandler': this.ProjectAuditLogHandler,
|
||||
},
|
||||
})
|
||||
@@ -404,535 +389,6 @@ describe('ProjectController', function () {
|
||||
})
|
||||
})
|
||||
|
||||
describe('projectListPage', function () {
|
||||
beforeEach(function () {
|
||||
this.tags = [
|
||||
{ name: 1, project_ids: ['1', '2', '3'] },
|
||||
{ name: 2, project_ids: ['a', '1'] },
|
||||
{ name: 3, project_ids: ['a', 'b', 'c', 'd'] },
|
||||
]
|
||||
this.notifications = [
|
||||
{
|
||||
_id: '1',
|
||||
user_id: '2',
|
||||
templateKey: '3',
|
||||
messageOpts: '4',
|
||||
key: '5',
|
||||
},
|
||||
]
|
||||
this.projects = [
|
||||
{ _id: 1, lastUpdated: 1, owner_ref: 'user-1' },
|
||||
{
|
||||
_id: 2,
|
||||
lastUpdated: 2,
|
||||
owner_ref: 'user-2',
|
||||
lastUpdatedBy: 'user-1',
|
||||
},
|
||||
]
|
||||
this.collabertions = [{ _id: 5, lastUpdated: 5, owner_ref: 'user-1' }]
|
||||
this.readOnly = [{ _id: 3, lastUpdated: 3, owner_ref: 'user-1' }]
|
||||
this.tokenReadAndWrite = [{ _id: 6, lastUpdated: 5, owner_ref: 'user-4' }]
|
||||
this.tokenReadOnly = [{ _id: 7, lastUpdated: 4, owner_ref: 'user-5' }]
|
||||
this.allProjects = {
|
||||
owned: this.projects,
|
||||
readAndWrite: this.collabertions,
|
||||
readOnly: this.readOnly,
|
||||
tokenReadAndWrite: this.tokenReadAndWrite,
|
||||
tokenReadOnly: this.tokenReadOnly,
|
||||
}
|
||||
|
||||
this.users = {
|
||||
'user-1': {
|
||||
first_name: 'James',
|
||||
},
|
||||
'user-2': {
|
||||
first_name: 'Henry',
|
||||
},
|
||||
}
|
||||
this.users[this.user._id] = this.user // Owner
|
||||
this.UserModel.findById = (id, fields, callback) => {
|
||||
callback(null, this.users[id])
|
||||
}
|
||||
this.UserGetter.getUser = (id, fields, callback) => {
|
||||
callback(null, this.users[id])
|
||||
}
|
||||
|
||||
this.LimitationsManager.hasPaidSubscription.callsArgWith(1, null, false)
|
||||
this.TagsHandler.getAllTags.callsArgWith(1, null, this.tags)
|
||||
this.NotificationsHandler.getUserNotifications = sinon
|
||||
.stub()
|
||||
.callsArgWith(1, null, this.notifications, {})
|
||||
this.ProjectGetter.findAllUsersProjects.callsArgWith(
|
||||
2,
|
||||
null,
|
||||
this.allProjects
|
||||
)
|
||||
})
|
||||
|
||||
it('should render the project/list page', function (done) {
|
||||
this.res.render = (pageName, opts) => {
|
||||
pageName.should.equal('project/list')
|
||||
done()
|
||||
}
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should send the tags', function (done) {
|
||||
this.res.render = (pageName, opts) => {
|
||||
opts.tags.length.should.equal(this.tags.length)
|
||||
done()
|
||||
}
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should create trigger ip matcher notifications', function (done) {
|
||||
this.settings.overleaf = true
|
||||
this.res.render = (pageName, opts) => {
|
||||
this.NotificationBuilder.ipMatcherAffiliation.called.should.equal(true)
|
||||
done()
|
||||
}
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should send the projects', function (done) {
|
||||
this.res.render = (pageName, opts) => {
|
||||
opts.projects.length.should.equal(
|
||||
this.projects.length +
|
||||
this.collabertions.length +
|
||||
this.readOnly.length +
|
||||
this.tokenReadAndWrite.length +
|
||||
this.tokenReadOnly.length
|
||||
)
|
||||
done()
|
||||
}
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should send the user', function (done) {
|
||||
this.res.render = (pageName, opts) => {
|
||||
opts.user.should.deep.equal(this.user)
|
||||
done()
|
||||
}
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should inject the users', function (done) {
|
||||
this.res.render = (pageName, opts) => {
|
||||
opts.projects[0].owner.should.equal(
|
||||
this.users[this.projects[0].owner_ref]
|
||||
)
|
||||
opts.projects[1].owner.should.equal(
|
||||
this.users[this.projects[1].owner_ref]
|
||||
)
|
||||
opts.projects[1].lastUpdatedBy.should.equal(
|
||||
this.users[this.projects[1].lastUpdatedBy]
|
||||
)
|
||||
done()
|
||||
}
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should send hasSubscription == false when no subscription', function (done) {
|
||||
this.res.render = (pageName, opts) => {
|
||||
opts.hasSubscription.should.equal(false)
|
||||
done()
|
||||
}
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should send hasSubscription == true when there is a subscription', function (done) {
|
||||
this.LimitationsManager.hasPaidSubscription = sinon
|
||||
.stub()
|
||||
.callsArgWith(1, null, true)
|
||||
this.res.render = (pageName, opts) => {
|
||||
opts.hasSubscription.should.equal(true)
|
||||
done()
|
||||
}
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
|
||||
it("should send the user's best subscription when saas feature present", function (done) {
|
||||
this.Features.hasFeature.withArgs('saas').returns(true)
|
||||
this.res.render = (pageName, opts) => {
|
||||
expect(opts.usersBestSubscription).to.deep.include({ type: 'free' })
|
||||
done()
|
||||
}
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should not return a best subscription without saas feature', function (done) {
|
||||
this.Features.hasFeature.withArgs('saas').returns(false)
|
||||
this.res.render = (pageName, opts) => {
|
||||
expect(opts.usersBestSubscription).to.be.undefined
|
||||
done()
|
||||
}
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
|
||||
describe('front widget', function (done) {
|
||||
beforeEach(function () {
|
||||
this.settings.overleaf = {
|
||||
front_chat_widget_room_id: 'chat-room-id',
|
||||
}
|
||||
})
|
||||
|
||||
it('should show for paid users', function (done) {
|
||||
this.user.features.github = true
|
||||
this.user.features.dropbox = true
|
||||
this.res.render = (pageName, opts) => {
|
||||
opts.frontChatWidgetRoomId.should.equal(
|
||||
this.settings.overleaf.front_chat_widget_room_id
|
||||
)
|
||||
done()
|
||||
}
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should show for sample users', function (done) {
|
||||
this.user._id = ObjectId('588f3ddae8ebc1bac07c9f00') // last two digits
|
||||
this.res.render = (pageName, opts) => {
|
||||
opts.frontChatWidgetRoomId.should.equal(
|
||||
this.settings.overleaf.front_chat_widget_room_id
|
||||
)
|
||||
done()
|
||||
}
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should not show for non sample users', function (done) {
|
||||
this.user._id = ObjectId('588f3ddae8ebc1bac07c9fff') // last two digits
|
||||
this.res.render = (pageName, opts) => {
|
||||
expect(opts.frontChatWidgetRoomId).to.equal(undefined)
|
||||
done()
|
||||
}
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
})
|
||||
|
||||
describe('With Institution SSO feature', function () {
|
||||
beforeEach(function (done) {
|
||||
this.institutionEmail = 'test@overleaf.com'
|
||||
this.institutionName = 'Overleaf'
|
||||
this.Features.hasFeature.withArgs('saml').returns(true)
|
||||
this.Features.hasFeature.withArgs('affiliations').returns(true)
|
||||
this.Features.hasFeature.withArgs('overleaf-integration').returns(true)
|
||||
done()
|
||||
})
|
||||
it('should show institution SSO available notification for confirmed domains', function () {
|
||||
this.UserGetter.getUserFullEmails.yields(null, [
|
||||
{
|
||||
email: 'test@overleaf.com',
|
||||
affiliation: {
|
||||
institution: {
|
||||
id: 1,
|
||||
confirmed: true,
|
||||
name: 'Overleaf',
|
||||
ssoBeta: false,
|
||||
ssoEnabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
this.res.render = (pageName, opts) => {
|
||||
expect(opts.notificationsInstitution).to.deep.include({
|
||||
email: this.institutionEmail,
|
||||
institutionId: 1,
|
||||
institutionName: this.institutionName,
|
||||
templateKey: 'notification_institution_sso_available',
|
||||
})
|
||||
}
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
it('should show a linked notification', function () {
|
||||
this.req.session.saml = {
|
||||
institutionEmail: this.institutionEmail,
|
||||
linked: {
|
||||
hasEntitlement: false,
|
||||
universityName: this.institutionName,
|
||||
},
|
||||
}
|
||||
this.res.render = (pageName, opts) => {
|
||||
expect(opts.notificationsInstitution).to.deep.include({
|
||||
email: this.institutionEmail,
|
||||
institutionName: this.institutionName,
|
||||
templateKey: 'notification_institution_sso_linked',
|
||||
})
|
||||
}
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
it('should show a linked another email notification', function () {
|
||||
// when they request to link an email but the institution returns
|
||||
// a different email
|
||||
this.res.render = (pageName, opts) => {
|
||||
expect(opts.notificationsInstitution).to.deep.include({
|
||||
institutionEmail: this.institutionEmail,
|
||||
requestedEmail: 'requested@overleaf.com',
|
||||
templateKey: 'notification_institution_sso_non_canonical',
|
||||
})
|
||||
}
|
||||
this.req.session.saml = {
|
||||
emailNonCanonical: this.institutionEmail,
|
||||
institutionEmail: this.institutionEmail,
|
||||
requestedEmail: 'requested@overleaf.com',
|
||||
linked: {
|
||||
hasEntitlement: false,
|
||||
universityName: this.institutionName,
|
||||
},
|
||||
}
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should show a notification when intent was to register via SSO but account existed', function () {
|
||||
this.res.render = (pageName, opts) => {
|
||||
expect(opts.notificationsInstitution).to.deep.include({
|
||||
email: this.institutionEmail,
|
||||
templateKey: 'notification_institution_sso_already_registered',
|
||||
})
|
||||
}
|
||||
this.req.session.saml = {
|
||||
institutionEmail: this.institutionEmail,
|
||||
linked: {
|
||||
hasEntitlement: false,
|
||||
universityName: 'Overleaf',
|
||||
},
|
||||
registerIntercept: {
|
||||
id: 1,
|
||||
name: 'Example University',
|
||||
},
|
||||
}
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should not show a register notification if the flow was abandoned', function () {
|
||||
// could initially start to register with an SSO email and then
|
||||
// abandon flow and login with an existing non-institution SSO email
|
||||
this.res.render = (pageName, opts) => {
|
||||
expect(opts.notificationsInstitution).to.deep.not.include({
|
||||
email: 'test@overleaf.com',
|
||||
templateKey: 'notification_institution_sso_already_registered',
|
||||
})
|
||||
}
|
||||
this.req.session.saml = {
|
||||
registerIntercept: {
|
||||
id: 1,
|
||||
name: 'Example University',
|
||||
},
|
||||
}
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should show error notification', function () {
|
||||
this.res.render = (pageName, opts) => {
|
||||
expect(opts.notificationsInstitution.length).to.equal(1)
|
||||
expect(opts.notificationsInstitution[0].templateKey).to.equal(
|
||||
'notification_institution_sso_error'
|
||||
)
|
||||
expect(opts.notificationsInstitution[0].error).to.be.instanceof(
|
||||
Errors.SAMLAlreadyLinkedError
|
||||
)
|
||||
}
|
||||
this.req.session.saml = {
|
||||
institutionEmail: this.institutionEmail,
|
||||
error: new Errors.SAMLAlreadyLinkedError(),
|
||||
}
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
|
||||
describe('for an unconfirmed domain for an SSO institution', function () {
|
||||
beforeEach(function (done) {
|
||||
this.UserGetter.getUserFullEmails.yields(null, [
|
||||
{
|
||||
email: 'test@overleaf-uncofirmed.com',
|
||||
affiliation: {
|
||||
institution: {
|
||||
id: 1,
|
||||
confirmed: false,
|
||||
name: 'Overleaf',
|
||||
ssoBeta: false,
|
||||
ssoEnabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
done()
|
||||
})
|
||||
it('should not show institution SSO available notification', function () {
|
||||
this.res.render = (pageName, opts) => {
|
||||
expect(opts.notificationsInstitution.length).to.equal(0)
|
||||
}
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
})
|
||||
describe('when linking/logging in initiated on institution side', function () {
|
||||
it('should not show a linked another email notification', function () {
|
||||
// this is only used when initated on Overleaf,
|
||||
// because we keep track of the requested email they tried to link
|
||||
this.res.render = (pageName, opts) => {
|
||||
expect(opts.notificationsInstitution).to.not.deep.include({
|
||||
institutionEmail: this.institutionEmail,
|
||||
requestedEmail: undefined,
|
||||
templateKey: 'notification_institution_sso_non_canonical',
|
||||
})
|
||||
}
|
||||
this.req.session.saml = {
|
||||
emailNonCanonical: this.institutionEmail,
|
||||
institutionEmail: this.institutionEmail,
|
||||
linked: {
|
||||
hasEntitlement: false,
|
||||
universityName: this.institutionName,
|
||||
},
|
||||
}
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
})
|
||||
describe('Institution with SSO beta testable', function () {
|
||||
beforeEach(function (done) {
|
||||
this.UserGetter.getUserFullEmails.yields(null, [
|
||||
{
|
||||
email: 'beta@beta.com',
|
||||
affiliation: {
|
||||
institution: {
|
||||
id: 2,
|
||||
confirmed: true,
|
||||
name: 'Beta University',
|
||||
ssoBeta: true,
|
||||
ssoEnabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
done()
|
||||
})
|
||||
it('should show institution SSO available notification when on a beta testing session', function () {
|
||||
this.req.session.samlBeta = true
|
||||
this.res.render = (pageName, opts) => {
|
||||
expect(opts.notificationsInstitution).to.deep.include({
|
||||
email: 'beta@beta.com',
|
||||
institutionId: 2,
|
||||
institutionName: 'Beta University',
|
||||
templateKey: 'notification_institution_sso_available',
|
||||
})
|
||||
}
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
it('should not show institution SSO available notification when not on a beta testing session', function () {
|
||||
this.req.session.samlBeta = false
|
||||
this.res.render = (pageName, opts) => {
|
||||
expect(opts.notificationsInstitution).to.deep.not.include({
|
||||
email: 'test@overleaf.com',
|
||||
institutionId: 1,
|
||||
institutionName: 'Overleaf',
|
||||
templateKey: 'notification_institution_sso_available',
|
||||
})
|
||||
}
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Without Institution SSO feature', function () {
|
||||
beforeEach(function (done) {
|
||||
this.Features.hasFeature.withArgs('saml').returns(false)
|
||||
done()
|
||||
})
|
||||
it('should not show institution sso available notification', function () {
|
||||
this.res.render = (pageName, opts) => {
|
||||
expect(opts.notificationsInstitution).to.deep.not.include({
|
||||
email: 'test@overleaf.com',
|
||||
institutionId: 1,
|
||||
institutionName: 'Overleaf',
|
||||
templateKey: 'notification_institution_sso_available',
|
||||
})
|
||||
}
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('projectListPage with duplicate projects', function () {
|
||||
beforeEach(function () {
|
||||
this.tags = [
|
||||
{ name: 1, project_ids: ['1', '2', '3'] },
|
||||
{ name: 2, project_ids: ['a', '1'] },
|
||||
{ name: 3, project_ids: ['a', 'b', 'c', 'd'] },
|
||||
]
|
||||
this.notifications = [
|
||||
{
|
||||
_id: '1',
|
||||
user_id: '2',
|
||||
templateKey: '3',
|
||||
messageOpts: '4',
|
||||
key: '5',
|
||||
},
|
||||
]
|
||||
this.projects = [
|
||||
{ _id: 1, lastUpdated: 1, owner_ref: 'user-1' },
|
||||
{ _id: 2, lastUpdated: 2, owner_ref: 'user-2' },
|
||||
]
|
||||
this.collabertions = [{ _id: 5, lastUpdated: 5, owner_ref: 'user-1' }]
|
||||
this.readOnly = [{ _id: 3, lastUpdated: 3, owner_ref: 'user-1' }]
|
||||
this.tokenReadAndWrite = [{ _id: 6, lastUpdated: 5, owner_ref: 'user-4' }]
|
||||
this.tokenReadOnly = [
|
||||
{ _id: 6, lastUpdated: 5, owner_ref: 'user-4' }, // Also in tokenReadAndWrite
|
||||
{ _id: 7, lastUpdated: 4, owner_ref: 'user-5' },
|
||||
]
|
||||
this.allProjects = {
|
||||
owned: this.projects,
|
||||
readAndWrite: this.collabertions,
|
||||
readOnly: this.readOnly,
|
||||
tokenReadAndWrite: this.tokenReadAndWrite,
|
||||
tokenReadOnly: this.tokenReadOnly,
|
||||
}
|
||||
|
||||
this.users = {
|
||||
'user-1': {
|
||||
first_name: 'James',
|
||||
},
|
||||
'user-2': {
|
||||
first_name: 'Henry',
|
||||
},
|
||||
}
|
||||
this.users[this.user._id] = this.user // Owner
|
||||
this.UserModel.findById = (id, fields, callback) => {
|
||||
callback(null, this.users[id])
|
||||
}
|
||||
|
||||
this.LimitationsManager.hasPaidSubscription.callsArgWith(1, null, false)
|
||||
this.TagsHandler.getAllTags.callsArgWith(1, null, this.tags)
|
||||
this.NotificationsHandler.getUserNotifications = sinon
|
||||
.stub()
|
||||
.callsArgWith(1, null, this.notifications, {})
|
||||
this.ProjectGetter.findAllUsersProjects.callsArgWith(
|
||||
2,
|
||||
null,
|
||||
this.allProjects
|
||||
)
|
||||
})
|
||||
|
||||
it('should render the project/list page', function (done) {
|
||||
this.res.render = (pageName, opts) => {
|
||||
pageName.should.equal('project/list')
|
||||
done()
|
||||
}
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should omit one of the projects', function (done) {
|
||||
this.res.render = (pageName, opts) => {
|
||||
opts.projects.length.should.equal(
|
||||
this.projects.length +
|
||||
this.collabertions.length +
|
||||
this.readOnly.length +
|
||||
this.tokenReadAndWrite.length +
|
||||
this.tokenReadOnly.length -
|
||||
1
|
||||
)
|
||||
done()
|
||||
}
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
})
|
||||
|
||||
describe('renameProject', function () {
|
||||
beforeEach(function () {
|
||||
this.newProjectName = 'my supper great new project'
|
||||
|
||||
Reference in New Issue
Block a user