SubscriptionGroupControllerTests.mjs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575
  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. getGroupPlanUpgradePreview: sinon
  53. .stub()
  54. .resolves(this.previewSubscriptionChangeData),
  55. },
  56. }
  57. this.SubscriptionLocator = {
  58. promises: {
  59. getSubscription: sinon.stub().resolves(this.subscription),
  60. },
  61. }
  62. this.SessionManager = {
  63. getLoggedInUserId(session) {
  64. return session.user._id
  65. },
  66. getSessionUser(session) {
  67. return session.user
  68. },
  69. }
  70. this.UserAuditLogHandler = {
  71. promises: {
  72. addEntry: sinon.stub().resolves(),
  73. },
  74. }
  75. this.Modules = {
  76. promises: {
  77. hooks: {
  78. fire: sinon.stub().resolves(),
  79. },
  80. },
  81. }
  82. this.SplitTestHandler = {
  83. promises: {
  84. getAssignment: sinon.stub().resolves(),
  85. },
  86. }
  87. this.UserGetter = {
  88. promises: {
  89. getUserEmail: sinon.stub().resolves(this.user.email),
  90. },
  91. }
  92. this.RecurlyClient = {}
  93. this.SubscriptionController = {}
  94. this.SubscriptionModel = { Subscription: {} }
  95. this.Controller = await esmock.strict(modulePath, {
  96. '../../../../app/src/Features/Subscription/SubscriptionGroupHandler':
  97. this.SubscriptionGroupHandler,
  98. '../../../../app/src/Features/Subscription/SubscriptionLocator':
  99. this.SubscriptionLocator,
  100. '../../../../app/src/Features/Authentication/SessionManager':
  101. this.SessionManager,
  102. '../../../../app/src/Features/User/UserAuditLogHandler':
  103. this.UserAuditLogHandler,
  104. '../../../../app/src/infrastructure/Modules': this.Modules,
  105. '../../../../app/src/Features/SplitTests/SplitTestHandler':
  106. this.SplitTestHandler,
  107. '../../../../app/src/Features/User/UserGetter': this.UserGetter,
  108. '../../../../app/src/Features/Errors/ErrorController':
  109. (this.ErrorController = {
  110. notFound: sinon.stub(),
  111. }),
  112. '../../../../app/src/Features/Subscription/SubscriptionController':
  113. this.SubscriptionController,
  114. '../../../../app/src/Features/Subscription/RecurlyClient':
  115. this.RecurlyClient,
  116. '../../../../app/src/models/Subscription': this.SubscriptionModel,
  117. '@overleaf/logger': {
  118. err: sinon.stub(),
  119. error: sinon.stub(),
  120. warn: sinon.stub(),
  121. log: sinon.stub(),
  122. debug: sinon.stub(),
  123. },
  124. })
  125. })
  126. describe('removeUserFromGroup', function () {
  127. it('should use the subscription id for the logged in user and take the user id from the params', function (done) {
  128. const userIdToRemove = '31231'
  129. this.req.params = { user_id: userIdToRemove }
  130. this.req.entity = this.subscription
  131. const res = {
  132. sendStatus: () => {
  133. this.SubscriptionGroupHandler.promises.removeUserFromGroup
  134. .calledWith(this.subscriptionId, userIdToRemove)
  135. .should.equal(true)
  136. done()
  137. },
  138. }
  139. this.Controller.removeUserFromGroup(this.req, res, done)
  140. })
  141. it('should log that the user has been removed', 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. sinon.assert.calledWith(
  148. this.UserAuditLogHandler.promises.addEntry,
  149. userIdToRemove,
  150. 'remove-from-group-subscription',
  151. this.adminUserId,
  152. this.req.ip,
  153. { subscriptionId: this.subscriptionId }
  154. )
  155. done()
  156. },
  157. }
  158. this.Controller.removeUserFromGroup(this.req, res, done)
  159. })
  160. it('should call the group SSO hooks with group SSO enabled', function (done) {
  161. const userIdToRemove = '31231'
  162. this.req.params = { user_id: userIdToRemove }
  163. this.req.entity = this.subscription
  164. this.Modules.promises.hooks.fire
  165. .withArgs('hasGroupSSOEnabled', this.subscription)
  166. .resolves([true])
  167. const res = {
  168. sendStatus: () => {
  169. this.Modules.promises.hooks.fire
  170. .calledWith('hasGroupSSOEnabled', this.subscription)
  171. .should.equal(true)
  172. this.Modules.promises.hooks.fire
  173. .calledWith(
  174. 'unlinkUserFromGroupSSO',
  175. userIdToRemove,
  176. this.subscriptionId
  177. )
  178. .should.equal(true)
  179. sinon.assert.calledTwice(this.Modules.promises.hooks.fire)
  180. done()
  181. },
  182. }
  183. this.Controller.removeUserFromGroup(this.req, res, done)
  184. })
  185. it('should call the group SSO hooks with group SSO disabled', function (done) {
  186. const userIdToRemove = '31231'
  187. this.req.params = { user_id: userIdToRemove }
  188. this.req.entity = this.subscription
  189. this.Modules.promises.hooks.fire
  190. .withArgs('hasGroupSSOEnabled', this.subscription)
  191. .resolves([false])
  192. const res = {
  193. sendStatus: () => {
  194. this.Modules.promises.hooks.fire
  195. .calledWith('hasGroupSSOEnabled', this.subscription)
  196. .should.equal(true)
  197. sinon.assert.calledOnce(this.Modules.promises.hooks.fire)
  198. done()
  199. },
  200. }
  201. this.Controller.removeUserFromGroup(this.req, res, done)
  202. })
  203. })
  204. describe('removeSelfFromGroup', function () {
  205. it('gets subscription and remove user', function (done) {
  206. this.req.query = { subscriptionId: this.subscriptionId }
  207. const memberUserIdToremove = 123456789
  208. this.req.session.user._id = memberUserIdToremove
  209. const res = {
  210. sendStatus: () => {
  211. sinon.assert.calledWith(
  212. this.SubscriptionLocator.promises.getSubscription,
  213. this.subscriptionId
  214. )
  215. sinon.assert.calledWith(
  216. this.SubscriptionGroupHandler.promises.removeUserFromGroup,
  217. this.subscriptionId,
  218. memberUserIdToremove
  219. )
  220. done()
  221. },
  222. }
  223. this.Controller.removeSelfFromGroup(this.req, res, done)
  224. })
  225. it('should log that the user has left the subscription', function (done) {
  226. this.req.query = { subscriptionId: this.subscriptionId }
  227. const memberUserIdToremove = '123456789'
  228. this.req.session.user._id = memberUserIdToremove
  229. const res = {
  230. sendStatus: () => {
  231. sinon.assert.calledWith(
  232. this.UserAuditLogHandler.promises.addEntry,
  233. memberUserIdToremove,
  234. 'remove-from-group-subscription',
  235. memberUserIdToremove,
  236. this.req.ip,
  237. { subscriptionId: this.subscriptionId }
  238. )
  239. done()
  240. },
  241. }
  242. this.Controller.removeSelfFromGroup(this.req, res, done)
  243. })
  244. it('should call the group SSO hooks with group SSO enabled', function (done) {
  245. this.req.query = { subscriptionId: this.subscriptionId }
  246. const memberUserIdToremove = '123456789'
  247. this.req.session.user._id = memberUserIdToremove
  248. this.Modules.promises.hooks.fire
  249. .withArgs('hasGroupSSOEnabled', this.subscription)
  250. .resolves([true])
  251. const res = {
  252. sendStatus: () => {
  253. this.Modules.promises.hooks.fire
  254. .calledWith('hasGroupSSOEnabled', this.subscription)
  255. .should.equal(true)
  256. this.Modules.promises.hooks.fire
  257. .calledWith(
  258. 'unlinkUserFromGroupSSO',
  259. memberUserIdToremove,
  260. this.subscriptionId
  261. )
  262. .should.equal(true)
  263. sinon.assert.calledTwice(this.Modules.promises.hooks.fire)
  264. done()
  265. },
  266. }
  267. this.Controller.removeSelfFromGroup(this.req, res, done)
  268. })
  269. it('should call the group SSO hooks with group SSO disabled', function (done) {
  270. const userIdToRemove = '31231'
  271. this.req.session.user._id = userIdToRemove
  272. this.req.params = { user_id: userIdToRemove }
  273. this.req.entity = this.subscription
  274. this.Modules.promises.hooks.fire
  275. .withArgs('hasGroupSSOEnabled', this.subscription)
  276. .resolves([false])
  277. const res = {
  278. sendStatus: () => {
  279. this.Modules.promises.hooks.fire
  280. .calledWith('hasGroupSSOEnabled', this.subscription)
  281. .should.equal(true)
  282. sinon.assert.calledOnce(this.Modules.promises.hooks.fire)
  283. done()
  284. },
  285. }
  286. this.Controller.removeSelfFromGroup(this.req, res, done)
  287. })
  288. })
  289. describe('addSeatsToGroupSubscription', function () {
  290. it('should render the "add seats" page', function (done) {
  291. const res = {
  292. render: (page, props) => {
  293. this.SubscriptionGroupHandler.promises.getUsersGroupSubscriptionDetails
  294. .calledWith(this.req)
  295. .should.equal(true)
  296. this.SubscriptionGroupHandler.promises.ensureFlexibleLicensingEnabled
  297. .calledWith(this.plan)
  298. .should.equal(true)
  299. page.should.equal('subscriptions/add-seats')
  300. props.subscriptionId.should.equal(this.subscriptionId)
  301. props.groupName.should.equal(this.subscription.teamName)
  302. props.totalLicenses.should.equal(this.subscription.membersLimit)
  303. done()
  304. },
  305. }
  306. this.Controller.addSeatsToGroupSubscription(this.req, res)
  307. })
  308. it('should redirect to subscription page when getting subscription details fails', function (done) {
  309. this.SubscriptionGroupHandler.promises.getUsersGroupSubscriptionDetails =
  310. sinon.stub().rejects()
  311. const res = {
  312. redirect: url => {
  313. url.should.equal('/user/subscription')
  314. done()
  315. },
  316. }
  317. this.Controller.addSeatsToGroupSubscription(this.req, res)
  318. })
  319. it('should redirect to subscription page when flexible licensing is not enabled', function (done) {
  320. this.SubscriptionGroupHandler.promises.ensureFlexibleLicensingEnabled =
  321. sinon.stub().rejects()
  322. const res = {
  323. redirect: url => {
  324. url.should.equal('/user/subscription')
  325. done()
  326. },
  327. }
  328. this.Controller.addSeatsToGroupSubscription(this.req, res)
  329. })
  330. })
  331. describe('previewAddSeatsSubscriptionChange', function () {
  332. it('should preview "add seats" change', function (done) {
  333. const res = {
  334. json: data => {
  335. this.SubscriptionGroupHandler.promises.previewAddSeatsSubscriptionChange
  336. .calledWith(this.req)
  337. .should.equal(true)
  338. data.should.deep.equal(this.previewSubscriptionChangeData)
  339. done()
  340. },
  341. }
  342. this.Controller.previewAddSeatsSubscriptionChange(this.req, res)
  343. })
  344. it('should fail previewing "add seats" change', function (done) {
  345. this.SubscriptionGroupHandler.promises.previewAddSeatsSubscriptionChange =
  346. sinon.stub().rejects()
  347. const res = {
  348. status: statusCode => {
  349. statusCode.should.equal(400)
  350. return {
  351. end: () => {
  352. done()
  353. },
  354. }
  355. },
  356. }
  357. this.Controller.previewAddSeatsSubscriptionChange(this.req, res)
  358. })
  359. })
  360. describe('createAddSeatsSubscriptionChange', function () {
  361. it('should apply "add seats" change', function (done) {
  362. const res = {
  363. json: data => {
  364. this.SubscriptionGroupHandler.promises.createAddSeatsSubscriptionChange
  365. .calledWith(this.req)
  366. .should.equal(true)
  367. data.should.deep.equal(this.createSubscriptionChangeData)
  368. done()
  369. },
  370. }
  371. this.Controller.createAddSeatsSubscriptionChange(this.req, res)
  372. })
  373. it('should fail applying "add seats" change', function (done) {
  374. this.SubscriptionGroupHandler.promises.createAddSeatsSubscriptionChange =
  375. sinon.stub().rejects()
  376. const res = {
  377. status: statusCode => {
  378. statusCode.should.equal(400)
  379. return {
  380. end: () => {
  381. done()
  382. },
  383. }
  384. },
  385. }
  386. this.Controller.createAddSeatsSubscriptionChange(this.req, res)
  387. })
  388. })
  389. describe('submitForm', function () {
  390. it('should build and pass the request body to the sales submit handler', function (done) {
  391. const adding = 100
  392. this.req.body = { adding }
  393. const res = {
  394. sendStatus: code => {
  395. this.Modules.promises.hooks.fire
  396. .calledWith('sendSupportRequest', {
  397. email: this.user.email,
  398. subject: 'Sales Contact Form',
  399. message:
  400. '\n' +
  401. '**Overleaf Sales Contact Form:**\n' +
  402. '\n' +
  403. '**Subject:** Self-Serve Group User Increase Request\n' +
  404. '\n' +
  405. `**Estimated Number of Users:** ${adding}\n` +
  406. '\n' +
  407. `**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.`,
  408. inbox: 'sales',
  409. })
  410. .should.equal(true)
  411. sinon.assert.calledOnce(this.Modules.promises.hooks.fire)
  412. code.should.equal(204)
  413. done()
  414. },
  415. }
  416. this.Controller.submitForm(this.req, res, done)
  417. })
  418. })
  419. describe('flexibleLicensingSplitTest', function () {
  420. it('passes when the variant is "enabled"', function (done) {
  421. const res = sinon.stub()
  422. const next = () => {
  423. this.ErrorController.notFound.notCalled.should.equal(true)
  424. done()
  425. }
  426. this.SplitTestHandler.promises.getAssignment.resolves({
  427. variant: 'enabled',
  428. })
  429. this.Controller.flexibleLicensingSplitTest(this.req, res, next, done)
  430. })
  431. it('returns error page when the variant is "default"', function (done) {
  432. const res = sinon.stub()
  433. const next = sinon.stub()
  434. this.ErrorController.notFound = sinon.stub().callsFake(() => {
  435. next.notCalled.should.equal(true)
  436. done()
  437. })
  438. this.SplitTestHandler.promises.getAssignment.resolves({
  439. variant: 'default',
  440. })
  441. this.Controller.flexibleLicensingSplitTest(this.req, res, next, done)
  442. })
  443. })
  444. describe('subscriptionUpgradePage', function () {
  445. it('should render "subscription upgrade" page', function (done) {
  446. const olSubscription = { membersLimit: 1, teamName: 'test team' }
  447. this.SubscriptionModel.Subscription.findOne = () => {
  448. return {
  449. exec: () => olSubscription,
  450. }
  451. }
  452. const res = {
  453. render: (page, data) => {
  454. this.SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview
  455. .calledWith(this.req.session.user._id)
  456. .should.equal(true)
  457. page.should.equal('subscriptions/upgrade-group-subscription-react')
  458. data.totalLicenses.should.equal(olSubscription.membersLimit)
  459. data.groupName.should.equal(olSubscription.teamName)
  460. data.changePreview.should.equal(this.previewSubscriptionChangeData)
  461. done()
  462. },
  463. }
  464. this.Controller.subscriptionUpgradePage(this.req, res)
  465. })
  466. it('should redirect if failed to generate preview', function (done) {
  467. this.SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview = sinon
  468. .stub()
  469. .rejects()
  470. const res = {
  471. redirect: url => {
  472. url.should.equal('/user/subscription')
  473. done()
  474. },
  475. }
  476. this.Controller.subscriptionUpgradePage(this.req, res)
  477. })
  478. })
  479. describe('upgradeSubscription', function () {
  480. it('should send 200 response', function (done) {
  481. this.SubscriptionGroupHandler.promises.upgradeGroupPlan = sinon
  482. .stub()
  483. .resolves()
  484. const res = {
  485. sendStatus: code => {
  486. code.should.equal(200)
  487. done()
  488. },
  489. }
  490. this.Controller.upgradeSubscription(this.req, res)
  491. })
  492. it('should send 500 response', function (done) {
  493. this.SubscriptionGroupHandler.promises.upgradeGroupPlan = sinon
  494. .stub()
  495. .rejects()
  496. const res = {
  497. sendStatus: code => {
  498. code.should.equal(500)
  499. done()
  500. },
  501. }
  502. this.Controller.upgradeSubscription(this.req, res)
  503. })
  504. })
  505. })