SubscriptionGroupControllerTests.mjs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647
  1. import esmock from 'esmock'
  2. import sinon from 'sinon'
  3. const modulePath =
  4. '../../../../app/src/Features/Subscription/SubscriptionGroupController'
  5. describe('SubscriptionGroupController', function () {
  6. beforeEach(async function () {
  7. this.user = { _id: '!@312431', email: 'user@email.com' }
  8. this.adminUserId = '123jlkj'
  9. this.subscriptionId = '123434325412'
  10. this.user_email = 'bob@gmail.com'
  11. this.req = {
  12. session: {
  13. user: {
  14. _id: this.adminUserId,
  15. email: this.user_email,
  16. },
  17. },
  18. params: {
  19. subscriptionId: this.subscriptionId,
  20. },
  21. query: {},
  22. }
  23. this.subscription = {
  24. _id: this.subscriptionId,
  25. teamName: 'Cool group',
  26. groupPlan: true,
  27. membersLimit: 5,
  28. }
  29. this.plan = {
  30. canUseFlexibleLicensing: true,
  31. }
  32. this.previewSubscriptionChangeData = {
  33. change: {},
  34. currency: 'USD',
  35. }
  36. this.createSubscriptionChangeData = { adding: 1 }
  37. this.SubscriptionGroupHandler = {
  38. promises: {
  39. removeUserFromGroup: sinon.stub().resolves(),
  40. getUsersGroupSubscriptionDetails: sinon.stub().resolves({
  41. subscription: this.subscription,
  42. plan: this.plan,
  43. recurlySubscription: {},
  44. }),
  45. previewAddSeatsSubscriptionChange: sinon
  46. .stub()
  47. .resolves(this.previewSubscriptionChangeData),
  48. createAddSeatsSubscriptionChange: sinon
  49. .stub()
  50. .resolves(this.createSubscriptionChangeData),
  51. ensureFlexibleLicensingEnabled: sinon.stub().resolves(),
  52. ensureSubscriptionIsActive: sinon.stub().resolves(),
  53. getGroupPlanUpgradePreview: sinon
  54. .stub()
  55. .resolves(this.previewSubscriptionChangeData),
  56. },
  57. }
  58. this.SubscriptionLocator = {
  59. promises: {
  60. getSubscription: sinon.stub().resolves(this.subscription),
  61. },
  62. }
  63. this.SessionManager = {
  64. getLoggedInUserId(session) {
  65. return session.user._id
  66. },
  67. getSessionUser(session) {
  68. return session.user
  69. },
  70. }
  71. this.UserAuditLogHandler = {
  72. promises: {
  73. addEntry: sinon.stub().resolves(),
  74. },
  75. }
  76. this.Modules = {
  77. promises: {
  78. hooks: {
  79. fire: sinon.stub().resolves(),
  80. },
  81. },
  82. }
  83. this.SplitTestHandler = {
  84. promises: {
  85. getAssignment: sinon.stub().resolves(),
  86. },
  87. }
  88. this.UserGetter = {
  89. promises: {
  90. getUserEmail: sinon.stub().resolves(this.user.email),
  91. },
  92. }
  93. this.paymentMethod = { cardType: 'Visa', lastFour: '1111' }
  94. this.RecurlyClient = {
  95. promises: {
  96. getPaymentMethod: sinon.stub().resolves(this.paymentMethod),
  97. },
  98. }
  99. this.SubscriptionController = {}
  100. this.SubscriptionModel = { Subscription: {} }
  101. this.PlansHelper = {
  102. isProfessionalGroupPlan: sinon.stub().returns(false),
  103. }
  104. this.Errors = {
  105. MissingBillingInfoError: class MissingBillingInfoError extends Error {},
  106. }
  107. this.Controller = await esmock.strict(modulePath, {
  108. '../../../../app/src/Features/Subscription/SubscriptionGroupHandler':
  109. this.SubscriptionGroupHandler,
  110. '../../../../app/src/Features/Subscription/SubscriptionLocator':
  111. this.SubscriptionLocator,
  112. '../../../../app/src/Features/Authentication/SessionManager':
  113. this.SessionManager,
  114. '../../../../app/src/Features/User/UserAuditLogHandler':
  115. this.UserAuditLogHandler,
  116. '../../../../app/src/infrastructure/Modules': this.Modules,
  117. '../../../../app/src/Features/SplitTests/SplitTestHandler':
  118. this.SplitTestHandler,
  119. '../../../../app/src/Features/User/UserGetter': this.UserGetter,
  120. '../../../../app/src/Features/Errors/ErrorController':
  121. (this.ErrorController = {
  122. notFound: sinon.stub(),
  123. }),
  124. '../../../../app/src/Features/Subscription/SubscriptionController':
  125. this.SubscriptionController,
  126. '../../../../app/src/Features/Subscription/RecurlyClient':
  127. this.RecurlyClient,
  128. '../../../../app/src/Features/Subscription/PlansHelper': this.PlansHelper,
  129. '../../../../app/src/Features/Subscription/Errors': this.Errors,
  130. '../../../../app/src/models/Subscription': this.SubscriptionModel,
  131. '@overleaf/logger': {
  132. err: sinon.stub(),
  133. error: sinon.stub(),
  134. warn: sinon.stub(),
  135. log: sinon.stub(),
  136. debug: sinon.stub(),
  137. },
  138. })
  139. })
  140. describe('removeUserFromGroup', function () {
  141. it('should use the subscription id for the logged in user and take the user id from the params', function (done) {
  142. const userIdToRemove = '31231'
  143. this.req.params = { user_id: userIdToRemove }
  144. this.req.entity = this.subscription
  145. const res = {
  146. sendStatus: () => {
  147. this.SubscriptionGroupHandler.promises.removeUserFromGroup
  148. .calledWith(this.subscriptionId, userIdToRemove)
  149. .should.equal(true)
  150. done()
  151. },
  152. }
  153. this.Controller.removeUserFromGroup(this.req, res, done)
  154. })
  155. it('should log that the user has been removed', function (done) {
  156. const userIdToRemove = '31231'
  157. this.req.params = { user_id: userIdToRemove }
  158. this.req.entity = this.subscription
  159. const res = {
  160. sendStatus: () => {
  161. sinon.assert.calledWith(
  162. this.UserAuditLogHandler.promises.addEntry,
  163. userIdToRemove,
  164. 'remove-from-group-subscription',
  165. this.adminUserId,
  166. this.req.ip,
  167. { subscriptionId: this.subscriptionId }
  168. )
  169. done()
  170. },
  171. }
  172. this.Controller.removeUserFromGroup(this.req, res, done)
  173. })
  174. it('should call the group SSO hooks with group SSO enabled', function (done) {
  175. const userIdToRemove = '31231'
  176. this.req.params = { user_id: userIdToRemove }
  177. this.req.entity = this.subscription
  178. this.Modules.promises.hooks.fire
  179. .withArgs('hasGroupSSOEnabled', this.subscription)
  180. .resolves([true])
  181. const res = {
  182. sendStatus: () => {
  183. this.Modules.promises.hooks.fire
  184. .calledWith('hasGroupSSOEnabled', this.subscription)
  185. .should.equal(true)
  186. this.Modules.promises.hooks.fire
  187. .calledWith(
  188. 'unlinkUserFromGroupSSO',
  189. userIdToRemove,
  190. this.subscriptionId
  191. )
  192. .should.equal(true)
  193. sinon.assert.calledTwice(this.Modules.promises.hooks.fire)
  194. done()
  195. },
  196. }
  197. this.Controller.removeUserFromGroup(this.req, res, done)
  198. })
  199. it('should call the group SSO hooks with group SSO disabled', function (done) {
  200. const userIdToRemove = '31231'
  201. this.req.params = { user_id: userIdToRemove }
  202. this.req.entity = this.subscription
  203. this.Modules.promises.hooks.fire
  204. .withArgs('hasGroupSSOEnabled', this.subscription)
  205. .resolves([false])
  206. const res = {
  207. sendStatus: () => {
  208. this.Modules.promises.hooks.fire
  209. .calledWith('hasGroupSSOEnabled', this.subscription)
  210. .should.equal(true)
  211. sinon.assert.calledOnce(this.Modules.promises.hooks.fire)
  212. done()
  213. },
  214. }
  215. this.Controller.removeUserFromGroup(this.req, res, done)
  216. })
  217. })
  218. describe('removeSelfFromGroup', function () {
  219. it('gets subscription and remove user', function (done) {
  220. this.req.query = { subscriptionId: this.subscriptionId }
  221. const memberUserIdToremove = 123456789
  222. this.req.session.user._id = memberUserIdToremove
  223. const res = {
  224. sendStatus: () => {
  225. sinon.assert.calledWith(
  226. this.SubscriptionLocator.promises.getSubscription,
  227. this.subscriptionId
  228. )
  229. sinon.assert.calledWith(
  230. this.SubscriptionGroupHandler.promises.removeUserFromGroup,
  231. this.subscriptionId,
  232. memberUserIdToremove
  233. )
  234. done()
  235. },
  236. }
  237. this.Controller.removeSelfFromGroup(this.req, res, done)
  238. })
  239. it('should log that the user has left the subscription', function (done) {
  240. this.req.query = { subscriptionId: this.subscriptionId }
  241. const memberUserIdToremove = '123456789'
  242. this.req.session.user._id = memberUserIdToremove
  243. const res = {
  244. sendStatus: () => {
  245. sinon.assert.calledWith(
  246. this.UserAuditLogHandler.promises.addEntry,
  247. memberUserIdToremove,
  248. 'remove-from-group-subscription',
  249. memberUserIdToremove,
  250. this.req.ip,
  251. { subscriptionId: this.subscriptionId }
  252. )
  253. done()
  254. },
  255. }
  256. this.Controller.removeSelfFromGroup(this.req, res, done)
  257. })
  258. it('should call the group SSO hooks with group SSO enabled', function (done) {
  259. this.req.query = { subscriptionId: this.subscriptionId }
  260. const memberUserIdToremove = '123456789'
  261. this.req.session.user._id = memberUserIdToremove
  262. this.Modules.promises.hooks.fire
  263. .withArgs('hasGroupSSOEnabled', this.subscription)
  264. .resolves([true])
  265. const res = {
  266. sendStatus: () => {
  267. this.Modules.promises.hooks.fire
  268. .calledWith('hasGroupSSOEnabled', this.subscription)
  269. .should.equal(true)
  270. this.Modules.promises.hooks.fire
  271. .calledWith(
  272. 'unlinkUserFromGroupSSO',
  273. memberUserIdToremove,
  274. this.subscriptionId
  275. )
  276. .should.equal(true)
  277. sinon.assert.calledTwice(this.Modules.promises.hooks.fire)
  278. done()
  279. },
  280. }
  281. this.Controller.removeSelfFromGroup(this.req, res, done)
  282. })
  283. it('should call the group SSO hooks with group SSO disabled', function (done) {
  284. const userIdToRemove = '31231'
  285. this.req.session.user._id = userIdToRemove
  286. this.req.params = { user_id: userIdToRemove }
  287. this.req.entity = this.subscription
  288. this.Modules.promises.hooks.fire
  289. .withArgs('hasGroupSSOEnabled', this.subscription)
  290. .resolves([false])
  291. const res = {
  292. sendStatus: () => {
  293. this.Modules.promises.hooks.fire
  294. .calledWith('hasGroupSSOEnabled', this.subscription)
  295. .should.equal(true)
  296. sinon.assert.calledOnce(this.Modules.promises.hooks.fire)
  297. done()
  298. },
  299. }
  300. this.Controller.removeSelfFromGroup(this.req, res, done)
  301. })
  302. })
  303. describe('addSeatsToGroupSubscription', function () {
  304. it('should render the "add seats" page', function (done) {
  305. const res = {
  306. render: (page, props) => {
  307. this.SubscriptionGroupHandler.promises.getUsersGroupSubscriptionDetails
  308. .calledWith(this.req.session.user._id)
  309. .should.equal(true)
  310. this.SubscriptionGroupHandler.promises.ensureFlexibleLicensingEnabled
  311. .calledWith(this.plan)
  312. .should.equal(true)
  313. this.SubscriptionGroupHandler.promises.ensureSubscriptionIsActive
  314. .calledWith(this.subscription)
  315. .should.equal(true)
  316. page.should.equal('subscriptions/add-seats')
  317. props.subscriptionId.should.equal(this.subscriptionId)
  318. props.groupName.should.equal(this.subscription.teamName)
  319. props.totalLicenses.should.equal(this.subscription.membersLimit)
  320. props.isProfessional.should.equal(false)
  321. done()
  322. },
  323. }
  324. this.Controller.addSeatsToGroupSubscription(this.req, res)
  325. })
  326. it('should redirect to subscription page when getting subscription details fails', function (done) {
  327. this.SubscriptionGroupHandler.promises.getUsersGroupSubscriptionDetails =
  328. sinon.stub().rejects()
  329. const res = {
  330. redirect: url => {
  331. url.should.equal('/user/subscription')
  332. done()
  333. },
  334. }
  335. this.Controller.addSeatsToGroupSubscription(this.req, res)
  336. })
  337. it('should redirect to subscription page when flexible licensing is not enabled', function (done) {
  338. this.SubscriptionGroupHandler.promises.ensureFlexibleLicensingEnabled =
  339. sinon.stub().rejects()
  340. const res = {
  341. redirect: url => {
  342. url.should.equal('/user/subscription')
  343. done()
  344. },
  345. }
  346. this.Controller.addSeatsToGroupSubscription(this.req, res)
  347. })
  348. it('should redirect to missing billing information page when billing information is missing', function (done) {
  349. this.RecurlyClient.promises.getPaymentMethod = sinon
  350. .stub()
  351. .throws(new this.Errors.MissingBillingInfoError())
  352. const res = {
  353. redirect: url => {
  354. url.should.equal(
  355. '/user/subscription/group/missing-billing-information'
  356. )
  357. done()
  358. },
  359. }
  360. this.Controller.addSeatsToGroupSubscription(this.req, res)
  361. })
  362. it('should redirect to subscription page when subscription is not active', function (done) {
  363. this.SubscriptionGroupHandler.promises.ensureSubscriptionIsActive = sinon
  364. .stub()
  365. .rejects()
  366. const res = {
  367. redirect: url => {
  368. url.should.equal('/user/subscription')
  369. done()
  370. },
  371. }
  372. this.Controller.addSeatsToGroupSubscription(this.req, res)
  373. })
  374. })
  375. describe('previewAddSeatsSubscriptionChange', function () {
  376. it('should preview "add seats" change', function (done) {
  377. this.req.body = { adding: 2 }
  378. const res = {
  379. json: data => {
  380. this.SubscriptionGroupHandler.promises.previewAddSeatsSubscriptionChange
  381. .calledWith(this.req.session.user._id, this.req.body.adding)
  382. .should.equal(true)
  383. data.should.deep.equal(this.previewSubscriptionChangeData)
  384. done()
  385. },
  386. }
  387. this.Controller.previewAddSeatsSubscriptionChange(this.req, res)
  388. })
  389. it('should fail previewing "add seats" change', function (done) {
  390. this.SubscriptionGroupHandler.promises.previewAddSeatsSubscriptionChange =
  391. sinon.stub().rejects()
  392. const res = {
  393. status: statusCode => {
  394. statusCode.should.equal(400)
  395. return {
  396. end: () => {
  397. done()
  398. },
  399. }
  400. },
  401. }
  402. this.Controller.previewAddSeatsSubscriptionChange(this.req, res)
  403. })
  404. })
  405. describe('createAddSeatsSubscriptionChange', function () {
  406. it('should apply "add seats" change', function (done) {
  407. this.req.body = { adding: 2 }
  408. const res = {
  409. json: data => {
  410. this.SubscriptionGroupHandler.promises.createAddSeatsSubscriptionChange
  411. .calledWith(this.req.session.user._id, this.req.body.adding)
  412. .should.equal(true)
  413. data.should.deep.equal(this.createSubscriptionChangeData)
  414. done()
  415. },
  416. }
  417. this.Controller.createAddSeatsSubscriptionChange(this.req, res)
  418. })
  419. it('should fail applying "add seats" change', function (done) {
  420. this.SubscriptionGroupHandler.promises.createAddSeatsSubscriptionChange =
  421. sinon.stub().rejects()
  422. const res = {
  423. status: statusCode => {
  424. statusCode.should.equal(400)
  425. return {
  426. end: () => {
  427. done()
  428. },
  429. }
  430. },
  431. }
  432. this.Controller.createAddSeatsSubscriptionChange(this.req, res)
  433. })
  434. })
  435. describe('submitForm', function () {
  436. it('should build and pass the request body to the sales submit handler', function (done) {
  437. const adding = 100
  438. this.req.body = { adding }
  439. const res = {
  440. sendStatus: code => {
  441. this.Modules.promises.hooks.fire
  442. .calledWith('sendSupportRequest', {
  443. email: this.user.email,
  444. subject: 'Sales Contact Form',
  445. message:
  446. '\n' +
  447. '**Overleaf Sales Contact Form:**\n' +
  448. '\n' +
  449. '**Subject:** Self-Serve Group User Increase Request\n' +
  450. '\n' +
  451. `**Estimated Number of Users:** ${adding}\n` +
  452. '\n' +
  453. `**Message:** This email has been generated on behalf of user with email **${this.user.email}** to request an increase in the total number of users for their subscription.`,
  454. inbox: 'sales',
  455. })
  456. .should.equal(true)
  457. sinon.assert.calledOnce(this.Modules.promises.hooks.fire)
  458. code.should.equal(204)
  459. done()
  460. },
  461. }
  462. this.Controller.submitForm(this.req, res, done)
  463. })
  464. })
  465. describe('flexibleLicensingSplitTest', function () {
  466. it('passes when the variant is "enabled"', function (done) {
  467. const res = sinon.stub()
  468. const next = () => {
  469. this.ErrorController.notFound.notCalled.should.equal(true)
  470. done()
  471. }
  472. this.SplitTestHandler.promises.getAssignment.resolves({
  473. variant: 'enabled',
  474. })
  475. this.Controller.flexibleLicensingSplitTest(this.req, res, next, done)
  476. })
  477. it('returns error page when the variant is "default"', function (done) {
  478. const res = sinon.stub()
  479. const next = sinon.stub()
  480. this.ErrorController.notFound = sinon.stub().callsFake(() => {
  481. next.notCalled.should.equal(true)
  482. done()
  483. })
  484. this.SplitTestHandler.promises.getAssignment.resolves({
  485. variant: 'default',
  486. })
  487. this.Controller.flexibleLicensingSplitTest(this.req, res, next, done)
  488. })
  489. })
  490. describe('subscriptionUpgradePage', function () {
  491. it('should render "subscription upgrade" page', function (done) {
  492. const olSubscription = { membersLimit: 1, teamName: 'test team' }
  493. this.SubscriptionModel.Subscription.findOne = () => {
  494. return {
  495. exec: () => olSubscription,
  496. }
  497. }
  498. const res = {
  499. render: (page, data) => {
  500. this.SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview
  501. .calledWith(this.req.session.user._id)
  502. .should.equal(true)
  503. page.should.equal('subscriptions/upgrade-group-subscription-react')
  504. data.totalLicenses.should.equal(olSubscription.membersLimit)
  505. data.groupName.should.equal(olSubscription.teamName)
  506. data.changePreview.should.equal(this.previewSubscriptionChangeData)
  507. done()
  508. },
  509. }
  510. this.Controller.subscriptionUpgradePage(this.req, res)
  511. })
  512. it('should redirect if failed to generate preview', function (done) {
  513. this.SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview = sinon
  514. .stub()
  515. .rejects()
  516. const res = {
  517. redirect: url => {
  518. url.should.equal('/user/subscription')
  519. done()
  520. },
  521. }
  522. this.Controller.subscriptionUpgradePage(this.req, res)
  523. })
  524. it('should redirect to missing billing information page when billing information is missing', function (done) {
  525. this.RecurlyClient.promises.getPaymentMethod = sinon
  526. .stub()
  527. .throws(new this.Errors.MissingBillingInfoError())
  528. const res = {
  529. redirect: url => {
  530. url.should.equal('/user/subscription')
  531. done()
  532. },
  533. }
  534. this.Controller.subscriptionUpgradePage(this.req, res)
  535. })
  536. })
  537. describe('upgradeSubscription', function () {
  538. it('should send 200 response', function (done) {
  539. this.SubscriptionGroupHandler.promises.upgradeGroupPlan = sinon
  540. .stub()
  541. .resolves()
  542. const res = {
  543. sendStatus: code => {
  544. code.should.equal(200)
  545. done()
  546. },
  547. }
  548. this.Controller.upgradeSubscription(this.req, res)
  549. })
  550. it('should send 500 response', function (done) {
  551. this.SubscriptionGroupHandler.promises.upgradeGroupPlan = sinon
  552. .stub()
  553. .rejects()
  554. const res = {
  555. sendStatus: code => {
  556. code.should.equal(500)
  557. done()
  558. },
  559. }
  560. this.Controller.upgradeSubscription(this.req, res)
  561. })
  562. })
  563. })