diff --git a/services/web/app/src/Features/Project/ProjectController.js b/services/web/app/src/Features/Project/ProjectController.js index d0ae5cdd79..15dfa4aaa4 100644 --- a/services/web/app/src/Features/Project/ProjectController.js +++ b/services/web/app/src/Features/Project/ProjectController.js @@ -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) { diff --git a/services/web/test/unit/src/Project/ProjectControllerTests.js b/services/web/test/unit/src/Project/ProjectControllerTests.js index 0be0909f26..f048da68bd 100644 --- a/services/web/test/unit/src/Project/ProjectControllerTests.js +++ b/services/web/test/unit/src/Project/ProjectControllerTests.js @@ -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'