SubscriptionControllerTests.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649
  1. /* eslint-disable
  2. max-len,
  3. mocha/handle-done-callback,
  4. no-return-assign,
  5. no-unused-vars,
  6. */
  7. // TODO: This file was created by bulk-decaffeinate.
  8. // Fix any style issues and re-enable lint.
  9. /*
  10. * decaffeinate suggestions:
  11. * DS102: Remove unnecessary code created because of implicit returns
  12. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  13. */
  14. const SandboxedModule = require('sandboxed-module')
  15. const sinon = require('sinon')
  16. const should = require('chai').should()
  17. const { expect } = require('chai')
  18. const MockRequest = require('../helpers/MockRequest')
  19. const MockResponse = require('../helpers/MockResponse')
  20. const modulePath =
  21. '../../../../app/src/Features/Subscription/SubscriptionController'
  22. const mockSubscriptions = {
  23. 'subscription-123-active': {
  24. uuid: 'subscription-123-active',
  25. plan: {
  26. name: 'Gold',
  27. plan_code: 'gold'
  28. },
  29. current_period_ends_at: new Date(),
  30. state: 'active',
  31. unit_amount_in_cents: 999,
  32. account: {
  33. account_code: 'user-123'
  34. }
  35. }
  36. }
  37. describe('SubscriptionController', function() {
  38. beforeEach(function() {
  39. this.user = {
  40. email: 'tom@yahoo.com',
  41. _id: 'one',
  42. signUpDate: new Date('2000-10-01')
  43. }
  44. this.activeRecurlySubscription =
  45. mockSubscriptions['subscription-123-active']
  46. this.AuthenticationController = {
  47. getLoggedInUser: sinon.stub().callsArgWith(1, null, this.user),
  48. getLoggedInUserId: sinon.stub().returns(this.user._id),
  49. getSessionUser: sinon.stub().returns(this.user),
  50. isUserLoggedIn: sinon.stub().returns(true)
  51. }
  52. this.SubscriptionHandler = {
  53. createSubscription: sinon.stub().callsArgWith(3),
  54. updateSubscription: sinon.stub().callsArgWith(3),
  55. reactivateSubscription: sinon.stub().callsArgWith(1),
  56. cancelSubscription: sinon.stub().callsArgWith(1),
  57. recurlyCallback: sinon.stub().callsArgWith(1),
  58. startFreeTrial: sinon.stub()
  59. }
  60. this.PlansLocator = { findLocalPlanInSettings: sinon.stub() }
  61. this.LimitationsManager = {
  62. hasPaidSubscription: sinon.stub(),
  63. userHasV1OrV2Subscription: sinon.stub(),
  64. userHasV2Subscription: sinon.stub()
  65. }
  66. this.SubscriptionViewModelBuilder = {
  67. buildUsersSubscriptionViewModel: sinon.stub().callsArgWith(1, null, {}),
  68. buildViewModel: sinon.stub()
  69. }
  70. this.settings = {
  71. coupon_codes: {
  72. upgradeToAnnualPromo: {
  73. student: 'STUDENTCODEHERE',
  74. collaborator: 'COLLABORATORCODEHERE'
  75. }
  76. },
  77. apis: {
  78. recurly: {
  79. subdomain: 'sl'
  80. }
  81. },
  82. siteUrl: 'http://de.sharelatex.dev:3000',
  83. gaExperiments: {}
  84. }
  85. this.GeoIpLookup = { getCurrencyCode: sinon.stub() }
  86. this.UserGetter = { getUser: sinon.stub().callsArgWith(2, null, this.user) }
  87. this.SubscriptionController = SandboxedModule.require(modulePath, {
  88. globals: {
  89. console: console
  90. },
  91. requires: {
  92. '../Authentication/AuthenticationController': this
  93. .AuthenticationController,
  94. './SubscriptionHandler': this.SubscriptionHandler,
  95. './PlansLocator': this.PlansLocator,
  96. './SubscriptionViewModelBuilder': this.SubscriptionViewModelBuilder,
  97. './LimitationsManager': this.LimitationsManager,
  98. '../../infrastructure/GeoIpLookup': this.GeoIpLookup,
  99. 'logger-sharelatex': {
  100. log() {},
  101. warn() {}
  102. },
  103. 'settings-sharelatex': this.settings,
  104. '../User/UserGetter': this.UserGetter,
  105. './RecurlyWrapper': (this.RecurlyWrapper = {}),
  106. './FeaturesUpdater': (this.FeaturesUpdater = {}),
  107. './GroupPlansData': (this.GroupPlansData = {}),
  108. './V1SubscriptionManager': (this.V1SubscriptionManager = {})
  109. }
  110. })
  111. this.res = new MockResponse()
  112. this.req = new MockRequest()
  113. this.req.body = {}
  114. this.req.query = { planCode: '123123' }
  115. return (this.stubbedCurrencyCode = 'GBP')
  116. })
  117. describe('plansPage', function() {
  118. beforeEach(function() {
  119. this.req.ip = '1234.3123.3131.333 313.133.445.666 653.5345.5345.534'
  120. return this.GeoIpLookup.getCurrencyCode.callsArgWith(
  121. 1,
  122. null,
  123. this.stubbedCurrencyCode
  124. )
  125. })
  126. describe('when user is logged in', function(done) {
  127. beforeEach(function(done) {
  128. this.res.callback = done
  129. return this.SubscriptionController.plansPage(this.req, this.res)
  130. })
  131. it('should fetch the current user', function(done) {
  132. this.UserGetter.getUser.callCount.should.equal(1)
  133. return done()
  134. })
  135. describe('not dependant on logged in state', function(done) {
  136. // these could have been put in 'when user is not logged in' too
  137. it('should set the recommended currency from the geoiplookup', function(done) {
  138. this.res.renderedVariables.recomendedCurrency.should.equal(
  139. this.stubbedCurrencyCode
  140. )
  141. this.GeoIpLookup.getCurrencyCode
  142. .calledWith(this.req.ip)
  143. .should.equal(true)
  144. return done()
  145. })
  146. it('should include data for features table', function(done) {
  147. this.res.renderedVariables.planFeatures.length.should.not.equal(0)
  148. return done()
  149. })
  150. })
  151. })
  152. describe('when user is not logged in', function(done) {
  153. beforeEach(function(done) {
  154. this.res.callback = done
  155. this.AuthenticationController.getLoggedInUserId = sinon
  156. .stub()
  157. .returns(null)
  158. return this.SubscriptionController.plansPage(this.req, this.res)
  159. })
  160. it('should not fetch the current user', function(done) {
  161. this.UserGetter.getUser.callCount.should.equal(0)
  162. return done()
  163. })
  164. })
  165. })
  166. describe('paymentPage', function() {
  167. beforeEach(function() {
  168. this.req.headers = {}
  169. this.SubscriptionHandler.validateNoSubscriptionInRecurly = sinon
  170. .stub()
  171. .yields(null, true)
  172. return this.GeoIpLookup.getCurrencyCode.callsArgWith(
  173. 1,
  174. null,
  175. this.stubbedCurrencyCode
  176. )
  177. })
  178. describe('with a user without a subscription', function() {
  179. beforeEach(function() {
  180. this.LimitationsManager.userHasV1OrV2Subscription.callsArgWith(
  181. 1,
  182. null,
  183. false
  184. )
  185. return this.PlansLocator.findLocalPlanInSettings.returns({})
  186. })
  187. describe('with a valid plan code', function() {
  188. it('should render the new subscription page', function(done) {
  189. this.res.render = (page, opts) => {
  190. page.should.equal('subscriptions/new')
  191. return done()
  192. }
  193. return this.SubscriptionController.paymentPage(this.req, this.res)
  194. })
  195. })
  196. })
  197. describe('with a user with subscription', function() {
  198. it('should redirect to the subscription dashboard', function(done) {
  199. this.LimitationsManager.userHasV1OrV2Subscription.callsArgWith(
  200. 1,
  201. null,
  202. true
  203. )
  204. this.res.redirect = url => {
  205. url.should.equal('/user/subscription?hasSubscription=true')
  206. return done()
  207. }
  208. return this.SubscriptionController.paymentPage(this.req, this.res)
  209. })
  210. })
  211. describe('with an invalid plan code', function() {
  212. it('should redirect to the subscription dashboard', function(done) {
  213. this.LimitationsManager.userHasV1OrV2Subscription.callsArgWith(
  214. 1,
  215. null,
  216. false
  217. )
  218. this.PlansLocator.findLocalPlanInSettings.returns(null)
  219. this.res.redirect = url => {
  220. url.should.equal('/user/subscription?hasSubscription=true')
  221. return done()
  222. }
  223. return this.SubscriptionController.paymentPage(this.req, this.res)
  224. })
  225. })
  226. describe('which currency to use', function() {
  227. beforeEach(function() {
  228. this.LimitationsManager.userHasV1OrV2Subscription.callsArgWith(
  229. 1,
  230. null,
  231. false
  232. )
  233. return this.PlansLocator.findLocalPlanInSettings.returns({})
  234. })
  235. it('should use the set currency from the query string', function(done) {
  236. this.req.query.currency = 'EUR'
  237. this.res.render = (page, opts) => {
  238. opts.currency.should.equal('EUR')
  239. opts.currency.should.not.equal(this.stubbedCurrencyCode)
  240. return done()
  241. }
  242. return this.SubscriptionController.paymentPage(this.req, this.res)
  243. })
  244. it('should upercase the currency code', function(done) {
  245. this.req.query.currency = 'eur'
  246. this.res.render = (page, opts) => {
  247. opts.currency.should.equal('EUR')
  248. return done()
  249. }
  250. return this.SubscriptionController.paymentPage(this.req, this.res)
  251. })
  252. it('should use the geo ip currency if non is provided', function(done) {
  253. this.req.query.currency = null
  254. this.res.render = (page, opts) => {
  255. opts.currency.should.equal(this.stubbedCurrencyCode)
  256. return done()
  257. }
  258. return this.SubscriptionController.paymentPage(this.req, this.res)
  259. })
  260. })
  261. describe('with a recurly subscription already', function() {
  262. it('should redirect to the subscription dashboard', function(done) {
  263. this.LimitationsManager.userHasV1OrV2Subscription.callsArgWith(
  264. 1,
  265. null,
  266. false
  267. )
  268. this.SubscriptionHandler.validateNoSubscriptionInRecurly = sinon
  269. .stub()
  270. .yields(null, false)
  271. this.res.redirect = url => {
  272. url.should.equal('/user/subscription?hasSubscription=true')
  273. return done()
  274. }
  275. return this.SubscriptionController.paymentPage(this.req, this.res)
  276. })
  277. })
  278. })
  279. describe('successful_subscription', function() {
  280. beforeEach(function(done) {
  281. this.SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel.callsArgWith(
  282. 1,
  283. null,
  284. {}
  285. )
  286. this.res.callback = done
  287. return this.SubscriptionController.successful_subscription(
  288. this.req,
  289. this.res
  290. )
  291. })
  292. })
  293. describe('userSubscriptionPage', function() {
  294. beforeEach(function(done) {
  295. this.SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel.callsArgWith(
  296. 1,
  297. null,
  298. {
  299. personalSubscription: (this.personalSubscription = {
  300. 'personal-subscription': 'mock'
  301. }),
  302. memberGroupSubscriptions: (this.memberGroupSubscriptions = {
  303. 'group-subscriptions': 'mock'
  304. })
  305. }
  306. )
  307. this.SubscriptionViewModelBuilder.buildViewModel.returns(
  308. (this.plans = { plans: 'mock' })
  309. )
  310. this.LimitationsManager.userHasV1OrV2Subscription.callsArgWith(
  311. 1,
  312. null,
  313. false
  314. )
  315. this.res.render = (view, data) => {
  316. this.data = data
  317. expect(view).to.equal('subscriptions/dashboard')
  318. return done()
  319. }
  320. return this.SubscriptionController.userSubscriptionPage(
  321. this.req,
  322. this.res
  323. )
  324. })
  325. it('should load the personal, groups and v1 subscriptions', function() {
  326. expect(this.data.personalSubscription).to.deep.equal(
  327. this.personalSubscription
  328. )
  329. return expect(this.data.memberGroupSubscriptions).to.deep.equal(
  330. this.memberGroupSubscriptions
  331. )
  332. })
  333. it('should load the user', function() {
  334. return expect(this.data.user).to.deep.equal(this.user)
  335. })
  336. it('should load the plans', function() {
  337. return expect(this.data.plans).to.deep.equal(this.plans)
  338. })
  339. })
  340. describe('createSubscription', function() {
  341. beforeEach(function(done) {
  342. this.res = {
  343. sendStatus() {
  344. return done()
  345. }
  346. }
  347. sinon.spy(this.res, 'sendStatus')
  348. this.subscriptionDetails = {
  349. card: '1234',
  350. cvv: '123'
  351. }
  352. this.req.body.recurly_token_id = '1234'
  353. this.req.body.subscriptionDetails = this.subscriptionDetails
  354. this.LimitationsManager.userHasV1OrV2Subscription.yields(null, false)
  355. return this.SubscriptionController.createSubscription(this.req, this.res)
  356. })
  357. it('should send the user and subscriptionId to the handler', function(done) {
  358. this.SubscriptionHandler.createSubscription
  359. .calledWith(
  360. this.user,
  361. this.subscriptionDetails,
  362. this.req.body.recurly_token_id
  363. )
  364. .should.equal(true)
  365. return done()
  366. })
  367. it('should redurect to the subscription page', function(done) {
  368. this.res.sendStatus.calledWith(201).should.equal(true)
  369. return done()
  370. })
  371. })
  372. describe('updateSubscription via post', function() {
  373. beforeEach(function(done) {
  374. this.res = {
  375. redirect() {
  376. return done()
  377. }
  378. }
  379. sinon.spy(this.res, 'redirect')
  380. this.plan_code = '1234'
  381. this.req.body.plan_code = this.plan_code
  382. return this.SubscriptionController.updateSubscription(this.req, this.res)
  383. })
  384. it('should send the user and subscriptionId to the handler', function(done) {
  385. this.SubscriptionHandler.updateSubscription
  386. .calledWith(this.user, this.plan_code)
  387. .should.equal(true)
  388. return done()
  389. })
  390. it('should redurect to the subscription page', function(done) {
  391. this.res.redirect.calledWith('/user/subscription').should.equal(true)
  392. return done()
  393. })
  394. })
  395. describe('reactivateSubscription', function() {
  396. beforeEach(function(done) {
  397. this.res = {
  398. redirect() {
  399. return done()
  400. }
  401. }
  402. sinon.spy(this.res, 'redirect')
  403. return this.SubscriptionController.reactivateSubscription(
  404. this.req,
  405. this.res
  406. )
  407. })
  408. it('should tell the handler to reactivate this user', function(done) {
  409. this.SubscriptionHandler.reactivateSubscription
  410. .calledWith(this.user)
  411. .should.equal(true)
  412. return done()
  413. })
  414. it('should redurect to the subscription page', function(done) {
  415. this.res.redirect.calledWith('/user/subscription').should.equal(true)
  416. return done()
  417. })
  418. })
  419. describe('cancelSubscription', function() {
  420. beforeEach(function(done) {
  421. this.res = {
  422. redirect() {
  423. return done()
  424. }
  425. }
  426. sinon.spy(this.res, 'redirect')
  427. return this.SubscriptionController.cancelSubscription(this.req, this.res)
  428. })
  429. it('should tell the handler to cancel this user', function(done) {
  430. this.SubscriptionHandler.cancelSubscription
  431. .calledWith(this.user)
  432. .should.equal(true)
  433. return done()
  434. })
  435. it('should redurect to the subscription page', function(done) {
  436. this.res.redirect
  437. .calledWith('/user/subscription/canceled')
  438. .should.equal(true)
  439. return done()
  440. })
  441. })
  442. describe('recurly callback', function() {
  443. describe('with a actionable request', function() {
  444. beforeEach(function(done) {
  445. this.req = {
  446. body: {
  447. expired_subscription_notification: {
  448. subscription: {
  449. uuid: this.activeRecurlySubscription.uuid
  450. }
  451. }
  452. }
  453. }
  454. this.res = {
  455. sendStatus() {
  456. return done()
  457. }
  458. }
  459. sinon.spy(this.res, 'sendStatus')
  460. return this.SubscriptionController.recurlyCallback(this.req, this.res)
  461. })
  462. it('should tell the SubscriptionHandler to process the recurly callback', function(done) {
  463. this.SubscriptionHandler.recurlyCallback.called.should.equal(true)
  464. return done()
  465. })
  466. it('should send a 200', function(done) {
  467. this.res.sendStatus.calledWith(200)
  468. return done()
  469. })
  470. })
  471. describe('with a non-actionable request', function() {
  472. beforeEach(function(done) {
  473. this.user.id = this.activeRecurlySubscription.account.account_code
  474. this.req = {
  475. body: {
  476. new_subscription_notification: {
  477. subscription: {
  478. uuid: this.activeRecurlySubscription.uuid
  479. }
  480. }
  481. }
  482. }
  483. this.res = {
  484. sendStatus() {
  485. return done()
  486. }
  487. }
  488. sinon.spy(this.res, 'sendStatus')
  489. return this.SubscriptionController.recurlyCallback(this.req, this.res)
  490. })
  491. it('should not call the subscriptionshandler', function() {
  492. return this.SubscriptionHandler.recurlyCallback.called.should.equal(
  493. false
  494. )
  495. })
  496. it('should respond with a 200 status', function() {
  497. return this.res.sendStatus.calledWith(200)
  498. })
  499. })
  500. })
  501. describe('renderUpgradeToAnnualPlanPage', function() {
  502. it('should redirect to the plans page if the user does not have a subscription', function(done) {
  503. this.LimitationsManager.userHasV2Subscription.callsArgWith(1, null, false)
  504. this.res.redirect = function(url) {
  505. url.should.equal('/user/subscription/plans')
  506. return done()
  507. }
  508. return this.SubscriptionController.renderUpgradeToAnnualPlanPage(
  509. this.req,
  510. this.res
  511. )
  512. })
  513. it('should pass the plan code to the view - student', function(done) {
  514. this.LimitationsManager.userHasV2Subscription.callsArgWith(
  515. 1,
  516. null,
  517. true,
  518. { planCode: 'Student free trial 14 days' }
  519. )
  520. this.res.render = function(view, opts) {
  521. view.should.equal('subscriptions/upgradeToAnnual')
  522. opts.planName.should.equal('student')
  523. return done()
  524. }
  525. return this.SubscriptionController.renderUpgradeToAnnualPlanPage(
  526. this.req,
  527. this.res
  528. )
  529. })
  530. it('should pass the plan code to the view - collaborator', function(done) {
  531. this.LimitationsManager.userHasV2Subscription.callsArgWith(
  532. 1,
  533. null,
  534. true,
  535. { planCode: 'free trial for Collaborator free trial 14 days' }
  536. )
  537. this.res.render = function(view, opts) {
  538. opts.planName.should.equal('collaborator')
  539. return done()
  540. }
  541. return this.SubscriptionController.renderUpgradeToAnnualPlanPage(
  542. this.req,
  543. this.res
  544. )
  545. })
  546. it('should pass annual as the plan name if the user is already on an annual plan', function(done) {
  547. this.LimitationsManager.userHasV2Subscription.callsArgWith(
  548. 1,
  549. null,
  550. true,
  551. { planCode: 'student annual with free trial' }
  552. )
  553. this.res.render = function(view, opts) {
  554. opts.planName.should.equal('annual')
  555. return done()
  556. }
  557. return this.SubscriptionController.renderUpgradeToAnnualPlanPage(
  558. this.req,
  559. this.res
  560. )
  561. })
  562. })
  563. describe('processUpgradeToAnnualPlan', function() {
  564. beforeEach(function() {})
  565. it('should tell the subscription handler to update the subscription with the annual plan and apply a coupon code', function(done) {
  566. this.req.body = { planName: 'student' }
  567. this.res.sendStatus = () => {
  568. this.SubscriptionHandler.updateSubscription
  569. .calledWith(this.user, 'student-annual', 'STUDENTCODEHERE')
  570. .should.equal(true)
  571. return done()
  572. }
  573. return this.SubscriptionController.processUpgradeToAnnualPlan(
  574. this.req,
  575. this.res
  576. )
  577. })
  578. it('should get the collaborator coupon code', function(done) {
  579. this.req.body = { planName: 'collaborator' }
  580. this.res.sendStatus = url => {
  581. this.SubscriptionHandler.updateSubscription
  582. .calledWith(this.user, 'collaborator-annual', 'COLLABORATORCODEHERE')
  583. .should.equal(true)
  584. return done()
  585. }
  586. return this.SubscriptionController.processUpgradeToAnnualPlan(
  587. this.req,
  588. this.res
  589. )
  590. })
  591. })
  592. })