mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-24 01:29:35 +02:00
[web] Check `domainCapturedByGroup` on domain instead of `group.domainCaptureEnabled` only for project/dash redirect GitOrigin-RevId: a6389da9c943327e5941eaa24eb274106526f80b
1102 lines
36 KiB
JavaScript
1102 lines
36 KiB
JavaScript
import { beforeEach, describe, it, expect, vi } from 'vitest'
|
|
import sinon from 'sinon'
|
|
import mongodb from 'mongodb-legacy'
|
|
import Errors from '../../../../app/src/Features/Errors/Errors.js'
|
|
import Settings from '@overleaf/settings'
|
|
|
|
const ObjectId = mongodb.ObjectId
|
|
|
|
const MODULE_PATH = `${import.meta.dirname}/../../../../app/src/Features/Project/ProjectListController`
|
|
|
|
// Mock AnalyticsManager as it isn't used in these tests but causes the User model to be imported and redeclares queues
|
|
vi.mock('../../../../app/src/Features/Analytics/AnalyticsManager.mjs', () => {
|
|
return {
|
|
default: {
|
|
setUserPropertyForUserInBackground: () => {},
|
|
},
|
|
}
|
|
})
|
|
|
|
describe('ProjectListController', function () {
|
|
beforeEach(async function (ctx) {
|
|
ctx.project_id = new ObjectId('abcdefabcdefabcdefabcdef')
|
|
|
|
ctx.user = {
|
|
_id: new ObjectId('123456123456123456123456'),
|
|
email: 'test@overleaf.com',
|
|
first_name: 'bjkdsjfk',
|
|
features: {},
|
|
emails: [{ email: 'test@overleaf.com' }],
|
|
lastActive: new Date(2),
|
|
signUpDate: new Date(1),
|
|
lastLoginIp: '111.111.111.112',
|
|
ace: {
|
|
syntaxValidation: true,
|
|
pdfViewer: 'pdfjs',
|
|
spellCheckLanguage: 'en',
|
|
autoPairDelimiters: true,
|
|
autoComplete: true,
|
|
fontSize: 12,
|
|
theme: 'textmate',
|
|
mode: 'none',
|
|
},
|
|
aiFeatures: { enabled: false },
|
|
}
|
|
ctx.users = {
|
|
'user-1': {
|
|
first_name: 'James',
|
|
},
|
|
'user-2': {
|
|
first_name: 'Henry',
|
|
},
|
|
}
|
|
ctx.users[ctx.user._id] = ctx.user // Owner
|
|
ctx.usersArr = Object.entries(ctx.users).map(([key, value]) => ({
|
|
_id: key,
|
|
...value,
|
|
}))
|
|
ctx.tags = [
|
|
{ name: 1, project_ids: ['1', '2', '3'] },
|
|
{ name: 2, project_ids: ['a', '1'] },
|
|
{ name: 3, project_ids: ['a', 'b', 'c', 'd'] },
|
|
]
|
|
ctx.notifications = [
|
|
{
|
|
_id: '1',
|
|
user_id: '2',
|
|
templateKey: '3',
|
|
messageOpts: '4',
|
|
key: '5',
|
|
},
|
|
]
|
|
ctx.settings = {
|
|
...Settings,
|
|
siteUrl: 'https://overleaf.com',
|
|
}
|
|
ctx.onboardingDataCollection = {
|
|
companyDivisionDepartment: '',
|
|
companyJobTitle: '',
|
|
firstName: 'Dos',
|
|
governmentJobTitle: '',
|
|
institutionName: '',
|
|
lastName: 'Mukasan',
|
|
nonprofitDivisionDepartment: '',
|
|
nonprofitJobTitle: '',
|
|
otherJobTitle: '',
|
|
primaryOccupation: 'company',
|
|
role: 'conductor',
|
|
subjectArea: 'music',
|
|
updatedAt: '2025-09-04T12:12:21.628Z',
|
|
usedLatex: 'occasionally',
|
|
}
|
|
ctx.TagsHandler = {
|
|
promises: {
|
|
getAllTags: sinon.stub().resolves(ctx.tags),
|
|
},
|
|
}
|
|
ctx.NotificationsHandler = {
|
|
promises: {
|
|
getUserNotifications: sinon.stub().resolves(ctx.notifications),
|
|
},
|
|
}
|
|
ctx.UserModel = {
|
|
findById: sinon.stub().resolves(ctx.user),
|
|
}
|
|
ctx.OnboardingDataCollectionModel = {
|
|
findById: sinon.stub().resolves(ctx.onboardingDataCollection),
|
|
}
|
|
ctx.UserPrimaryEmailCheckHandler = {
|
|
requiresPrimaryEmailCheck: sinon.stub().returns(false),
|
|
}
|
|
ctx.ProjectGetter = {
|
|
promises: {
|
|
findAllUsersProjects: sinon.stub(),
|
|
},
|
|
}
|
|
ctx.ProjectHelper = {
|
|
isArchived: sinon.stub(),
|
|
isTrashed: sinon.stub(),
|
|
}
|
|
ctx.SessionManager = {
|
|
getLoggedInUserId: sinon.stub().returns(ctx.user._id),
|
|
}
|
|
ctx.UserController = {
|
|
logout: sinon.stub(),
|
|
}
|
|
ctx.UserGetter = {
|
|
promises: {
|
|
getUsers: sinon.stub().resolves(ctx.usersArr),
|
|
getUserFullEmails: sinon.stub().resolves([]),
|
|
getWritefullData: sinon.stub().resolves({ isPremium: true }),
|
|
},
|
|
}
|
|
ctx.Features = {
|
|
hasFeature: sinon.stub(),
|
|
}
|
|
ctx.SplitTestHandler = {
|
|
promises: {
|
|
getAssignment: sinon.stub().resolves({ variant: 'default' }),
|
|
featureFlagEnabledForUser: sinon.stub().resolves(false),
|
|
hasUserBeenAssignedToVariant: sinon.stub().resolves(false),
|
|
},
|
|
}
|
|
ctx.SplitTestSessionHandler = {
|
|
promises: {
|
|
sessionMaintenance: sinon.stub().resolves(),
|
|
},
|
|
}
|
|
ctx.SubscriptionViewModelBuilder = {
|
|
promises: {
|
|
getUsersSubscriptionDetails: sinon.stub().resolves({
|
|
bestSubscription: { type: 'free' },
|
|
individualSubscription: null,
|
|
memberGroupSubscriptions: [],
|
|
managedGroupSubscriptions: [],
|
|
}),
|
|
},
|
|
}
|
|
ctx.SurveyHandler = {
|
|
promises: {
|
|
getSurvey: sinon.stub().resolves({}),
|
|
},
|
|
}
|
|
ctx.NotificationBuilder = {
|
|
promises: {
|
|
ipMatcherAffiliation: sinon.stub().returns({ create: sinon.stub() }),
|
|
},
|
|
}
|
|
ctx.GeoIpLookup = {
|
|
promises: {
|
|
getCurrencyCode: sinon.stub().resolves({
|
|
countryCode: 'US',
|
|
currencyCode: 'USD',
|
|
}),
|
|
},
|
|
}
|
|
ctx.TutorialHandler = {
|
|
getInactiveTutorials: sinon.stub().returns([]),
|
|
}
|
|
|
|
ctx.Modules = {
|
|
promises: {
|
|
hooks: {
|
|
fire: sinon.stub().resolves([]),
|
|
},
|
|
},
|
|
}
|
|
|
|
ctx.PermissionsManager = {
|
|
promises: {
|
|
checkUserPermissions: sinon.stub().resolves(true),
|
|
},
|
|
}
|
|
|
|
ctx.SubscriptionLocator = {
|
|
promises: {
|
|
getUsersSubscription: sinon.stub().resolves({}),
|
|
},
|
|
}
|
|
|
|
vi.doMock('mongodb-legacy', () => ({
|
|
default: { ObjectId },
|
|
}))
|
|
|
|
vi.doMock('@overleaf/settings', () => ({
|
|
default: ctx.settings,
|
|
}))
|
|
|
|
vi.doMock(
|
|
'../../../../app/src/Features/SplitTests/SplitTestHandler',
|
|
() => ({
|
|
default: ctx.SplitTestHandler,
|
|
})
|
|
)
|
|
|
|
vi.doMock(
|
|
'../../../../app/src/Features/SplitTests/SplitTestSessionHandler',
|
|
() => ({
|
|
default: ctx.SplitTestSessionHandler,
|
|
})
|
|
)
|
|
|
|
vi.doMock('../../../../app/src/Features/User/UserController', () => ({
|
|
default: ctx.UserController,
|
|
}))
|
|
|
|
vi.doMock('../../../../app/src/Features/Project/ProjectHelper', () => ({
|
|
default: ctx.ProjectHelper,
|
|
}))
|
|
|
|
vi.doMock('../../../../app/src/Features/Tags/TagsHandler', () => ({
|
|
default: ctx.TagsHandler,
|
|
}))
|
|
|
|
vi.doMock(
|
|
'../../../../app/src/Features/Notifications/NotificationsHandler',
|
|
() => ({
|
|
default: ctx.NotificationsHandler,
|
|
})
|
|
)
|
|
|
|
vi.doMock('../../../../app/src/models/User', () => ({
|
|
User: ctx.UserModel,
|
|
}))
|
|
|
|
vi.doMock('../../../../app/src/models/OnboardingDataCollection', () => ({
|
|
OnboardingDataCollection: ctx.OnboardingDataCollectionModel,
|
|
}))
|
|
|
|
vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({
|
|
default: ctx.ProjectGetter,
|
|
}))
|
|
|
|
vi.doMock(
|
|
'../../../../app/src/Features/Authentication/SessionManager',
|
|
() => ({
|
|
default: ctx.SessionManager,
|
|
})
|
|
)
|
|
|
|
vi.doMock('../../../../app/src/infrastructure/Features', () => ({
|
|
default: ctx.Features,
|
|
}))
|
|
|
|
vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({
|
|
default: ctx.UserGetter,
|
|
}))
|
|
|
|
vi.doMock(
|
|
'../../../../app/src/Features/Subscription/SubscriptionViewModelBuilder',
|
|
() => ({
|
|
default: ctx.SubscriptionViewModelBuilder,
|
|
})
|
|
)
|
|
|
|
vi.doMock('../../../../app/src/infrastructure/Modules', () => ({
|
|
default: ctx.Modules,
|
|
}))
|
|
|
|
vi.doMock('../../../../app/src/Features/Survey/SurveyHandler', () => ({
|
|
default: ctx.SurveyHandler,
|
|
}))
|
|
|
|
vi.doMock(
|
|
'../../../../app/src/Features/User/UserPrimaryEmailCheckHandler',
|
|
() => ({
|
|
default: ctx.UserPrimaryEmailCheckHandler,
|
|
})
|
|
)
|
|
|
|
vi.doMock(
|
|
'../../../../app/src/Features/Notifications/NotificationsBuilder',
|
|
() => ({
|
|
default: ctx.NotificationBuilder,
|
|
})
|
|
)
|
|
|
|
vi.doMock('../../../../app/src/infrastructure/GeoIpLookup', () => ({
|
|
default: ctx.GeoIpLookup,
|
|
}))
|
|
|
|
vi.doMock('../../../../app/src/Features/Tutorial/TutorialHandler', () => ({
|
|
default: ctx.TutorialHandler,
|
|
}))
|
|
|
|
vi.doMock(
|
|
'../../../../app/src/Features/Authorization/PermissionsManager',
|
|
() => ({
|
|
default: ctx.PermissionsManager,
|
|
})
|
|
)
|
|
vi.doMock(
|
|
'../../../../app/src/Features/Subscription/SubscriptionLocator',
|
|
() => ({
|
|
default: ctx.SubscriptionLocator,
|
|
})
|
|
)
|
|
|
|
ctx.ProjectListController = (await import(MODULE_PATH)).default
|
|
|
|
ctx.req = {
|
|
query: {},
|
|
params: {
|
|
Project_id: ctx.project_id,
|
|
},
|
|
headers: {},
|
|
session: {
|
|
user: ctx.user,
|
|
},
|
|
body: {},
|
|
i18n: {
|
|
translate() {},
|
|
},
|
|
}
|
|
ctx.res = {}
|
|
})
|
|
|
|
describe('projectListPage', function () {
|
|
beforeEach(function (ctx) {
|
|
ctx.projects = [
|
|
{ _id: 1, lastUpdated: new Date(1), owner_ref: 'user-1' },
|
|
{
|
|
_id: 2,
|
|
lastUpdated: new Date(2),
|
|
owner_ref: 'user-2',
|
|
lastUpdatedBy: 'user-1',
|
|
},
|
|
]
|
|
ctx.readAndWrite = [
|
|
{ _id: 5, lastUpdated: new Date(5), owner_ref: 'user-1' },
|
|
]
|
|
ctx.readOnly = [{ _id: 3, lastUpdated: new Date(3), owner_ref: 'user-1' }]
|
|
ctx.tokenReadAndWrite = [
|
|
{ _id: 6, lastUpdated: new Date(5), owner_ref: 'user-4' },
|
|
]
|
|
ctx.tokenReadOnly = [
|
|
{ _id: 7, lastUpdated: new Date(4), owner_ref: 'user-5' },
|
|
]
|
|
ctx.review = [{ _id: 8, lastUpdated: new Date(4), owner_ref: 'user-6' }]
|
|
ctx.allProjects = {
|
|
owned: ctx.projects,
|
|
readAndWrite: ctx.readAndWrite,
|
|
readOnly: ctx.readOnly,
|
|
tokenReadAndWrite: ctx.tokenReadAndWrite,
|
|
tokenReadOnly: ctx.tokenReadOnly,
|
|
review: ctx.review,
|
|
}
|
|
|
|
ctx.ProjectGetter.promises.findAllUsersProjects.resolves(ctx.allProjects)
|
|
})
|
|
|
|
it('should render the project/list-react page', async function (ctx) {
|
|
ctx.res.render = (pageName, opts) => {
|
|
pageName.should.equal('project/list-react')
|
|
}
|
|
await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
|
|
})
|
|
|
|
it('should invoke the session maintenance', async function (ctx) {
|
|
ctx.Features.hasFeature.withArgs('saas').returns(true)
|
|
ctx.res.render = () => {
|
|
ctx.SplitTestSessionHandler.promises.sessionMaintenance.should.have.been.calledWith(
|
|
ctx.req,
|
|
ctx.user
|
|
)
|
|
}
|
|
await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
|
|
})
|
|
|
|
it('should send the tags', async function (ctx) {
|
|
ctx.res.render = (pageName, opts) => {
|
|
opts.tags.length.should.equal(ctx.tags.length)
|
|
}
|
|
await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
|
|
})
|
|
|
|
it('should create trigger ip matcher notifications', async function (ctx) {
|
|
ctx.settings.overleaf = true
|
|
ctx.req.ip = '111.111.111.111'
|
|
ctx.res.render = (pageName, opts) => {
|
|
ctx.NotificationBuilder.promises.ipMatcherAffiliation.called.should.equal(
|
|
true
|
|
)
|
|
}
|
|
await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
|
|
})
|
|
|
|
it('should send the projects', async function (ctx) {
|
|
ctx.res.render = (pageName, opts) => {
|
|
opts.prefetchedProjectsBlob.projects.length.should.equal(
|
|
ctx.projects.length +
|
|
ctx.readAndWrite.length +
|
|
ctx.readOnly.length +
|
|
ctx.tokenReadAndWrite.length +
|
|
ctx.tokenReadOnly.length +
|
|
ctx.review.length
|
|
)
|
|
}
|
|
await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
|
|
})
|
|
|
|
it('should send the user', async function (ctx) {
|
|
ctx.res.render = (pageName, opts) => {
|
|
opts.user.should.deep.equal(ctx.user)
|
|
}
|
|
await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
|
|
})
|
|
|
|
it('should inject the users', async function (ctx) {
|
|
ctx.res.render = (pageName, opts) => {
|
|
const projects = opts.prefetchedProjectsBlob.projects
|
|
|
|
projects
|
|
.filter(p => p.id === '1')[0]
|
|
.owner.firstName.should.equal(
|
|
ctx.users[ctx.projects.filter(p => p._id === 1)[0].owner_ref]
|
|
.first_name
|
|
)
|
|
projects
|
|
.filter(p => p.id === '2')[0]
|
|
.owner.firstName.should.equal(
|
|
ctx.users[ctx.projects.filter(p => p._id === 2)[0].owner_ref]
|
|
.first_name
|
|
)
|
|
projects
|
|
.filter(p => p.id === '2')[0]
|
|
.lastUpdatedBy.firstName.should.equal(
|
|
ctx.users[ctx.projects.filter(p => p._id === 2)[0].lastUpdatedBy]
|
|
.first_name
|
|
)
|
|
}
|
|
await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
|
|
})
|
|
|
|
it("should send the user's best subscription when saas feature present", async function (ctx) {
|
|
ctx.Features.hasFeature.withArgs('saas').returns(true)
|
|
ctx.res.render = (pageName, opts) => {
|
|
expect(opts.usersBestSubscription).to.deep.include({ type: 'free' })
|
|
}
|
|
await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
|
|
})
|
|
|
|
it('should not return a best subscription without saas feature', async function (ctx) {
|
|
ctx.Features.hasFeature.withArgs('saas').returns(false)
|
|
ctx.res.render = (pageName, opts) => {
|
|
expect(opts.usersBestSubscription).to.be.undefined
|
|
}
|
|
await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
|
|
})
|
|
|
|
it('should send groupRole to customer.io for group admins', async function (ctx) {
|
|
ctx.Features.hasFeature.withArgs('saas').returns(true)
|
|
ctx.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails.resolves(
|
|
{
|
|
bestSubscription: { type: 'free' },
|
|
individualSubscription: null,
|
|
memberGroupSubscriptions: [],
|
|
managedGroupSubscriptions: [
|
|
{
|
|
planCode: 'group_professional',
|
|
membersLimit: 12,
|
|
},
|
|
],
|
|
}
|
|
)
|
|
ctx.res.render = () => {}
|
|
|
|
await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
|
|
|
|
expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
|
|
'setUserProperties',
|
|
ctx.user._id,
|
|
sinon.match({
|
|
group_role: 'admin',
|
|
})
|
|
)
|
|
})
|
|
|
|
it('should show INR Banner for Indian users with free account', async function (ctx) {
|
|
// usersBestSubscription is only available when saas feature is present
|
|
ctx.Features.hasFeature.withArgs('saas').returns(true)
|
|
ctx.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails.resolves(
|
|
{
|
|
memberGroupSubscriptions: [],
|
|
managedGroupSubscriptions: [],
|
|
bestSubscription: {
|
|
type: 'free',
|
|
},
|
|
}
|
|
)
|
|
ctx.GeoIpLookup.promises.getCurrencyCode.resolves({
|
|
countryCode: 'IN',
|
|
})
|
|
ctx.res.render = (pageName, opts) => {
|
|
expect(opts.showInrGeoBanner).to.be.true
|
|
}
|
|
await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
|
|
})
|
|
|
|
it('should not show INR Banner for Indian users with premium account', async function (ctx) {
|
|
// usersBestSubscription is only available when saas feature is present
|
|
ctx.Features.hasFeature.withArgs('saas').returns(true)
|
|
ctx.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails.resolves(
|
|
{
|
|
memberGroupSubscriptions: [],
|
|
managedGroupSubscriptions: [],
|
|
bestSubscription: {
|
|
type: 'individual',
|
|
},
|
|
}
|
|
)
|
|
ctx.GeoIpLookup.promises.getCurrencyCode.resolves({
|
|
countryCode: 'IN',
|
|
})
|
|
ctx.res.render = (pageName, opts) => {
|
|
expect(opts.showInrGeoBanner).to.be.false
|
|
}
|
|
await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
|
|
})
|
|
|
|
it('should redirect to domain capture page', async function (ctx) {
|
|
ctx.Features.hasFeature.withArgs('saas').returns(true)
|
|
ctx.SplitTestHandler.promises.getAssignment
|
|
.withArgs(ctx.req, ctx.res, 'domain-capture-redirect')
|
|
.resolves({ variant: 'enabled' })
|
|
ctx.Modules.promises.hooks.fire
|
|
.withArgs('findDomainCaptureGroupsUserCouldBePartOf', ctx.user._id)
|
|
.resolves([
|
|
[
|
|
{
|
|
subscription: { managedUsersEnabled: true },
|
|
},
|
|
],
|
|
])
|
|
ctx.res.redirect = url => {
|
|
url.should.equal('/domain-capture')
|
|
}
|
|
await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
|
|
})
|
|
|
|
it('should not redirect to domain capture page when no domain capture groups found', async function (ctx) {
|
|
ctx.Features.hasFeature.withArgs('saas').returns(true)
|
|
ctx.SplitTestHandler.promises.getAssignment
|
|
.withArgs(ctx.req, ctx.res, 'domain-capture-redirect')
|
|
.resolves({ variant: 'enabled' })
|
|
ctx.Modules.promises.hooks.fire
|
|
.withArgs('findDomainCaptureGroupsUserCouldBePartOf', ctx.user._id)
|
|
.resolves([[]])
|
|
let redirectCalled = false
|
|
ctx.res.redirect = () => {
|
|
redirectCalled = true
|
|
}
|
|
let redirectTo = ''
|
|
ctx.res.render = (pageName, opts) => {
|
|
redirectTo = pageName
|
|
}
|
|
await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
|
|
expect(redirectCalled).to.be.false
|
|
expect(redirectTo).to.equal('project/list-react')
|
|
})
|
|
|
|
describe('when user linked to SSO', function () {
|
|
const linkedEmail = 'picard@starfleet.com'
|
|
const universityName = 'Starfleet'
|
|
const notificationData = {
|
|
email: linkedEmail,
|
|
institutionName: universityName,
|
|
}
|
|
beforeEach(function (ctx) {
|
|
ctx.Features.hasFeature.withArgs('saml').returns(true)
|
|
ctx.req.session.saml = {
|
|
institutionEmail: linkedEmail,
|
|
linked: {
|
|
universityName,
|
|
},
|
|
}
|
|
})
|
|
|
|
it('should render with Commons template when Commons was linked', async function (ctx) {
|
|
ctx.res.render = (pageName, opts) => {
|
|
expect(opts.notificationsInstitution).to.deep.equal([
|
|
Object.assign(
|
|
{ templateKey: 'notification_institution_sso_linked' },
|
|
notificationData
|
|
),
|
|
])
|
|
}
|
|
await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
|
|
})
|
|
|
|
describe('when via domain capture', function () {
|
|
beforeEach(function (ctx) {
|
|
ctx.req.session.saml.domainCaptureEnabled = true
|
|
})
|
|
|
|
it('should render with group template', async function (ctx) {
|
|
ctx.res.render = (pageName, opts) => {
|
|
expect(opts.notificationsInstitution).to.deep.equal([
|
|
Object.assign(
|
|
{ templateKey: 'notification_group_sso_linked' },
|
|
notificationData
|
|
),
|
|
])
|
|
}
|
|
await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
|
|
})
|
|
|
|
describe('user created via domain capture and group is managed', function () {
|
|
beforeEach(function (ctx) {
|
|
ctx.req.session.saml.userCreatedViaDomainCapture = true
|
|
})
|
|
it('should render with notification_group_sso_linked', async function (ctx) {
|
|
ctx.res.render = (pageName, opts) => {
|
|
expect(opts.notificationsInstitution).to.deep.equal([
|
|
Object.assign(
|
|
{
|
|
templateKey: 'notification_group_sso_linked',
|
|
},
|
|
notificationData
|
|
),
|
|
])
|
|
}
|
|
await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
|
|
})
|
|
|
|
it('should render with notification_account_created_via_group_domain_capture_and_managed_users_enabled when managed user is enabled', async function (ctx) {
|
|
ctx.req.session.saml.managedUsersEnabled = true
|
|
ctx.res.render = (pageName, opts) => {
|
|
expect(opts.notificationsInstitution).to.deep.equal([
|
|
Object.assign(
|
|
{
|
|
templateKey:
|
|
'notification_account_created_via_group_domain_capture_and_managed_users_enabled',
|
|
},
|
|
notificationData
|
|
),
|
|
])
|
|
}
|
|
await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('With Institution SSO feature', function () {
|
|
beforeEach(function (ctx) {
|
|
ctx.institutionEmail = 'test@overleaf.com'
|
|
ctx.institutionName = 'Overleaf'
|
|
ctx.Features.hasFeature.withArgs('saml').returns(true)
|
|
ctx.Features.hasFeature.withArgs('affiliations').returns(true)
|
|
ctx.Features.hasFeature.withArgs('saas').returns(true)
|
|
})
|
|
it('should show institution SSO available notification for confirmed domains', async function (ctx) {
|
|
ctx.UserGetter.promises.getUserFullEmails.resolves([
|
|
{
|
|
email: 'test@overleaf.com',
|
|
affiliation: {
|
|
institution: {
|
|
id: 1,
|
|
confirmed: true,
|
|
name: 'Overleaf',
|
|
ssoBeta: false,
|
|
ssoEnabled: true,
|
|
},
|
|
},
|
|
},
|
|
])
|
|
ctx.res.render = (pageName, opts) => {
|
|
expect(opts.notificationsInstitution).to.deep.include({
|
|
email: ctx.institutionEmail,
|
|
institutionId: 1,
|
|
institutionName: ctx.institutionName,
|
|
templateKey: 'notification_institution_sso_available',
|
|
})
|
|
}
|
|
await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
|
|
})
|
|
it('should show a linked notification', async function (ctx) {
|
|
ctx.req.session.saml = {
|
|
institutionEmail: ctx.institutionEmail,
|
|
linked: {
|
|
hasEntitlement: false,
|
|
universityName: ctx.institutionName,
|
|
},
|
|
}
|
|
ctx.res.render = (pageName, opts) => {
|
|
expect(opts.notificationsInstitution).to.deep.include({
|
|
email: ctx.institutionEmail,
|
|
institutionName: ctx.institutionName,
|
|
templateKey: 'notification_institution_sso_linked',
|
|
})
|
|
}
|
|
await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
|
|
})
|
|
it('should show a group linked notification when domain capture enabled', async function (ctx) {
|
|
ctx.req.session.saml = {
|
|
institutionEmail: ctx.institutionEmail,
|
|
linked: {
|
|
hasEntitlement: false,
|
|
universityName: ctx.institutionName,
|
|
},
|
|
domainCaptureEnabled: true,
|
|
}
|
|
ctx.res.render = (pageName, opts) => {
|
|
expect(opts.notificationsInstitution).to.deep.include({
|
|
email: ctx.institutionEmail,
|
|
institutionName: ctx.institutionName,
|
|
templateKey: 'notification_group_sso_linked',
|
|
})
|
|
}
|
|
await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
|
|
})
|
|
it('should show a success notification when joining group via domain capture page', async function (ctx) {
|
|
ctx.req.session.saml = {
|
|
linkedGroup: true,
|
|
universityName: ctx.institutionName,
|
|
domainCaptureJoin: true,
|
|
}
|
|
ctx.res.render = (pageName, opts) => {
|
|
expect(opts).to.deep.include({
|
|
groupSsoSetupSuccess: true,
|
|
joinedGroupName: ctx.institutionName,
|
|
viaDomainCapture: true,
|
|
})
|
|
}
|
|
await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
|
|
})
|
|
it('should show a linked another email notification', async function (ctx) {
|
|
// when they request to link an email but the institution returns
|
|
// a different email
|
|
ctx.res.render = (pageName, opts) => {
|
|
expect(opts.notificationsInstitution).to.deep.include({
|
|
institutionEmail: ctx.institutionEmail,
|
|
requestedEmail: 'requested@overleaf.com',
|
|
templateKey: 'notification_institution_sso_non_canonical',
|
|
})
|
|
}
|
|
ctx.req.session.saml = {
|
|
emailNonCanonical: ctx.institutionEmail,
|
|
institutionEmail: ctx.institutionEmail,
|
|
requestedEmail: 'requested@overleaf.com',
|
|
linked: {
|
|
hasEntitlement: false,
|
|
universityName: ctx.institutionName,
|
|
},
|
|
}
|
|
await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
|
|
})
|
|
|
|
it('should show a notification when intent was to register via SSO but account existed', async function (ctx) {
|
|
ctx.res.render = (pageName, opts) => {
|
|
expect(opts.notificationsInstitution).to.deep.include({
|
|
email: ctx.institutionEmail,
|
|
templateKey: 'notification_institution_sso_already_registered',
|
|
})
|
|
}
|
|
ctx.req.session.saml = {
|
|
institutionEmail: ctx.institutionEmail,
|
|
linked: {
|
|
hasEntitlement: false,
|
|
universityName: 'Overleaf',
|
|
},
|
|
registerIntercept: {
|
|
id: 1,
|
|
name: 'Example University',
|
|
},
|
|
}
|
|
await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
|
|
})
|
|
|
|
it('should not show a register notification if the flow was abandoned', async function (ctx) {
|
|
// could initially start to register with an SSO email and then
|
|
// abandon flow and login with an existing non-institution SSO email
|
|
ctx.res.render = (pageName, opts) => {
|
|
expect(opts.notificationsInstitution).to.deep.not.include({
|
|
email: 'test@overleaf.com',
|
|
templateKey: 'notification_institution_sso_already_registered',
|
|
})
|
|
}
|
|
ctx.req.session.saml = {
|
|
registerIntercept: {
|
|
id: 1,
|
|
name: 'Example University',
|
|
},
|
|
}
|
|
await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
|
|
})
|
|
|
|
it('should show error notification', async function (ctx) {
|
|
ctx.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
|
|
)
|
|
}
|
|
ctx.req.session.saml = {
|
|
institutionEmail: ctx.institutionEmail,
|
|
error: new Errors.SAMLAlreadyLinkedError(),
|
|
}
|
|
await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
|
|
})
|
|
|
|
describe('for an unconfirmed domain for an SSO institution', function () {
|
|
beforeEach(function (ctx) {
|
|
ctx.UserGetter.promises.getUserFullEmails.resolves([
|
|
{
|
|
email: 'test@overleaf-uncofirmed.com',
|
|
affiliation: {
|
|
institution: {
|
|
id: 1,
|
|
confirmed: false,
|
|
name: 'Overleaf',
|
|
ssoBeta: false,
|
|
ssoEnabled: true,
|
|
},
|
|
},
|
|
},
|
|
])
|
|
})
|
|
it('should not show institution SSO available notification', async function (ctx) {
|
|
ctx.res.render = (pageName, opts) => {
|
|
expect(opts.notificationsInstitution.length).to.equal(0)
|
|
}
|
|
await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
|
|
})
|
|
})
|
|
describe('when linking/logging in initiated on institution side', function () {
|
|
it('should not show a linked another email notification', async function (ctx) {
|
|
// this is only used when initated on Overleaf,
|
|
// because we keep track of the requested email they tried to link
|
|
ctx.res.render = (pageName, opts) => {
|
|
expect(opts.notificationsInstitution).to.not.deep.include({
|
|
institutionEmail: ctx.institutionEmail,
|
|
requestedEmail: undefined,
|
|
templateKey: 'notification_institution_sso_non_canonical',
|
|
})
|
|
}
|
|
ctx.req.session.saml = {
|
|
emailNonCanonical: ctx.institutionEmail,
|
|
institutionEmail: ctx.institutionEmail,
|
|
linked: {
|
|
hasEntitlement: false,
|
|
universityName: ctx.institutionName,
|
|
},
|
|
}
|
|
await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
|
|
})
|
|
})
|
|
describe('Institution with SSO beta testable', function () {
|
|
beforeEach(function (ctx) {
|
|
ctx.UserGetter.promises.getUserFullEmails.resolves([
|
|
{
|
|
email: 'beta@beta.com',
|
|
affiliation: {
|
|
institution: {
|
|
id: 2,
|
|
confirmed: true,
|
|
name: 'Beta University',
|
|
ssoBeta: true,
|
|
ssoEnabled: false,
|
|
},
|
|
},
|
|
},
|
|
])
|
|
})
|
|
it('should show institution SSO available notification when on a beta testing session', async function (ctx) {
|
|
ctx.req.session.samlBeta = true
|
|
ctx.res.render = (pageName, opts) => {
|
|
expect(opts.notificationsInstitution).to.deep.include({
|
|
email: 'beta@beta.com',
|
|
institutionId: 2,
|
|
institutionName: 'Beta University',
|
|
templateKey: 'notification_institution_sso_available',
|
|
})
|
|
}
|
|
await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
|
|
})
|
|
it('should not show institution SSO available notification when not on a beta testing session', async function (ctx) {
|
|
ctx.req.session.samlBeta = false
|
|
ctx.res.render = (pageName, opts) => {
|
|
expect(opts.notificationsInstitution).to.deep.not.include({
|
|
email: 'test@overleaf.com',
|
|
institutionId: 1,
|
|
institutionName: 'Overleaf',
|
|
templateKey: 'notification_institution_sso_available',
|
|
})
|
|
}
|
|
await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
|
|
})
|
|
})
|
|
describe('group domain capture enabled for domain', function () {
|
|
it('does not show institution SSO available notification', async function (ctx) {
|
|
ctx.UserGetter.promises.getUserFullEmails.resolves([
|
|
{
|
|
email: 'test@overleaf.com',
|
|
affiliation: {
|
|
group: { domainCaptureEnabled: true },
|
|
institution: {
|
|
id: 1,
|
|
confirmed: true,
|
|
name: 'Overleaf',
|
|
ssoBeta: false,
|
|
ssoEnabled: true,
|
|
},
|
|
},
|
|
},
|
|
])
|
|
ctx.res.render = (pageName, opts) => {
|
|
expect(opts.notificationsInstitution).to.deep.equal([])
|
|
}
|
|
await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Without Institution SSO feature', function () {
|
|
beforeEach(function (ctx) {
|
|
ctx.Features.hasFeature.withArgs('saml').returns(false)
|
|
})
|
|
it('should not show institution sso available notification', async function (ctx) {
|
|
ctx.res.render = (pageName, opts) => {
|
|
expect(opts.notificationsInstitution).to.deep.not.include({
|
|
email: 'test@overleaf.com',
|
|
institutionId: 1,
|
|
institutionName: 'Overleaf',
|
|
templateKey: 'notification_institution_sso_available',
|
|
})
|
|
}
|
|
await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
|
|
})
|
|
})
|
|
|
|
describe('enterprise banner', function () {
|
|
beforeEach(function (ctx) {
|
|
ctx.Features.hasFeature.withArgs('saas').returns(true)
|
|
ctx.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails.resolves(
|
|
{ memberGroupSubscriptions: [], managedGroupSubscriptions: [] }
|
|
)
|
|
ctx.UserGetter.promises.getUserFullEmails.resolves([
|
|
{
|
|
email: 'test@test-domain.com',
|
|
},
|
|
])
|
|
})
|
|
|
|
describe('normal enterprise banner', function () {
|
|
it('shows banner', async function (ctx) {
|
|
ctx.res.render = (pageName, opts) => {
|
|
expect(opts.showGroupsAndEnterpriseBanner).to.be.true
|
|
}
|
|
await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
|
|
})
|
|
|
|
it('does not show banner if user is part of any affiliation', async function (ctx) {
|
|
ctx.UserGetter.promises.getUserFullEmails.resolves([
|
|
{
|
|
email: 'test@overleaf.com',
|
|
affiliation: {
|
|
licence: 'pro_plus',
|
|
institution: {
|
|
id: 1,
|
|
confirmed: true,
|
|
name: 'Overleaf',
|
|
ssoBeta: false,
|
|
ssoEnabled: true,
|
|
},
|
|
},
|
|
},
|
|
])
|
|
|
|
ctx.res.render = (pageName, opts) => {
|
|
expect(opts.showGroupsAndEnterpriseBanner).to.be.false
|
|
}
|
|
await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
|
|
})
|
|
|
|
it('does not show banner if user is part of any group subscription', async function (ctx) {
|
|
ctx.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails.resolves(
|
|
{ memberGroupSubscriptions: [{}] }
|
|
)
|
|
|
|
ctx.res.render = (pageName, opts) => {
|
|
expect(opts.showGroupsAndEnterpriseBanner).to.be.false
|
|
}
|
|
await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
|
|
})
|
|
|
|
it('have a banner variant of "FOMO" or "on-premise"', async function (ctx) {
|
|
ctx.res.render = (pageName, opts) => {
|
|
expect(opts.groupsAndEnterpriseBannerVariant).to.be.oneOf([
|
|
'FOMO',
|
|
'on-premise',
|
|
])
|
|
}
|
|
await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
|
|
})
|
|
})
|
|
|
|
describe('US government enterprise banner', function () {
|
|
it('does not show enterprise banner if US government enterprise banner is shown', async function (ctx) {
|
|
const emails = [
|
|
{
|
|
email: 'test@test.mil',
|
|
confirmedAt: new Date('2024-01-01'),
|
|
},
|
|
]
|
|
|
|
ctx.UserGetter.promises.getUserFullEmails.resolves(emails)
|
|
ctx.Modules.promises.hooks.fire
|
|
.withArgs('getUSGovBanner', emails, false, [])
|
|
.resolves([
|
|
{
|
|
showUSGovBanner: true,
|
|
usGovBannerVariant: 'variant',
|
|
},
|
|
])
|
|
ctx.res.render = (pageName, opts) => {
|
|
expect(opts.showGroupsAndEnterpriseBanner).to.be.false
|
|
expect(opts.showUSGovBanner).to.be.true
|
|
}
|
|
await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('projectListReactPage with duplicate projects', function () {
|
|
beforeEach(function (ctx) {
|
|
ctx.projects = [
|
|
{ _id: 1, lastUpdated: new Date(1), owner_ref: 'user-1' },
|
|
{ _id: 2, lastUpdated: new Date(2), owner_ref: 'user-2' },
|
|
]
|
|
ctx.readAndWrite = [
|
|
{ _id: 5, lastUpdated: new Date(5), owner_ref: 'user-1' },
|
|
]
|
|
ctx.readOnly = [{ _id: 3, lastUpdated: new Date(3), owner_ref: 'user-1' }]
|
|
ctx.tokenReadAndWrite = [
|
|
{ _id: 6, lastUpdated: new Date(5), owner_ref: 'user-4' },
|
|
]
|
|
ctx.tokenReadOnly = [
|
|
{ _id: 6, lastUpdated: new Date(5), owner_ref: 'user-4' }, // Also in tokenReadAndWrite
|
|
{ _id: 7, lastUpdated: new Date(4), owner_ref: 'user-5' },
|
|
]
|
|
ctx.review = [{ _id: 8, lastUpdated: new Date(5), owner_ref: 'user-6' }]
|
|
ctx.allProjects = {
|
|
owned: ctx.projects,
|
|
readAndWrite: ctx.readAndWrite,
|
|
readOnly: ctx.readOnly,
|
|
tokenReadAndWrite: ctx.tokenReadAndWrite,
|
|
tokenReadOnly: ctx.tokenReadOnly,
|
|
review: ctx.review,
|
|
}
|
|
|
|
ctx.ProjectGetter.promises.findAllUsersProjects.resolves(ctx.allProjects)
|
|
})
|
|
|
|
it('should render the project/list-react page', async function (ctx) {
|
|
ctx.res.render = (pageName, opts) => {
|
|
pageName.should.equal('project/list-react')
|
|
}
|
|
await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
|
|
})
|
|
|
|
it('should omit one of the projects', async function (ctx) {
|
|
ctx.res.render = (pageName, opts) => {
|
|
opts.prefetchedProjectsBlob.projects.length.should.equal(
|
|
ctx.projects.length +
|
|
ctx.readAndWrite.length +
|
|
ctx.readOnly.length +
|
|
ctx.tokenReadAndWrite.length +
|
|
ctx.tokenReadOnly.length +
|
|
ctx.review.length -
|
|
1
|
|
)
|
|
}
|
|
await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
|
|
})
|
|
})
|
|
})
|