SubscriptionGroupController.test.mjs 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900
  1. import { vi } from 'vitest'
  2. import sinon from 'sinon'
  3. const modulePath =
  4. '../../../../app/src/Features/Subscription/SubscriptionGroupController'
  5. describe('SubscriptionGroupController', function () {
  6. beforeEach(async function (ctx) {
  7. ctx.user = { _id: '!@312431', email: 'user@email.com' }
  8. ctx.adminUserId = '123jlkj'
  9. ctx.subscriptionId = '123434325412'
  10. ctx.user_email = 'bob@gmail.com'
  11. ctx.req = {
  12. session: {
  13. user: {
  14. _id: ctx.adminUserId,
  15. email: ctx.user_email,
  16. },
  17. },
  18. params: {
  19. subscriptionId: ctx.subscriptionId,
  20. },
  21. query: {},
  22. }
  23. ctx.subscription = {
  24. _id: ctx.subscriptionId,
  25. teamName: 'Cool group',
  26. groupPlan: true,
  27. membersLimit: 5,
  28. }
  29. ctx.plan = {
  30. canUseFlexibleLicensing: true,
  31. }
  32. ctx.recurlySubscription = {
  33. get isCollectionMethodManual() {
  34. return true
  35. },
  36. }
  37. ctx.previewSubscriptionChangeData = {
  38. change: {},
  39. currency: 'USD',
  40. }
  41. ctx.createSubscriptionChangeData = { adding: 1 }
  42. ctx.paymentMethod = { cardType: 'Visa', lastFour: '1111' }
  43. ctx.SubscriptionGroupHandler = {
  44. promises: {
  45. removeUserFromGroup: sinon.stub().resolves(),
  46. getUsersGroupSubscriptionDetails: sinon.stub().resolves({
  47. subscription: ctx.subscription,
  48. plan: ctx.plan,
  49. paymentProviderSubscription: ctx.recurlySubscription,
  50. }),
  51. previewAddSeatsSubscriptionChange: sinon
  52. .stub()
  53. .resolves(ctx.previewSubscriptionChangeData),
  54. createAddSeatsSubscriptionChange: sinon
  55. .stub()
  56. .resolves(ctx.createSubscriptionChangeData),
  57. ensureFlexibleLicensingEnabled: sinon.stub().resolves(),
  58. ensureSubscriptionIsActive: sinon.stub().resolves(),
  59. ensureSubscriptionCollectionMethodIsNotManual: sinon.stub().resolves(),
  60. ensureSubscriptionHasNoPendingChanges: sinon.stub().resolves(),
  61. ensureSubscriptionHasNoPastDueInvoice: sinon.stub().resolves(),
  62. getGroupPlanUpgradePreview: sinon
  63. .stub()
  64. .resolves(ctx.previewSubscriptionChangeData),
  65. checkBillingInfoExistence: sinon.stub().resolves(ctx.paymentMethod),
  66. updateSubscriptionPaymentTerms: sinon.stub().resolves(),
  67. ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual:
  68. sinon.stub().resolves(),
  69. },
  70. }
  71. ctx.SubscriptionLocator = {
  72. promises: {
  73. getSubscription: sinon.stub().resolves(ctx.subscription),
  74. },
  75. }
  76. ctx.SessionManager = {
  77. getLoggedInUserId(session) {
  78. return session.user._id
  79. },
  80. getSessionUser(session) {
  81. return session.user
  82. },
  83. }
  84. ctx.UserAuditLogHandler = {
  85. promises: {
  86. addEntry: sinon.stub().resolves(),
  87. },
  88. }
  89. ctx.Modules = {
  90. promises: {
  91. hooks: {
  92. fire: sinon.stub().resolves(),
  93. },
  94. },
  95. }
  96. ctx.UserGetter = {
  97. promises: {
  98. getUserEmail: sinon.stub().resolves(ctx.user.email),
  99. },
  100. }
  101. ctx.paymentMethod = { cardType: 'Visa', lastFour: '1111' }
  102. ctx.RecurlyClient = {
  103. promises: {
  104. getPaymentMethod: sinon.stub().resolves(ctx.paymentMethod),
  105. },
  106. }
  107. ctx.SubscriptionController = {}
  108. ctx.SubscriptionModel = { Subscription: {} }
  109. ctx.PlansHelper = {
  110. isProfessionalGroupPlan: sinon.stub().returns(false),
  111. }
  112. ctx.Errors = {
  113. MissingBillingInfoError: class extends Error {},
  114. ManuallyCollectedError: class extends Error {},
  115. PendingChangeError: class extends Error {},
  116. InactiveError: class extends Error {},
  117. SubtotalLimitExceededError: class extends Error {},
  118. HasPastDueInvoiceError: class extends Error {},
  119. HasNoAdditionalLicenseWhenManuallyCollectedError: class extends Error {},
  120. }
  121. vi.doMock(
  122. '../../../../app/src/Features/Subscription/SubscriptionGroupHandler',
  123. () => ({
  124. default: ctx.SubscriptionGroupHandler,
  125. })
  126. )
  127. vi.doMock(
  128. '../../../../app/src/Features/Subscription/SubscriptionLocator',
  129. () => ({
  130. default: ctx.SubscriptionLocator,
  131. })
  132. )
  133. vi.doMock(
  134. '../../../../app/src/Features/Authentication/SessionManager',
  135. () => ({
  136. default: ctx.SessionManager,
  137. })
  138. )
  139. vi.doMock('../../../../app/src/Features/User/UserAuditLogHandler', () => ({
  140. default: ctx.UserAuditLogHandler,
  141. }))
  142. vi.doMock('../../../../app/src/infrastructure/Modules', () => ({
  143. default: ctx.Modules,
  144. }))
  145. vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({
  146. default: ctx.UserGetter,
  147. }))
  148. vi.doMock('../../../../app/src/Features/Errors/ErrorController', () => ({
  149. default: (ctx.ErrorController = {
  150. notFound: sinon.stub(),
  151. }),
  152. }))
  153. vi.doMock(
  154. '../../../../app/src/Features/Subscription/SubscriptionController',
  155. () => ({
  156. default: ctx.SubscriptionController,
  157. })
  158. )
  159. vi.doMock(
  160. '../../../../app/src/Features/Subscription/RecurlyClient',
  161. () => ({
  162. default: ctx.RecurlyClient,
  163. })
  164. )
  165. vi.doMock(
  166. '../../../../app/src/Features/Subscription/PlansHelper',
  167. () => ctx.PlansHelper
  168. )
  169. vi.doMock(
  170. '../../../../app/src/Features/Subscription/Errors',
  171. () => ctx.Errors
  172. )
  173. vi.doMock(
  174. '../../../../app/src/models/Subscription',
  175. () => ctx.SubscriptionModel
  176. )
  177. vi.doMock('@overleaf/logger', () => ({
  178. default: {
  179. err: sinon.stub(),
  180. error: sinon.stub(),
  181. warn: sinon.stub(),
  182. log: sinon.stub(),
  183. debug: sinon.stub(),
  184. },
  185. }))
  186. ctx.Controller = (await import(modulePath)).default
  187. })
  188. describe('removeUserFromGroup', function () {
  189. it('should use the subscription id for the logged in user and take the user id from the params', async function (ctx) {
  190. await new Promise(resolve => {
  191. const userIdToRemove = '31231'
  192. ctx.req.params = { user_id: userIdToRemove }
  193. ctx.req.entity = ctx.subscription
  194. const res = {
  195. sendStatus: () => {
  196. ctx.SubscriptionGroupHandler.promises.removeUserFromGroup
  197. .calledWith(ctx.subscriptionId, userIdToRemove, {
  198. initiatorId: ctx.req.session.user._id,
  199. ipAddress: ctx.req.ip,
  200. })
  201. .should.equal(true)
  202. resolve()
  203. },
  204. }
  205. ctx.Controller.removeUserFromGroup(ctx.req, res, resolve)
  206. })
  207. })
  208. it('should log that the user has been removed', async function (ctx) {
  209. await new Promise(resolve => {
  210. const userIdToRemove = '31231'
  211. ctx.req.params = { user_id: userIdToRemove }
  212. ctx.req.entity = ctx.subscription
  213. const res = {
  214. sendStatus: () => {
  215. sinon.assert.calledWith(
  216. ctx.UserAuditLogHandler.promises.addEntry,
  217. userIdToRemove,
  218. 'remove-from-group-subscription',
  219. ctx.adminUserId,
  220. ctx.req.ip,
  221. { subscriptionId: ctx.subscriptionId }
  222. )
  223. resolve()
  224. },
  225. }
  226. ctx.Controller.removeUserFromGroup(ctx.req, res, resolve)
  227. })
  228. })
  229. it('should call the group SSO hooks with group SSO enabled', async function (ctx) {
  230. await new Promise(resolve => {
  231. const userIdToRemove = '31231'
  232. ctx.req.params = { user_id: userIdToRemove }
  233. ctx.req.entity = ctx.subscription
  234. ctx.Modules.promises.hooks.fire
  235. .withArgs('hasGroupSSOEnabled', ctx.subscription)
  236. .resolves([true])
  237. const res = {
  238. sendStatus: () => {
  239. ctx.Modules.promises.hooks.fire
  240. .calledWith('hasGroupSSOEnabled', ctx.subscription)
  241. .should.equal(true)
  242. ctx.Modules.promises.hooks.fire
  243. .calledWith(
  244. 'unlinkUserFromGroupSSO',
  245. userIdToRemove,
  246. ctx.subscriptionId
  247. )
  248. .should.equal(true)
  249. sinon.assert.calledTwice(ctx.Modules.promises.hooks.fire)
  250. resolve()
  251. },
  252. }
  253. ctx.Controller.removeUserFromGroup(ctx.req, res, resolve)
  254. })
  255. })
  256. it('should call the group SSO hooks with group SSO disabled', async function (ctx) {
  257. await new Promise(resolve => {
  258. const userIdToRemove = '31231'
  259. ctx.req.params = { user_id: userIdToRemove }
  260. ctx.req.entity = ctx.subscription
  261. ctx.Modules.promises.hooks.fire
  262. .withArgs('hasGroupSSOEnabled', ctx.subscription)
  263. .resolves([false])
  264. const res = {
  265. sendStatus: () => {
  266. ctx.Modules.promises.hooks.fire
  267. .calledWith('hasGroupSSOEnabled', ctx.subscription)
  268. .should.equal(true)
  269. sinon.assert.calledOnce(ctx.Modules.promises.hooks.fire)
  270. resolve()
  271. },
  272. }
  273. ctx.Controller.removeUserFromGroup(ctx.req, res, resolve)
  274. })
  275. })
  276. })
  277. describe('removeSelfFromGroup', function () {
  278. it('gets subscription and remove user', async function (ctx) {
  279. await new Promise(resolve => {
  280. ctx.req.query = { subscriptionId: ctx.subscriptionId }
  281. const memberUserIdToremove = 123456789
  282. ctx.req.session.user._id = memberUserIdToremove
  283. const res = {
  284. sendStatus: () => {
  285. sinon.assert.calledWith(
  286. ctx.SubscriptionLocator.promises.getSubscription,
  287. ctx.subscriptionId
  288. )
  289. sinon.assert.calledWith(
  290. ctx.SubscriptionGroupHandler.promises.removeUserFromGroup,
  291. ctx.subscriptionId,
  292. memberUserIdToremove,
  293. {
  294. initiatorId: ctx.req.session.user._id,
  295. ipAddress: ctx.req.ip,
  296. }
  297. )
  298. resolve()
  299. },
  300. }
  301. ctx.Controller.removeSelfFromGroup(ctx.req, res, resolve)
  302. })
  303. })
  304. it('should log that the user has left the subscription', async function (ctx) {
  305. await new Promise(resolve => {
  306. ctx.req.query = { subscriptionId: ctx.subscriptionId }
  307. const memberUserIdToremove = '123456789'
  308. ctx.req.session.user._id = memberUserIdToremove
  309. const res = {
  310. sendStatus: () => {
  311. sinon.assert.calledWith(
  312. ctx.UserAuditLogHandler.promises.addEntry,
  313. memberUserIdToremove,
  314. 'remove-from-group-subscription',
  315. memberUserIdToremove,
  316. ctx.req.ip,
  317. { subscriptionId: ctx.subscriptionId }
  318. )
  319. resolve()
  320. },
  321. }
  322. ctx.Controller.removeSelfFromGroup(ctx.req, res, resolve)
  323. })
  324. })
  325. it('should call the group SSO hooks with group SSO enabled', async function (ctx) {
  326. await new Promise(resolve => {
  327. ctx.req.query = { subscriptionId: ctx.subscriptionId }
  328. const memberUserIdToremove = '123456789'
  329. ctx.req.session.user._id = memberUserIdToremove
  330. ctx.Modules.promises.hooks.fire
  331. .withArgs('hasGroupSSOEnabled', ctx.subscription)
  332. .resolves([true])
  333. const res = {
  334. sendStatus: () => {
  335. ctx.Modules.promises.hooks.fire
  336. .calledWith('hasGroupSSOEnabled', ctx.subscription)
  337. .should.equal(true)
  338. ctx.Modules.promises.hooks.fire
  339. .calledWith(
  340. 'unlinkUserFromGroupSSO',
  341. memberUserIdToremove,
  342. ctx.subscriptionId
  343. )
  344. .should.equal(true)
  345. sinon.assert.calledTwice(ctx.Modules.promises.hooks.fire)
  346. resolve()
  347. },
  348. }
  349. ctx.Controller.removeSelfFromGroup(ctx.req, res, resolve)
  350. })
  351. })
  352. it('should call the group SSO hooks with group SSO disabled', async function (ctx) {
  353. await new Promise(resolve => {
  354. const userIdToRemove = '31231'
  355. ctx.req.session.user._id = userIdToRemove
  356. ctx.req.params = { user_id: userIdToRemove }
  357. ctx.req.entity = ctx.subscription
  358. ctx.Modules.promises.hooks.fire
  359. .withArgs('hasGroupSSOEnabled', ctx.subscription)
  360. .resolves([false])
  361. const res = {
  362. sendStatus: () => {
  363. ctx.Modules.promises.hooks.fire
  364. .calledWith('hasGroupSSOEnabled', ctx.subscription)
  365. .should.equal(true)
  366. sinon.assert.calledOnce(ctx.Modules.promises.hooks.fire)
  367. resolve()
  368. },
  369. }
  370. ctx.Controller.removeSelfFromGroup(ctx.req, res, resolve)
  371. })
  372. })
  373. })
  374. describe('addSeatsToGroupSubscription', function () {
  375. it('should render the "add seats" page', async function (ctx) {
  376. await new Promise((resolve, reject) => {
  377. const res = {
  378. render: (page, props) => {
  379. ctx.SubscriptionGroupHandler.promises.getUsersGroupSubscriptionDetails
  380. .calledWith(ctx.req.session.user._id)
  381. .should.equal(true)
  382. ctx.SubscriptionGroupHandler.promises.ensureFlexibleLicensingEnabled
  383. .calledWith(ctx.plan)
  384. .should.equal(true)
  385. ctx.SubscriptionGroupHandler.promises.ensureSubscriptionHasNoPendingChanges
  386. .calledWith(ctx.recurlySubscription)
  387. .should.equal(true)
  388. ctx.SubscriptionGroupHandler.promises.ensureSubscriptionIsActive
  389. .calledWith(ctx.subscription)
  390. .should.equal(true)
  391. ctx.SubscriptionGroupHandler.promises.ensureSubscriptionHasNoPastDueInvoice
  392. .calledWith(ctx.subscription)
  393. .should.equal(true)
  394. ctx.SubscriptionGroupHandler.promises.checkBillingInfoExistence
  395. .calledWith(ctx.recurlySubscription, ctx.adminUserId)
  396. .should.equal(true)
  397. ctx.SubscriptionGroupHandler.promises.ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual
  398. .calledWith(ctx.recurlySubscription)
  399. .should.equal(true)
  400. page.should.equal('subscriptions/add-seats')
  401. props.subscriptionId.should.equal(ctx.subscriptionId)
  402. props.groupName.should.equal(ctx.subscription.teamName)
  403. props.totalLicenses.should.equal(ctx.subscription.membersLimit)
  404. props.isProfessional.should.equal(false)
  405. props.isCollectionMethodManual.should.equal(true)
  406. resolve()
  407. },
  408. }
  409. ctx.Controller.addSeatsToGroupSubscription(ctx.req, res)
  410. })
  411. })
  412. it('should redirect to subscription page when getting subscription details fails', async function (ctx) {
  413. await new Promise(resolve => {
  414. ctx.SubscriptionGroupHandler.promises.getUsersGroupSubscriptionDetails =
  415. sinon.stub().rejects()
  416. const res = {
  417. redirect: url => {
  418. url.should.equal('/user/subscription')
  419. resolve()
  420. },
  421. }
  422. ctx.Controller.addSeatsToGroupSubscription(ctx.req, res)
  423. })
  424. })
  425. it('should redirect to subscription page when flexible licensing is not enabled', async function (ctx) {
  426. await new Promise(resolve => {
  427. ctx.SubscriptionGroupHandler.promises.ensureFlexibleLicensingEnabled =
  428. sinon.stub().rejects()
  429. const res = {
  430. redirect: url => {
  431. url.should.equal('/user/subscription')
  432. resolve()
  433. },
  434. }
  435. ctx.Controller.addSeatsToGroupSubscription(ctx.req, res)
  436. })
  437. })
  438. it('should redirect to missing billing information page when billing information is missing', async function (ctx) {
  439. await new Promise(resolve => {
  440. ctx.SubscriptionGroupHandler.promises.checkBillingInfoExistence = sinon
  441. .stub()
  442. .throws(new ctx.Errors.MissingBillingInfoError())
  443. const res = {
  444. redirect: url => {
  445. url.should.equal(
  446. '/user/subscription/group/missing-billing-information'
  447. )
  448. resolve()
  449. },
  450. }
  451. ctx.Controller.addSeatsToGroupSubscription(ctx.req, res)
  452. })
  453. })
  454. it('should redirect to manually collected subscription error page when collection method is manual and has no additional license add-on', async function (ctx) {
  455. await new Promise(resolve => {
  456. ctx.SubscriptionGroupHandler.promises.ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual =
  457. sinon
  458. .stub()
  459. .throws(
  460. new ctx.Errors.HasNoAdditionalLicenseWhenManuallyCollectedError()
  461. )
  462. const res = {
  463. redirect: url => {
  464. url.should.equal(
  465. '/user/subscription/group/manually-collected-subscription'
  466. )
  467. resolve()
  468. },
  469. }
  470. ctx.Controller.addSeatsToGroupSubscription(ctx.req, res)
  471. })
  472. })
  473. it('should redirect to subscription page when there is a pending change', async function (ctx) {
  474. await new Promise(resolve => {
  475. ctx.SubscriptionGroupHandler.promises.ensureSubscriptionHasNoPendingChanges =
  476. sinon.stub().throws(new ctx.Errors.PendingChangeError())
  477. const res = {
  478. redirect: url => {
  479. url.should.equal('/user/subscription')
  480. resolve()
  481. },
  482. }
  483. ctx.Controller.addSeatsToGroupSubscription(ctx.req, res)
  484. })
  485. })
  486. it('should redirect to subscription page when subscription is not active', async function (ctx) {
  487. await new Promise(resolve => {
  488. ctx.SubscriptionGroupHandler.promises.ensureSubscriptionIsActive = sinon
  489. .stub()
  490. .rejects()
  491. const res = {
  492. redirect: url => {
  493. url.should.equal('/user/subscription')
  494. resolve()
  495. },
  496. }
  497. ctx.Controller.addSeatsToGroupSubscription(ctx.req, res)
  498. })
  499. })
  500. it('should redirect to subscription page when subscription has pending invoice', async function (ctx) {
  501. ctx.SubscriptionGroupHandler.promises.ensureSubscriptionHasNoPastDueInvoice =
  502. sinon.stub().rejects()
  503. await new Promise(resolve => {
  504. const res = {
  505. redirect: url => {
  506. url.should.equal('/user/subscription')
  507. resolve()
  508. },
  509. }
  510. ctx.Controller.addSeatsToGroupSubscription(ctx.req, res)
  511. })
  512. })
  513. })
  514. describe('previewAddSeatsSubscriptionChange', function () {
  515. it('should preview "add seats" change', async function (ctx) {
  516. await new Promise(resolve => {
  517. ctx.req.body = { adding: 2 }
  518. const res = {
  519. json: data => {
  520. ctx.SubscriptionGroupHandler.promises.previewAddSeatsSubscriptionChange
  521. .calledWith(ctx.req.session.user._id, ctx.req.body.adding)
  522. .should.equal(true)
  523. data.should.deep.equal(ctx.previewSubscriptionChangeData)
  524. resolve()
  525. },
  526. }
  527. ctx.Controller.previewAddSeatsSubscriptionChange(ctx.req, res)
  528. })
  529. })
  530. it('should fail previewing "add seats" change', async function (ctx) {
  531. await new Promise(resolve => {
  532. ctx.SubscriptionGroupHandler.promises.previewAddSeatsSubscriptionChange =
  533. sinon.stub().rejects()
  534. const res = {
  535. status: statusCode => {
  536. statusCode.should.equal(500)
  537. return {
  538. end: () => {
  539. resolve()
  540. },
  541. }
  542. },
  543. }
  544. ctx.Controller.previewAddSeatsSubscriptionChange(ctx.req, res)
  545. })
  546. })
  547. it('should fail previewing "add seats" change with SubtotalLimitExceededError', async function (ctx) {
  548. await new Promise(resolve => {
  549. ctx.req.body = { adding: 2 }
  550. ctx.SubscriptionGroupHandler.promises.previewAddSeatsSubscriptionChange =
  551. sinon.stub().throws(new ctx.Errors.SubtotalLimitExceededError())
  552. const res = {
  553. status: statusCode => {
  554. statusCode.should.equal(422)
  555. return {
  556. json: data => {
  557. data.should.deep.equal({
  558. code: 'subtotal_limit_exceeded',
  559. adding: ctx.req.body.adding,
  560. })
  561. resolve()
  562. },
  563. }
  564. },
  565. }
  566. ctx.Controller.previewAddSeatsSubscriptionChange(ctx.req, res)
  567. })
  568. })
  569. })
  570. describe('createAddSeatsSubscriptionChange', function () {
  571. it('should apply "add seats" change', async function (ctx) {
  572. await new Promise(resolve => {
  573. ctx.req.body = { adding: 2 }
  574. const res = {
  575. json: data => {
  576. ctx.SubscriptionGroupHandler.promises.createAddSeatsSubscriptionChange
  577. .calledWith(ctx.req.session.user._id, ctx.req.body.adding)
  578. .should.equal(true)
  579. data.should.deep.equal(ctx.createSubscriptionChangeData)
  580. resolve()
  581. },
  582. }
  583. ctx.Controller.createAddSeatsSubscriptionChange(ctx.req, res)
  584. })
  585. })
  586. it('should fail applying "add seats" change', async function (ctx) {
  587. await new Promise(resolve => {
  588. ctx.SubscriptionGroupHandler.promises.createAddSeatsSubscriptionChange =
  589. sinon.stub().rejects()
  590. const res = {
  591. status: statusCode => {
  592. statusCode.should.equal(500)
  593. return {
  594. end: () => {
  595. resolve()
  596. },
  597. }
  598. },
  599. }
  600. ctx.Controller.createAddSeatsSubscriptionChange(ctx.req, res)
  601. })
  602. })
  603. it('should fail applying "add seats" change with SubtotalLimitExceededError', async function (ctx) {
  604. await new Promise(resolve => {
  605. ctx.req.body = { adding: 2 }
  606. ctx.SubscriptionGroupHandler.promises.createAddSeatsSubscriptionChange =
  607. sinon.stub().throws(new ctx.Errors.SubtotalLimitExceededError())
  608. const res = {
  609. status: statusCode => {
  610. statusCode.should.equal(422)
  611. return {
  612. json: data => {
  613. data.should.deep.equal({
  614. code: 'subtotal_limit_exceeded',
  615. adding: ctx.req.body.adding,
  616. })
  617. resolve()
  618. },
  619. }
  620. },
  621. }
  622. ctx.Controller.createAddSeatsSubscriptionChange(ctx.req, res)
  623. })
  624. })
  625. })
  626. describe('submitForm', function () {
  627. it('should build and pass the request body to the sales submit handler', async function (ctx) {
  628. await new Promise(resolve => {
  629. const adding = 100
  630. const poNumber = 'PO123456'
  631. ctx.req.body = { adding, poNumber }
  632. const res = {
  633. sendStatus: code => {
  634. ctx.SubscriptionGroupHandler.promises.updateSubscriptionPaymentTerms(
  635. ctx.adminUserId,
  636. ctx.recurlySubscription,
  637. poNumber
  638. )
  639. ctx.Modules.promises.hooks.fire
  640. .calledWith('sendSupportRequest', {
  641. email: ctx.user.email,
  642. subject: 'Sales Contact Form',
  643. message:
  644. '\n' +
  645. '**Overleaf Sales Contact Form:**\n' +
  646. '\n' +
  647. '**Subject:** Self-Serve Group User Increase Request\n' +
  648. '\n' +
  649. `**Estimated Number of Users:** ${adding}\n` +
  650. '\n' +
  651. `**PO Number:** ${poNumber}\n` +
  652. '\n' +
  653. `**Message:** This email has been generated on behalf of user with email **${ctx.user.email}** to request an increase in the total number of users for their subscription.`,
  654. inbox: 'sales',
  655. })
  656. .should.equal(true)
  657. sinon.assert.calledOnce(ctx.Modules.promises.hooks.fire)
  658. code.should.equal(204)
  659. resolve()
  660. },
  661. }
  662. ctx.Controller.submitForm(ctx.req, res, resolve)
  663. })
  664. })
  665. })
  666. describe('subscriptionUpgradePage', function () {
  667. it('should render "subscription upgrade" page', async function (ctx) {
  668. await new Promise(resolve => {
  669. const olSubscription = { membersLimit: 1, teamName: 'test team' }
  670. ctx.SubscriptionModel.Subscription.findOne = () => {
  671. return {
  672. exec: () => olSubscription,
  673. }
  674. }
  675. const res = {
  676. render: (page, data) => {
  677. ctx.SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview
  678. .calledWith(ctx.req.session.user._id)
  679. .should.equal(true)
  680. page.should.equal('subscriptions/upgrade-group-subscription-react')
  681. data.totalLicenses.should.equal(olSubscription.membersLimit)
  682. data.groupName.should.equal(olSubscription.teamName)
  683. data.changePreview.should.equal(ctx.previewSubscriptionChangeData)
  684. resolve()
  685. },
  686. }
  687. ctx.Controller.subscriptionUpgradePage(ctx.req, res)
  688. })
  689. })
  690. it('should redirect if failed to generate preview', async function (ctx) {
  691. await new Promise(resolve => {
  692. ctx.SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview = sinon
  693. .stub()
  694. .rejects()
  695. const res = {
  696. redirect: url => {
  697. url.should.equal('/user/subscription')
  698. resolve()
  699. },
  700. }
  701. ctx.Controller.subscriptionUpgradePage(ctx.req, res)
  702. })
  703. })
  704. it('should redirect to missing billing information page when billing information is missing', async function (ctx) {
  705. await new Promise(resolve => {
  706. ctx.SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview = sinon
  707. .stub()
  708. .throws(new ctx.Errors.MissingBillingInfoError())
  709. const res = {
  710. redirect: url => {
  711. url.should.equal(
  712. '/user/subscription/group/missing-billing-information'
  713. )
  714. resolve()
  715. },
  716. }
  717. ctx.Controller.subscriptionUpgradePage(ctx.req, res)
  718. })
  719. })
  720. it('should redirect to manually collected subscription error page when collection method is manual', async function (ctx) {
  721. await new Promise(resolve => {
  722. ctx.SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview = sinon
  723. .stub()
  724. .throws(new ctx.Errors.ManuallyCollectedError())
  725. const res = {
  726. redirect: url => {
  727. url.should.equal(
  728. '/user/subscription/group/manually-collected-subscription'
  729. )
  730. resolve()
  731. },
  732. }
  733. ctx.Controller.subscriptionUpgradePage(ctx.req, res)
  734. })
  735. })
  736. it('should redirect to subtotal limit exceeded page', async function (ctx) {
  737. await new Promise(resolve => {
  738. ctx.SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview = sinon
  739. .stub()
  740. .throws(new ctx.Errors.SubtotalLimitExceededError())
  741. const res = {
  742. redirect: url => {
  743. url.should.equal('/user/subscription/group/subtotal-limit-exceeded')
  744. resolve()
  745. },
  746. }
  747. ctx.Controller.subscriptionUpgradePage(ctx.req, res)
  748. })
  749. })
  750. })
  751. describe('upgradeSubscription', function () {
  752. it('should send 200 response', async function (ctx) {
  753. await new Promise(resolve => {
  754. ctx.SubscriptionGroupHandler.promises.upgradeGroupPlan = sinon
  755. .stub()
  756. .resolves()
  757. const res = {
  758. sendStatus: code => {
  759. code.should.equal(200)
  760. resolve()
  761. },
  762. }
  763. ctx.Controller.upgradeSubscription(ctx.req, res)
  764. })
  765. })
  766. it('should send 500 response', async function (ctx) {
  767. await new Promise(resolve => {
  768. ctx.SubscriptionGroupHandler.promises.upgradeGroupPlan = sinon
  769. .stub()
  770. .rejects()
  771. const res = {
  772. sendStatus: code => {
  773. code.should.equal(500)
  774. resolve()
  775. },
  776. }
  777. ctx.Controller.upgradeSubscription(ctx.req, res)
  778. })
  779. })
  780. })
  781. })