ProjectListControllerTests.mjs 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764
  1. import esmock from 'esmock'
  2. import sinon from 'sinon'
  3. import { expect } from 'chai'
  4. import mongodb from 'mongodb-legacy'
  5. import Errors from '../../../../app/src/Features/Errors/Errors.js'
  6. const ObjectId = mongodb.ObjectId
  7. const MODULE_PATH = new URL(
  8. '../../../../app/src/Features/Project/ProjectListController',
  9. import.meta.url
  10. ).pathname
  11. describe('ProjectListController', function () {
  12. beforeEach(async function () {
  13. this.project_id = new ObjectId('abcdefabcdefabcdefabcdef')
  14. this.user = {
  15. _id: new ObjectId('123456123456123456123456'),
  16. email: 'test@overleaf.com',
  17. first_name: 'bjkdsjfk',
  18. features: {},
  19. emails: [{ email: 'test@overleaf.com' }],
  20. lastLoginIp: '111.111.111.112',
  21. }
  22. this.users = {
  23. 'user-1': {
  24. first_name: 'James',
  25. },
  26. 'user-2': {
  27. first_name: 'Henry',
  28. },
  29. }
  30. this.users[this.user._id] = this.user // Owner
  31. this.usersArr = Object.entries(this.users).map(([key, value]) => ({
  32. _id: key,
  33. ...value,
  34. }))
  35. this.tags = [
  36. { name: 1, project_ids: ['1', '2', '3'] },
  37. { name: 2, project_ids: ['a', '1'] },
  38. { name: 3, project_ids: ['a', 'b', 'c', 'd'] },
  39. ]
  40. this.notifications = [
  41. {
  42. _id: '1',
  43. user_id: '2',
  44. templateKey: '3',
  45. messageOpts: '4',
  46. key: '5',
  47. },
  48. ]
  49. this.settings = {
  50. siteUrl: 'https://overleaf.com',
  51. }
  52. this.LimitationsManager = {
  53. promises: {
  54. userIsMemberOfGroupSubscription: sinon.stub().resolves(false),
  55. },
  56. }
  57. this.TagsHandler = {
  58. promises: {
  59. getAllTags: sinon.stub().resolves(this.tags),
  60. },
  61. }
  62. this.NotificationsHandler = {
  63. promises: {
  64. getUserNotifications: sinon.stub().resolves(this.notifications),
  65. },
  66. }
  67. this.UserModel = {
  68. findById: sinon.stub().resolves(this.user),
  69. }
  70. this.UserPrimaryEmailCheckHandler = {
  71. requiresPrimaryEmailCheck: sinon.stub().returns(false),
  72. }
  73. this.ProjectGetter = {
  74. promises: {
  75. findAllUsersProjects: sinon.stub(),
  76. },
  77. }
  78. this.ProjectHelper = {
  79. isArchived: sinon.stub(),
  80. isTrashed: sinon.stub(),
  81. }
  82. this.SessionManager = {
  83. getLoggedInUserId: sinon.stub().returns(this.user._id),
  84. }
  85. this.UserController = {
  86. logout: sinon.stub(),
  87. }
  88. this.UserGetter = {
  89. promises: {
  90. getUsers: sinon.stub().resolves(this.usersArr),
  91. getUserFullEmails: sinon.stub().resolves([]),
  92. },
  93. }
  94. this.Features = {
  95. hasFeature: sinon.stub(),
  96. }
  97. this.Metrics = {
  98. inc: sinon.stub(),
  99. }
  100. this.SplitTestHandler = {
  101. promises: {
  102. getAssignment: sinon.stub().resolves({ variant: 'default' }),
  103. },
  104. }
  105. this.SplitTestSessionHandler = {
  106. promises: {
  107. sessionMaintenance: sinon.stub().resolves(),
  108. },
  109. }
  110. this.SubscriptionViewModelBuilder = {
  111. promises: {
  112. getBestSubscription: sinon.stub().resolves({ type: 'free' }),
  113. },
  114. }
  115. this.SurveyHandler = {
  116. promises: {
  117. getSurvey: sinon.stub().resolves({}),
  118. },
  119. }
  120. this.NotificationBuilder = {
  121. promises: {
  122. ipMatcherAffiliation: sinon.stub().returns({ create: sinon.stub() }),
  123. },
  124. }
  125. this.SubscriptionLocator = {
  126. promises: {
  127. getUserSubscription: sinon.stub().resolves({}),
  128. },
  129. }
  130. this.GeoIpLookup = {
  131. promises: {
  132. getCurrencyCode: sinon.stub().resolves({
  133. countryCode: 'US',
  134. currencyCode: 'USD',
  135. }),
  136. },
  137. }
  138. this.TutorialHandler = {
  139. getInactiveTutorials: sinon.stub().returns([]),
  140. }
  141. this.Modules = {
  142. promises: {
  143. hooks: {
  144. fire: sinon.stub().resolves([]),
  145. },
  146. },
  147. }
  148. this.ProjectListController = await esmock.strict(MODULE_PATH, {
  149. 'mongodb-legacy': { ObjectId },
  150. '@overleaf/settings': this.settings,
  151. '@overleaf/metrics': this.Metrics,
  152. '../../../../app/src/Features/SplitTests/SplitTestHandler':
  153. this.SplitTestHandler,
  154. '../../../../app/src/Features/SplitTests/SplitTestSessionHandler':
  155. this.SplitTestSessionHandler,
  156. '../../../../app/src/Features/User/UserController': this.UserController,
  157. '../../../../app/src/Features/Project/ProjectHelper': this.ProjectHelper,
  158. '../../../../app/src/Features/Subscription/LimitationsManager':
  159. this.LimitationsManager,
  160. '../../../../app/src/Features/Tags/TagsHandler': this.TagsHandler,
  161. '../../../../app/src/Features/Notifications/NotificationsHandler':
  162. this.NotificationsHandler,
  163. '../../../../app/src/models/User': { User: this.UserModel },
  164. '../../../../app/src/Features/Project/ProjectGetter': this.ProjectGetter,
  165. '../../../../app/src/Features/Authentication/SessionManager':
  166. this.SessionManager,
  167. '../../../../app/src/infrastructure/Features': this.Features,
  168. '../../../../app/src/Features/User/UserGetter': this.UserGetter,
  169. '../../../../app/src/Features/Subscription/SubscriptionViewModelBuilder':
  170. this.SubscriptionViewModelBuilder,
  171. '../../../../app/src/infrastructure/Modules': this.Modules,
  172. '../../../../app/src/Features/Survey/SurveyHandler': this.SurveyHandler,
  173. '../../../../app/src/Features/User/UserPrimaryEmailCheckHandler':
  174. this.UserPrimaryEmailCheckHandler,
  175. '../../../../app/src/Features/Notifications/NotificationsBuilder':
  176. this.NotificationBuilder,
  177. '../../../../app/src/Features/Subscription/SubscriptionLocator':
  178. this.SubscriptionLocator,
  179. '../../../../app/src/infrastructure/GeoIpLookup': this.GeoIpLookup,
  180. '../../../../app/src/Features/Tutorial/TutorialHandler':
  181. this.TutorialHandler,
  182. })
  183. this.req = {
  184. query: {},
  185. params: {
  186. Project_id: this.project_id,
  187. },
  188. headers: {},
  189. session: {
  190. user: this.user,
  191. },
  192. body: {},
  193. i18n: {
  194. translate() {},
  195. },
  196. }
  197. this.res = {}
  198. })
  199. describe('projectListPage', function () {
  200. beforeEach(function () {
  201. this.projects = [
  202. { _id: 1, lastUpdated: 1, owner_ref: 'user-1' },
  203. {
  204. _id: 2,
  205. lastUpdated: 2,
  206. owner_ref: 'user-2',
  207. lastUpdatedBy: 'user-1',
  208. },
  209. ]
  210. this.readAndWrite = [{ _id: 5, lastUpdated: 5, owner_ref: 'user-1' }]
  211. this.readOnly = [{ _id: 3, lastUpdated: 3, owner_ref: 'user-1' }]
  212. this.tokenReadAndWrite = [{ _id: 6, lastUpdated: 5, owner_ref: 'user-4' }]
  213. this.tokenReadOnly = [{ _id: 7, lastUpdated: 4, owner_ref: 'user-5' }]
  214. this.review = [{ _id: 8, lastUpdated: 4, owner_ref: 'user-6' }]
  215. this.allProjects = {
  216. owned: this.projects,
  217. readAndWrite: this.readAndWrite,
  218. readOnly: this.readOnly,
  219. tokenReadAndWrite: this.tokenReadAndWrite,
  220. tokenReadOnly: this.tokenReadOnly,
  221. review: this.review,
  222. }
  223. this.ProjectGetter.promises.findAllUsersProjects.resolves(
  224. this.allProjects
  225. )
  226. })
  227. it('should render the project/list-react page', function (done) {
  228. this.res.render = (pageName, opts) => {
  229. pageName.should.equal('project/list-react')
  230. done()
  231. }
  232. this.ProjectListController.projectListPage(this.req, this.res)
  233. })
  234. it('should invoke the session maintenance', function (done) {
  235. this.Features.hasFeature.withArgs('saas').returns(true)
  236. this.res.render = () => {
  237. this.SplitTestSessionHandler.promises.sessionMaintenance.should.have.been.calledWith(
  238. this.req,
  239. this.user
  240. )
  241. done()
  242. }
  243. this.ProjectListController.projectListPage(this.req, this.res)
  244. })
  245. it('should send the tags', function (done) {
  246. this.res.render = (pageName, opts) => {
  247. opts.tags.length.should.equal(this.tags.length)
  248. done()
  249. }
  250. this.ProjectListController.projectListPage(this.req, this.res)
  251. })
  252. it('should create trigger ip matcher notifications', function (done) {
  253. this.settings.overleaf = true
  254. this.req.ip = '111.111.111.111'
  255. this.res.render = (pageName, opts) => {
  256. this.NotificationBuilder.promises.ipMatcherAffiliation.called.should.equal(
  257. true
  258. )
  259. done()
  260. }
  261. this.ProjectListController.projectListPage(this.req, this.res)
  262. })
  263. it('should send the projects', function (done) {
  264. this.res.render = (pageName, opts) => {
  265. opts.prefetchedProjectsBlob.projects.length.should.equal(
  266. this.projects.length +
  267. this.readAndWrite.length +
  268. this.readOnly.length +
  269. this.tokenReadAndWrite.length +
  270. this.tokenReadOnly.length +
  271. this.review.length
  272. )
  273. done()
  274. }
  275. this.ProjectListController.projectListPage(this.req, this.res)
  276. })
  277. it('should send the user', function (done) {
  278. this.res.render = (pageName, opts) => {
  279. opts.user.should.deep.equal(this.user)
  280. done()
  281. }
  282. this.ProjectListController.projectListPage(this.req, this.res)
  283. })
  284. it('should inject the users', function (done) {
  285. this.res.render = (pageName, opts) => {
  286. const projects = opts.prefetchedProjectsBlob.projects
  287. projects
  288. .filter(p => p.id === '1')[0]
  289. .owner.firstName.should.equal(
  290. this.users[this.projects.filter(p => p._id === 1)[0].owner_ref]
  291. .first_name
  292. )
  293. projects
  294. .filter(p => p.id === '2')[0]
  295. .owner.firstName.should.equal(
  296. this.users[this.projects.filter(p => p._id === 2)[0].owner_ref]
  297. .first_name
  298. )
  299. projects
  300. .filter(p => p.id === '2')[0]
  301. .lastUpdatedBy.firstName.should.equal(
  302. this.users[this.projects.filter(p => p._id === 2)[0].lastUpdatedBy]
  303. .first_name
  304. )
  305. done()
  306. }
  307. this.ProjectListController.projectListPage(this.req, this.res)
  308. })
  309. it("should send the user's best subscription when saas feature present", function (done) {
  310. this.Features.hasFeature.withArgs('saas').returns(true)
  311. this.res.render = (pageName, opts) => {
  312. expect(opts.usersBestSubscription).to.deep.include({ type: 'free' })
  313. done()
  314. }
  315. this.ProjectListController.projectListPage(this.req, this.res)
  316. })
  317. it('should not return a best subscription without saas feature', function (done) {
  318. this.Features.hasFeature.withArgs('saas').returns(false)
  319. this.res.render = (pageName, opts) => {
  320. expect(opts.usersBestSubscription).to.be.undefined
  321. done()
  322. }
  323. this.ProjectListController.projectListPage(this.req, this.res)
  324. })
  325. it('should show INR Banner for Indian users with free account', function (done) {
  326. // usersBestSubscription is only available when saas feature is present
  327. this.Features.hasFeature.withArgs('saas').returns(true)
  328. this.SubscriptionViewModelBuilder.promises.getBestSubscription.resolves({
  329. type: 'free',
  330. })
  331. this.GeoIpLookup.promises.getCurrencyCode.resolves({
  332. countryCode: 'IN',
  333. })
  334. this.res.render = (pageName, opts) => {
  335. expect(opts.showInrGeoBanner).to.be.true
  336. done()
  337. }
  338. this.ProjectListController.projectListPage(this.req, this.res)
  339. })
  340. it('should not show INR Banner for Indian users with premium account', function (done) {
  341. // usersBestSubscription is only available when saas feature is present
  342. this.Features.hasFeature.withArgs('saas').returns(true)
  343. this.SubscriptionViewModelBuilder.promises.getBestSubscription.resolves({
  344. type: 'individual',
  345. })
  346. this.GeoIpLookup.promises.getCurrencyCode.resolves({
  347. countryCode: 'IN',
  348. })
  349. this.res.render = (pageName, opts) => {
  350. expect(opts.showInrGeoBanner).to.be.false
  351. done()
  352. }
  353. this.ProjectListController.projectListPage(this.req, this.res)
  354. })
  355. describe('With Institution SSO feature', function () {
  356. beforeEach(function (done) {
  357. this.institutionEmail = 'test@overleaf.com'
  358. this.institutionName = 'Overleaf'
  359. this.Features.hasFeature.withArgs('saml').returns(true)
  360. this.Features.hasFeature.withArgs('affiliations').returns(true)
  361. this.Features.hasFeature.withArgs('saas').returns(true)
  362. done()
  363. })
  364. it('should show institution SSO available notification for confirmed domains', function () {
  365. this.UserGetter.promises.getUserFullEmails.resolves([
  366. {
  367. email: 'test@overleaf.com',
  368. affiliation: {
  369. institution: {
  370. id: 1,
  371. confirmed: true,
  372. name: 'Overleaf',
  373. ssoBeta: false,
  374. ssoEnabled: true,
  375. },
  376. },
  377. },
  378. ])
  379. this.res.render = (pageName, opts) => {
  380. expect(opts.notificationsInstitution).to.deep.include({
  381. email: this.institutionEmail,
  382. institutionId: 1,
  383. institutionName: this.institutionName,
  384. templateKey: 'notification_institution_sso_available',
  385. })
  386. }
  387. this.ProjectListController.projectListPage(this.req, this.res)
  388. })
  389. it('should show a linked notification', function () {
  390. this.req.session.saml = {
  391. institutionEmail: this.institutionEmail,
  392. linked: {
  393. hasEntitlement: false,
  394. universityName: this.institutionName,
  395. },
  396. }
  397. this.res.render = (pageName, opts) => {
  398. expect(opts.notificationsInstitution).to.deep.include({
  399. email: this.institutionEmail,
  400. institutionName: this.institutionName,
  401. templateKey: 'notification_institution_sso_linked',
  402. })
  403. }
  404. this.ProjectListController.projectListPage(this.req, this.res)
  405. })
  406. it('should show a linked another email notification', function () {
  407. // when they request to link an email but the institution returns
  408. // a different email
  409. this.res.render = (pageName, opts) => {
  410. expect(opts.notificationsInstitution).to.deep.include({
  411. institutionEmail: this.institutionEmail,
  412. requestedEmail: 'requested@overleaf.com',
  413. templateKey: 'notification_institution_sso_non_canonical',
  414. })
  415. }
  416. this.req.session.saml = {
  417. emailNonCanonical: this.institutionEmail,
  418. institutionEmail: this.institutionEmail,
  419. requestedEmail: 'requested@overleaf.com',
  420. linked: {
  421. hasEntitlement: false,
  422. universityName: this.institutionName,
  423. },
  424. }
  425. this.ProjectListController.projectListPage(this.req, this.res)
  426. })
  427. it('should show a notification when intent was to register via SSO but account existed', function () {
  428. this.res.render = (pageName, opts) => {
  429. expect(opts.notificationsInstitution).to.deep.include({
  430. email: this.institutionEmail,
  431. templateKey: 'notification_institution_sso_already_registered',
  432. })
  433. }
  434. this.req.session.saml = {
  435. institutionEmail: this.institutionEmail,
  436. linked: {
  437. hasEntitlement: false,
  438. universityName: 'Overleaf',
  439. },
  440. registerIntercept: {
  441. id: 1,
  442. name: 'Example University',
  443. },
  444. }
  445. this.ProjectListController.projectListPage(this.req, this.res)
  446. })
  447. it('should not show a register notification if the flow was abandoned', function () {
  448. // could initially start to register with an SSO email and then
  449. // abandon flow and login with an existing non-institution SSO email
  450. this.res.render = (pageName, opts) => {
  451. expect(opts.notificationsInstitution).to.deep.not.include({
  452. email: 'test@overleaf.com',
  453. templateKey: 'notification_institution_sso_already_registered',
  454. })
  455. }
  456. this.req.session.saml = {
  457. registerIntercept: {
  458. id: 1,
  459. name: 'Example University',
  460. },
  461. }
  462. this.ProjectListController.projectListPage(this.req, this.res)
  463. })
  464. it('should show error notification', function () {
  465. this.res.render = (pageName, opts) => {
  466. expect(opts.notificationsInstitution.length).to.equal(1)
  467. expect(opts.notificationsInstitution[0].templateKey).to.equal(
  468. 'notification_institution_sso_error'
  469. )
  470. expect(opts.notificationsInstitution[0].error).to.be.instanceof(
  471. Errors.SAMLAlreadyLinkedError
  472. )
  473. }
  474. this.req.session.saml = {
  475. institutionEmail: this.institutionEmail,
  476. error: new Errors.SAMLAlreadyLinkedError(),
  477. }
  478. this.ProjectListController.projectListPage(this.req, this.res)
  479. })
  480. describe('for an unconfirmed domain for an SSO institution', function () {
  481. beforeEach(function (done) {
  482. this.UserGetter.promises.getUserFullEmails.resolves([
  483. {
  484. email: 'test@overleaf-uncofirmed.com',
  485. affiliation: {
  486. institution: {
  487. id: 1,
  488. confirmed: false,
  489. name: 'Overleaf',
  490. ssoBeta: false,
  491. ssoEnabled: true,
  492. },
  493. },
  494. },
  495. ])
  496. done()
  497. })
  498. it('should not show institution SSO available notification', function () {
  499. this.res.render = (pageName, opts) => {
  500. expect(opts.notificationsInstitution.length).to.equal(0)
  501. }
  502. this.ProjectListController.projectListPage(this.req, this.res)
  503. })
  504. })
  505. describe('when linking/logging in initiated on institution side', function () {
  506. it('should not show a linked another email notification', function () {
  507. // this is only used when initated on Overleaf,
  508. // because we keep track of the requested email they tried to link
  509. this.res.render = (pageName, opts) => {
  510. expect(opts.notificationsInstitution).to.not.deep.include({
  511. institutionEmail: this.institutionEmail,
  512. requestedEmail: undefined,
  513. templateKey: 'notification_institution_sso_non_canonical',
  514. })
  515. }
  516. this.req.session.saml = {
  517. emailNonCanonical: this.institutionEmail,
  518. institutionEmail: this.institutionEmail,
  519. linked: {
  520. hasEntitlement: false,
  521. universityName: this.institutionName,
  522. },
  523. }
  524. this.ProjectListController.projectListPage(this.req, this.res)
  525. })
  526. })
  527. describe('Institution with SSO beta testable', function () {
  528. beforeEach(function (done) {
  529. this.UserGetter.promises.getUserFullEmails.resolves([
  530. {
  531. email: 'beta@beta.com',
  532. affiliation: {
  533. institution: {
  534. id: 2,
  535. confirmed: true,
  536. name: 'Beta University',
  537. ssoBeta: true,
  538. ssoEnabled: false,
  539. },
  540. },
  541. },
  542. ])
  543. done()
  544. })
  545. it('should show institution SSO available notification when on a beta testing session', function () {
  546. this.req.session.samlBeta = true
  547. this.res.render = (pageName, opts) => {
  548. expect(opts.notificationsInstitution).to.deep.include({
  549. email: 'beta@beta.com',
  550. institutionId: 2,
  551. institutionName: 'Beta University',
  552. templateKey: 'notification_institution_sso_available',
  553. })
  554. }
  555. this.ProjectListController.projectListPage(this.req, this.res)
  556. })
  557. it('should not show institution SSO available notification when not on a beta testing session', function () {
  558. this.req.session.samlBeta = false
  559. this.res.render = (pageName, opts) => {
  560. expect(opts.notificationsInstitution).to.deep.not.include({
  561. email: 'test@overleaf.com',
  562. institutionId: 1,
  563. institutionName: 'Overleaf',
  564. templateKey: 'notification_institution_sso_available',
  565. })
  566. }
  567. this.ProjectListController.projectListPage(this.req, this.res)
  568. })
  569. })
  570. })
  571. describe('Without Institution SSO feature', function () {
  572. beforeEach(function (done) {
  573. this.Features.hasFeature.withArgs('saml').returns(false)
  574. done()
  575. })
  576. it('should not show institution sso available notification', function () {
  577. this.res.render = (pageName, opts) => {
  578. expect(opts.notificationsInstitution).to.deep.not.include({
  579. email: 'test@overleaf.com',
  580. institutionId: 1,
  581. institutionName: 'Overleaf',
  582. templateKey: 'notification_institution_sso_available',
  583. })
  584. }
  585. this.ProjectListController.projectListPage(this.req, this.res)
  586. })
  587. })
  588. describe('enterprise banner', function () {
  589. beforeEach(function (done) {
  590. this.Features.hasFeature.withArgs('saas').returns(true)
  591. this.LimitationsManager.promises.userIsMemberOfGroupSubscription.resolves(
  592. { isMember: false }
  593. )
  594. this.UserGetter.promises.getUserFullEmails.resolves([
  595. {
  596. email: 'test@test-domain.com',
  597. },
  598. ])
  599. done()
  600. })
  601. describe('normal enterprise banner', function () {
  602. it('shows banner', function () {
  603. this.res.render = (pageName, opts) => {
  604. expect(opts.showGroupsAndEnterpriseBanner).to.be.true
  605. }
  606. this.ProjectListController.projectListPage(this.req, this.res)
  607. })
  608. it('does not show banner if user is part of any affiliation', function () {
  609. this.UserGetter.promises.getUserFullEmails.resolves([
  610. {
  611. email: 'test@overleaf.com',
  612. affiliation: {
  613. licence: 'pro_plus',
  614. institution: {
  615. id: 1,
  616. confirmed: true,
  617. name: 'Overleaf',
  618. ssoBeta: false,
  619. ssoEnabled: true,
  620. },
  621. },
  622. },
  623. ])
  624. this.res.render = (pageName, opts) => {
  625. expect(opts.showGroupsAndEnterpriseBanner).to.be.false
  626. }
  627. this.ProjectListController.projectListPage(this.req, this.res)
  628. })
  629. it('does not show banner if user is part of any group subscription', function () {
  630. this.LimitationsManager.promises.userIsMemberOfGroupSubscription.resolves(
  631. { isMember: true }
  632. )
  633. this.res.render = (pageName, opts) => {
  634. expect(opts.showGroupsAndEnterpriseBanner).to.be.false
  635. }
  636. this.ProjectListController.projectListPage(this.req, this.res)
  637. })
  638. it('have a banner variant of "FOMO" or "on-premise"', function () {
  639. this.res.render = (pageName, opts) => {
  640. expect(opts.groupsAndEnterpriseBannerVariant).to.be.oneOf([
  641. 'FOMO',
  642. 'on-premise',
  643. ])
  644. }
  645. this.ProjectListController.projectListPage(this.req, this.res)
  646. })
  647. })
  648. describe('US government enterprise banner', function () {
  649. it('does not show enterprise banner if US government enterprise banner is shown', function () {
  650. const emails = [
  651. {
  652. email: 'test@test.mil',
  653. confirmedAt: new Date('2024-01-01'),
  654. },
  655. ]
  656. this.UserGetter.promises.getUserFullEmails.resolves(emails)
  657. this.Modules.promises.hooks.fire
  658. .withArgs('getUSGovBanner', emails, false, false)
  659. .resolves([
  660. {
  661. showUSGovBanner: true,
  662. usGovBannerVariant: 'variant',
  663. },
  664. ])
  665. this.res.render = (pageName, opts) => {
  666. expect(opts.showGroupsAndEnterpriseBanner).to.be.false
  667. expect(opts.showUSGovBanner).to.be.true
  668. }
  669. this.ProjectListController.projectListPage(this.req, this.res)
  670. })
  671. })
  672. })
  673. })
  674. describe('projectListReactPage with duplicate projects', function () {
  675. beforeEach(function () {
  676. this.projects = [
  677. { _id: 1, lastUpdated: 1, owner_ref: 'user-1' },
  678. { _id: 2, lastUpdated: 2, owner_ref: 'user-2' },
  679. ]
  680. this.readAndWrite = [{ _id: 5, lastUpdated: 5, owner_ref: 'user-1' }]
  681. this.readOnly = [{ _id: 3, lastUpdated: 3, owner_ref: 'user-1' }]
  682. this.tokenReadAndWrite = [{ _id: 6, lastUpdated: 5, owner_ref: 'user-4' }]
  683. this.tokenReadOnly = [
  684. { _id: 6, lastUpdated: 5, owner_ref: 'user-4' }, // Also in tokenReadAndWrite
  685. { _id: 7, lastUpdated: 4, owner_ref: 'user-5' },
  686. ]
  687. this.review = [{ _id: 8, lastUpdated: 5, owner_ref: 'user-6' }]
  688. this.allProjects = {
  689. owned: this.projects,
  690. readAndWrite: this.readAndWrite,
  691. readOnly: this.readOnly,
  692. tokenReadAndWrite: this.tokenReadAndWrite,
  693. tokenReadOnly: this.tokenReadOnly,
  694. review: this.review,
  695. }
  696. this.ProjectGetter.promises.findAllUsersProjects.resolves(
  697. this.allProjects
  698. )
  699. })
  700. it('should render the project/list-react page', function (done) {
  701. this.res.render = (pageName, opts) => {
  702. pageName.should.equal('project/list-react')
  703. done()
  704. }
  705. this.ProjectListController.projectListPage(this.req, this.res)
  706. })
  707. it('should omit one of the projects', function (done) {
  708. this.res.render = (pageName, opts) => {
  709. opts.prefetchedProjectsBlob.projects.length.should.equal(
  710. this.projects.length +
  711. this.readAndWrite.length +
  712. this.readOnly.length +
  713. this.tokenReadAndWrite.length +
  714. this.tokenReadOnly.length +
  715. this.review.length -
  716. 1
  717. )
  718. done()
  719. }
  720. this.ProjectListController.projectListPage(this.req, this.res)
  721. })
  722. })
  723. })