SubscriptionGroupController.test.mjs 27 KB

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