SubscriptionControllerTests.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890
  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 { expect } = require('chai')
  17. const MockRequest = require('../helpers/MockRequest')
  18. const MockResponse = require('../helpers/MockResponse')
  19. const modulePath =
  20. '../../../../app/src/Features/Subscription/SubscriptionController'
  21. const Errors = require('../../../../app/src/Features/Errors/Errors')
  22. const SubscriptionErrors = require('../../../../app/src/Features/Subscription/Errors')
  23. const mockSubscriptions = {
  24. 'subscription-123-active': {
  25. uuid: 'subscription-123-active',
  26. plan: {
  27. name: 'Gold',
  28. plan_code: 'gold',
  29. },
  30. current_period_ends_at: new Date(),
  31. state: 'active',
  32. unit_amount_in_cents: 999,
  33. account: {
  34. account_code: 'user-123',
  35. },
  36. },
  37. }
  38. describe('SubscriptionController', function () {
  39. beforeEach(function () {
  40. this.user = {
  41. email: 'tom@yahoo.com',
  42. _id: 'one',
  43. signUpDate: new Date('2000-10-01'),
  44. }
  45. this.activeRecurlySubscription =
  46. mockSubscriptions['subscription-123-active']
  47. this.SessionManager = {
  48. getLoggedInUser: sinon.stub().callsArgWith(1, null, this.user),
  49. getLoggedInUserId: sinon.stub().returns(this.user._id),
  50. getSessionUser: sinon.stub().returns(this.user),
  51. isUserLoggedIn: sinon.stub().returns(true),
  52. }
  53. this.SubscriptionHandler = {
  54. createSubscription: sinon.stub().callsArgWith(3),
  55. updateSubscription: sinon.stub().callsArgWith(3),
  56. reactivateSubscription: sinon.stub().callsArgWith(1),
  57. cancelSubscription: sinon.stub().callsArgWith(1),
  58. syncSubscription: sinon.stub().yields(),
  59. attemptPaypalInvoiceCollection: sinon.stub().yields(),
  60. startFreeTrial: sinon.stub(),
  61. promises: {
  62. createSubscription: sinon.stub().resolves(),
  63. updateSubscription: sinon.stub().resolves(),
  64. reactivateSubscription: sinon.stub().resolves(),
  65. cancelSubscription: sinon.stub().resolves(),
  66. syncSubscription: sinon.stub().resolves(),
  67. attemptPaypalInvoiceCollection: sinon.stub().resolves(),
  68. startFreeTrial: sinon.stub().resolves(),
  69. },
  70. }
  71. this.PlansLocator = { findLocalPlanInSettings: sinon.stub() }
  72. this.LimitationsManager = {
  73. hasPaidSubscription: sinon.stub(),
  74. userHasV1OrV2Subscription: sinon.stub(),
  75. userHasV2Subscription: sinon.stub(),
  76. promises: {
  77. hasPaidSubscription: sinon.stub().resolves(),
  78. userHasV1OrV2Subscription: sinon.stub().resolves(),
  79. userHasV2Subscription: sinon.stub().resolves(),
  80. },
  81. }
  82. this.SubscriptionViewModelBuilder = {
  83. buildUsersSubscriptionViewModel: sinon.stub().callsArgWith(1, null, {}),
  84. buildPlansList: sinon.stub(),
  85. promises: {
  86. buildUsersSubscriptionViewModel: sinon.stub().resolves({}),
  87. },
  88. }
  89. this.settings = {
  90. coupon_codes: {
  91. upgradeToAnnualPromo: {
  92. student: 'STUDENTCODEHERE',
  93. collaborator: 'COLLABORATORCODEHERE',
  94. },
  95. },
  96. groupPlanModalOptions: {
  97. plan_codes: [],
  98. currencies: [
  99. {
  100. display: 'GBP (£)',
  101. code: 'GBP',
  102. },
  103. ],
  104. sizes: ['42'],
  105. usages: [{ code: 'foo', display: 'Foo' }],
  106. },
  107. apis: {
  108. recurly: {
  109. subdomain: 'sl',
  110. },
  111. },
  112. siteUrl: 'http://de.sharelatex.dev:3000',
  113. gaExperiments: {},
  114. }
  115. this.GeoIpLookup = {
  116. isValidCurrencyParam: sinon.stub().returns(true),
  117. getCurrencyCode: sinon.stub(),
  118. promises: {
  119. getCurrencyCode: sinon.stub(),
  120. },
  121. }
  122. this.UserGetter = {
  123. getUser: sinon.stub().callsArgWith(2, null, this.user),
  124. promises: {
  125. getUser: sinon.stub().resolves(this.user),
  126. },
  127. }
  128. this.SplitTestV2Hander = {
  129. promises: {
  130. getAssignmentForSession: sinon.stub().resolves({ variant: 'default' }),
  131. },
  132. }
  133. this.SubscriptionController = SandboxedModule.require(modulePath, {
  134. requires: {
  135. '../SplitTests/SplitTestV2Handler': this.SplitTestV2Hander,
  136. '../Authentication/SessionManager': this.SessionManager,
  137. './SubscriptionHandler': this.SubscriptionHandler,
  138. './PlansLocator': this.PlansLocator,
  139. './SubscriptionViewModelBuilder': this.SubscriptionViewModelBuilder,
  140. './LimitationsManager': this.LimitationsManager,
  141. '../../infrastructure/GeoIpLookup': this.GeoIpLookup,
  142. '@overleaf/settings': this.settings,
  143. '../User/UserGetter': this.UserGetter,
  144. './RecurlyWrapper': (this.RecurlyWrapper = {
  145. updateAccountEmailAddress: sinon.stub().yields(),
  146. }),
  147. './RecurlyEventHandler': { sendRecurlyAnalyticsEvent: sinon.stub() },
  148. './FeaturesUpdater': (this.FeaturesUpdater = {}),
  149. './GroupPlansData': (this.GroupPlansData = {}),
  150. './V1SubscriptionManager': (this.V1SubscriptionManager = {}),
  151. '../Errors/HttpErrorHandler': (this.HttpErrorHandler = {
  152. unprocessableEntity: sinon.stub(),
  153. }),
  154. './Errors': SubscriptionErrors,
  155. '../Analytics/AnalyticsManager': (this.AnalyticsManager = {
  156. recordEventForUser: sinon.stub(),
  157. recordEventForSession: sinon.stub(),
  158. setUserPropertyForUser: sinon.stub(),
  159. }),
  160. '../SplitTests/SplitTestHandler': (this.SplitTestHandler = {
  161. getTestSegmentation: () => {},
  162. }),
  163. },
  164. })
  165. this.res = new MockResponse()
  166. this.req = new MockRequest()
  167. this.req.body = {}
  168. this.req.query = { planCode: '123123' }
  169. return (this.stubbedCurrencyCode = 'GBP')
  170. })
  171. describe('plansPage', function () {
  172. beforeEach(function () {
  173. this.req.ip = '1234.3123.3131.333 313.133.445.666 653.5345.5345.534'
  174. return this.GeoIpLookup.promises.getCurrencyCode.resolves({
  175. currencyCode: this.stubbedCurrencyCode,
  176. })
  177. })
  178. describe('splitTest', function () {
  179. const cases = [
  180. {
  181. variant: 'default',
  182. template: 'subscriptions/plans',
  183. },
  184. {
  185. variant: 'de-ng',
  186. template: 'subscriptions/plans-marketing',
  187. },
  188. ]
  189. for (const { variant, template } of cases) {
  190. describe(variant, function () {
  191. beforeEach(function () {
  192. const assignment = { variant }
  193. this.SplitTestV2Hander.promises.getAssignmentForSession.resolves(
  194. assignment
  195. )
  196. })
  197. it(`should render template ${template}`, function (done) {
  198. this.res.render = page => {
  199. page.should.equal(template)
  200. done()
  201. }
  202. this.SubscriptionController.plansPage(this.req, this.res)
  203. })
  204. })
  205. }
  206. })
  207. describe('groupPlanModal data', function () {
  208. it('should pass local currency if valid', function (done) {
  209. this.res.render = (page, opts) => {
  210. page.should.equal('subscriptions/plans')
  211. opts.groupPlanModalDefaults.currency.should.equal('GBP')
  212. done()
  213. }
  214. this.GeoIpLookup.promises.getCurrencyCode.resolves({
  215. currencyCode: 'GBP',
  216. })
  217. this.SubscriptionController.plansPage(this.req, this.res)
  218. })
  219. it('should fallback to USD when valid', function (done) {
  220. this.res.render = (page, opts) => {
  221. page.should.equal('subscriptions/plans')
  222. opts.groupPlanModalDefaults.currency.should.equal('USD')
  223. done()
  224. }
  225. this.GeoIpLookup.promises.getCurrencyCode.resolves({
  226. currencyCode: 'FOO',
  227. })
  228. this.SubscriptionController.plansPage(this.req, this.res)
  229. })
  230. it('should pass valid options for group plan modal and discard invalid', function (done) {
  231. this.res.render = (page, opts) => {
  232. page.should.equal('subscriptions/plans')
  233. opts.groupPlanModalDefaults.size.should.equal('42')
  234. opts.groupPlanModalDefaults.plan_code.should.equal('collaborator')
  235. opts.groupPlanModalDefaults.currency.should.equal('GBP')
  236. opts.groupPlanModalDefaults.usage.should.equal('foo')
  237. done()
  238. }
  239. this.req.query = {
  240. number: '42',
  241. currency: 'ABC',
  242. plan: 'does-not-exist',
  243. usage: 'foo',
  244. }
  245. this.SubscriptionController.plansPage(this.req, this.res)
  246. })
  247. })
  248. })
  249. describe('paymentPage', function () {
  250. beforeEach(function () {
  251. this.req.headers = {}
  252. this.SubscriptionHandler.promises.validateNoSubscriptionInRecurly = sinon
  253. .stub()
  254. .resolves(true)
  255. return this.GeoIpLookup.promises.getCurrencyCode.resolves({
  256. currencyCode: this.stubbedCurrencyCode,
  257. })
  258. })
  259. describe('with a user without a subscription', function () {
  260. beforeEach(function () {
  261. this.LimitationsManager.promises.userHasV1OrV2Subscription.resolves(
  262. false
  263. )
  264. return this.PlansLocator.findLocalPlanInSettings.returns({})
  265. })
  266. describe('with a valid plan code', function () {
  267. it('should render the new subscription page', function (done) {
  268. this.res.render = (page, opts) => {
  269. page.should.equal('subscriptions/new')
  270. done()
  271. }
  272. this.SubscriptionController.paymentPage(this.req, this.res)
  273. })
  274. })
  275. })
  276. describe('with a user with subscription', function () {
  277. it('should redirect to the subscription dashboard', function (done) {
  278. this.PlansLocator.findLocalPlanInSettings.returns({})
  279. this.LimitationsManager.promises.userHasV1OrV2Subscription.resolves(
  280. true
  281. )
  282. this.res.redirect = url => {
  283. url.should.equal('/user/subscription?hasSubscription=true')
  284. return done()
  285. }
  286. return this.SubscriptionController.paymentPage(this.req, this.res)
  287. })
  288. })
  289. describe('with an invalid plan code', function () {
  290. it('should return 422 error - Unprocessable Entity', function (done) {
  291. this.LimitationsManager.promises.userHasV1OrV2Subscription.resolves(
  292. false
  293. )
  294. this.PlansLocator.findLocalPlanInSettings.returns(null)
  295. this.HttpErrorHandler.unprocessableEntity = sinon.spy(
  296. (req, res, message) => {
  297. expect(req).to.exist
  298. expect(res).to.exist
  299. expect(message).to.deep.equal('Plan not found')
  300. done()
  301. }
  302. )
  303. return this.SubscriptionController.paymentPage(this.req, this.res)
  304. })
  305. })
  306. describe('which currency to use', function () {
  307. beforeEach(function () {
  308. this.LimitationsManager.promises.userHasV1OrV2Subscription.resolves(
  309. false
  310. )
  311. return this.PlansLocator.findLocalPlanInSettings.returns({})
  312. })
  313. it('should use the set currency from the query string', function (done) {
  314. this.req.query.currency = 'EUR'
  315. this.res.render = (page, opts) => {
  316. opts.currency.should.equal('EUR')
  317. opts.currency.should.not.equal(this.stubbedCurrencyCode)
  318. return done()
  319. }
  320. return this.SubscriptionController.paymentPage(this.req, this.res)
  321. })
  322. it('should upercase the currency code', function (done) {
  323. this.req.query.currency = 'eur'
  324. this.res.render = (page, opts) => {
  325. opts.currency.should.equal('EUR')
  326. return done()
  327. }
  328. return this.SubscriptionController.paymentPage(this.req, this.res)
  329. })
  330. it('should use the geo ip currency if non is provided', function (done) {
  331. this.req.query.currency = null
  332. this.res.render = (page, opts) => {
  333. opts.currency.should.equal(this.stubbedCurrencyCode)
  334. done()
  335. }
  336. this.SubscriptionController.paymentPage(this.req, this.res)
  337. })
  338. it('should use the geo ip currency if not valid', function (done) {
  339. this.req.query.currency = 'WAT'
  340. this.GeoIpLookup.isValidCurrencyParam.returns(false)
  341. this.res.render = (page, opts) => {
  342. opts.currency.should.equal(this.stubbedCurrencyCode)
  343. done()
  344. }
  345. this.SubscriptionController.paymentPage(this.req, this.res)
  346. })
  347. })
  348. describe('with a recurly subscription already', function () {
  349. it('should redirect to the subscription dashboard', function (done) {
  350. this.PlansLocator.findLocalPlanInSettings.returns({})
  351. this.LimitationsManager.promises.userHasV1OrV2Subscription.resolves(
  352. false
  353. )
  354. this.SubscriptionHandler.promises.validateNoSubscriptionInRecurly.resolves(
  355. false
  356. )
  357. this.res.redirect = url => {
  358. url.should.equal('/user/subscription?hasSubscription=true')
  359. return done()
  360. }
  361. return this.SubscriptionController.paymentPage(this.req, this.res)
  362. })
  363. })
  364. })
  365. describe('successfulSubscription', function () {
  366. beforeEach(function (done) {
  367. this.SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel.callsArgWith(
  368. 1,
  369. null,
  370. {}
  371. )
  372. this.res.callback = done
  373. return this.SubscriptionController.successfulSubscription(
  374. this.req,
  375. this.res
  376. )
  377. })
  378. })
  379. describe('userSubscriptionPage', function () {
  380. beforeEach(function (done) {
  381. this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel.resolves(
  382. {
  383. personalSubscription: (this.personalSubscription = {
  384. 'personal-subscription': 'mock',
  385. }),
  386. memberGroupSubscriptions: (this.memberGroupSubscriptions = {
  387. 'group-subscriptions': 'mock',
  388. }),
  389. }
  390. )
  391. this.SubscriptionViewModelBuilder.buildPlansList.returns(
  392. (this.plans = { plans: 'mock' })
  393. )
  394. this.LimitationsManager.promises.userHasV1OrV2Subscription.resolves(false)
  395. this.res.render = (view, data) => {
  396. this.data = data
  397. expect(view).to.equal('subscriptions/dashboard')
  398. done()
  399. }
  400. this.SubscriptionController.userSubscriptionPage(this.req, this.res)
  401. })
  402. it('should load the personal, groups and v1 subscriptions', function () {
  403. expect(this.data.personalSubscription).to.deep.equal(
  404. this.personalSubscription
  405. )
  406. return expect(this.data.memberGroupSubscriptions).to.deep.equal(
  407. this.memberGroupSubscriptions
  408. )
  409. })
  410. it('should load the user', function () {
  411. return expect(this.data.user).to.deep.equal(this.user)
  412. })
  413. it('should load the plans', function () {
  414. return expect(this.data.plans).to.deep.equal(this.plans)
  415. })
  416. })
  417. describe('createSubscription', function () {
  418. beforeEach(function (done) {
  419. this.res = {
  420. sendStatus() {
  421. return done()
  422. },
  423. }
  424. sinon.spy(this.res, 'sendStatus')
  425. this.subscriptionDetails = {
  426. card: '1234',
  427. cvv: '123',
  428. }
  429. this.recurlyTokenIds = {
  430. billing: '1234',
  431. threeDSecureActionResult: '5678',
  432. }
  433. this.req.body.recurly_token_id = this.recurlyTokenIds.billing
  434. this.req.body.recurly_three_d_secure_action_result_token_id = this.recurlyTokenIds.threeDSecureActionResult
  435. this.req.body.subscriptionDetails = this.subscriptionDetails
  436. this.LimitationsManager.userHasV1OrV2Subscription.yields(null, false)
  437. return this.SubscriptionController.createSubscription(this.req, this.res)
  438. })
  439. it('should send the user and subscriptionId to the handler', function (done) {
  440. this.SubscriptionHandler.createSubscription
  441. .calledWithMatch(
  442. this.user,
  443. this.subscriptionDetails,
  444. this.recurlyTokenIds
  445. )
  446. .should.equal(true)
  447. return done()
  448. })
  449. it('should redurect to the subscription page', function (done) {
  450. this.res.sendStatus.calledWith(201).should.equal(true)
  451. return done()
  452. })
  453. })
  454. describe('createSubscription with errors', function () {
  455. it('should handle users with subscription', function (done) {
  456. this.LimitationsManager.userHasV1OrV2Subscription.yields(null, true)
  457. this.SubscriptionController.createSubscription(this.req, {
  458. sendStatus: status => {
  459. expect(status).to.equal(409)
  460. this.SubscriptionHandler.createSubscription.called.should.equal(false)
  461. done()
  462. },
  463. })
  464. })
  465. it('should handle 3DSecure errors', function (done) {
  466. this.next = sinon.stub()
  467. this.LimitationsManager.userHasV1OrV2Subscription.yields(null, false)
  468. this.SubscriptionHandler.createSubscription.yields(
  469. new SubscriptionErrors.RecurlyTransactionError({})
  470. )
  471. this.HttpErrorHandler.unprocessableEntity = sinon.spy(
  472. (req, res, message) => {
  473. expect(req).to.exist
  474. expect(res).to.exist
  475. expect(message).to.deep.equal('Unknown transaction error')
  476. done()
  477. }
  478. )
  479. this.SubscriptionController.createSubscription(this.req, this.res)
  480. })
  481. it('should handle validation errors', function (done) {
  482. this.next = sinon.stub()
  483. this.LimitationsManager.userHasV1OrV2Subscription.yields(null, false)
  484. this.SubscriptionHandler.createSubscription.yields(
  485. new Errors.InvalidError('invalid error test')
  486. )
  487. this.HttpErrorHandler.unprocessableEntity = sinon.spy(
  488. (req, res, message) => {
  489. expect(req).to.exist
  490. expect(res).to.exist
  491. expect(message).to.deep.equal('invalid error test')
  492. done()
  493. }
  494. )
  495. this.SubscriptionController.createSubscription(this.req, this.res)
  496. })
  497. it('should handle recurly errors', function (done) {
  498. this.LimitationsManager.userHasV1OrV2Subscription.yields(null, false)
  499. this.SubscriptionHandler.createSubscription.yields(
  500. new SubscriptionErrors.RecurlyTransactionError({})
  501. )
  502. this.HttpErrorHandler.unprocessableEntity = sinon.spy(
  503. (req, res, info) => {
  504. expect(req).to.exist
  505. expect(res).to.exist
  506. expect(info).to.deep.equal('Unknown transaction error')
  507. done()
  508. }
  509. )
  510. return this.SubscriptionController.createSubscription(this.req, this.res)
  511. })
  512. it('should handle invalid error', function (done) {
  513. this.LimitationsManager.userHasV1OrV2Subscription.yields(null, false)
  514. this.SubscriptionHandler.createSubscription.yields(
  515. new Errors.InvalidError({})
  516. )
  517. this.HttpErrorHandler.unprocessableEntity = sinon.spy((req, res) => {
  518. expect(req).to.exist
  519. expect(res).to.exist
  520. done()
  521. })
  522. return this.SubscriptionController.createSubscription(this.req, this.res)
  523. })
  524. })
  525. describe('updateSubscription via post', function () {
  526. beforeEach(function (done) {
  527. this.res = {
  528. redirect() {
  529. return done()
  530. },
  531. }
  532. sinon.spy(this.res, 'redirect')
  533. this.plan_code = '1234'
  534. this.req.body.plan_code = this.plan_code
  535. return this.SubscriptionController.updateSubscription(this.req, this.res)
  536. })
  537. it('should send the user and subscriptionId to the handler', function (done) {
  538. this.SubscriptionHandler.updateSubscription
  539. .calledWith(this.user, this.plan_code)
  540. .should.equal(true)
  541. return done()
  542. })
  543. it('should redurect to the subscription page', function (done) {
  544. this.res.redirect.calledWith('/user/subscription').should.equal(true)
  545. return done()
  546. })
  547. })
  548. describe('updateAccountEmailAddress via put', function () {
  549. it('should send the user and subscriptionId to RecurlyWrapper', function () {
  550. this.res.sendStatus = sinon.spy()
  551. this.SubscriptionController.updateAccountEmailAddress(this.req, this.res)
  552. this.RecurlyWrapper.updateAccountEmailAddress
  553. .calledWith(this.user._id, this.user.email)
  554. .should.equal(true)
  555. })
  556. it('should respond with 200', function () {
  557. this.res.sendStatus = sinon.spy()
  558. this.SubscriptionController.updateAccountEmailAddress(this.req, this.res)
  559. this.res.sendStatus.calledWith(200).should.equal(true)
  560. })
  561. it('should send the error to the next handler when updating recurly account email fails', function (done) {
  562. this.RecurlyWrapper.updateAccountEmailAddress.yields(new Error())
  563. this.next = sinon.spy(error => {
  564. expect(error).instanceOf(Error)
  565. done()
  566. })
  567. this.SubscriptionController.updateAccountEmailAddress(
  568. this.req,
  569. this.res,
  570. this.next
  571. )
  572. })
  573. })
  574. describe('reactivateSubscription', function () {
  575. beforeEach(function (done) {
  576. this.res = {
  577. redirect() {
  578. return done()
  579. },
  580. }
  581. sinon.spy(this.res, 'redirect')
  582. return this.SubscriptionController.reactivateSubscription(
  583. this.req,
  584. this.res
  585. )
  586. })
  587. it('should tell the handler to reactivate this user', function (done) {
  588. this.SubscriptionHandler.reactivateSubscription
  589. .calledWith(this.user)
  590. .should.equal(true)
  591. return done()
  592. })
  593. it('should redurect to the subscription page', function (done) {
  594. this.res.redirect.calledWith('/user/subscription').should.equal(true)
  595. return done()
  596. })
  597. })
  598. describe('cancelSubscription', function () {
  599. beforeEach(function (done) {
  600. this.res = {
  601. redirect() {
  602. return done()
  603. },
  604. }
  605. sinon.spy(this.res, 'redirect')
  606. return this.SubscriptionController.cancelSubscription(this.req, this.res)
  607. })
  608. it('should tell the handler to cancel this user', function (done) {
  609. this.SubscriptionHandler.cancelSubscription
  610. .calledWith(this.user)
  611. .should.equal(true)
  612. return done()
  613. })
  614. it('should redurect to the subscription page', function (done) {
  615. this.res.redirect
  616. .calledWith('/user/subscription/canceled')
  617. .should.equal(true)
  618. return done()
  619. })
  620. })
  621. describe('recurly callback', function () {
  622. describe('with a sync subscription request', function () {
  623. beforeEach(function (done) {
  624. this.req = {
  625. body: {
  626. expired_subscription_notification: {
  627. account: {
  628. account_code: this.user._id,
  629. },
  630. subscription: {
  631. uuid: this.activeRecurlySubscription.uuid,
  632. plan: {
  633. plan_code: 'collaborator',
  634. state: 'active',
  635. },
  636. },
  637. },
  638. },
  639. }
  640. this.res = {
  641. sendStatus() {
  642. return done()
  643. },
  644. }
  645. sinon.spy(this.res, 'sendStatus')
  646. return this.SubscriptionController.recurlyCallback(this.req, this.res)
  647. })
  648. it('should tell the SubscriptionHandler to process the recurly callback', function (done) {
  649. this.SubscriptionHandler.syncSubscription.called.should.equal(true)
  650. return done()
  651. })
  652. it('should send a 200', function (done) {
  653. this.res.sendStatus.calledWith(200)
  654. return done()
  655. })
  656. })
  657. describe('with a billing info updated request', function () {
  658. beforeEach(function (done) {
  659. this.req = {
  660. body: {
  661. billing_info_updated_notification: {
  662. account: {
  663. account_code: 'mock-account-code',
  664. },
  665. },
  666. },
  667. }
  668. this.res = {
  669. sendStatus() {
  670. done()
  671. },
  672. }
  673. sinon.spy(this.res, 'sendStatus')
  674. this.SubscriptionController.recurlyCallback(this.req, this.res)
  675. })
  676. it('should call attemptPaypalInvoiceCollection', function (done) {
  677. this.SubscriptionHandler.attemptPaypalInvoiceCollection
  678. .calledWith('mock-account-code')
  679. .should.equal(true)
  680. done()
  681. })
  682. it('should send a 200', function (done) {
  683. this.res.sendStatus.calledWith(200)
  684. done()
  685. })
  686. })
  687. describe('with a non-actionable request', function () {
  688. beforeEach(function (done) {
  689. this.user.id = this.activeRecurlySubscription.account.account_code
  690. this.req = {
  691. body: {
  692. renewed_subscription_notification: {
  693. account: {
  694. account_code: this.user._id,
  695. },
  696. subscription: {
  697. uuid: this.activeRecurlySubscription.uuid,
  698. plan: {
  699. plan_code: 'collaborator',
  700. state: 'active',
  701. },
  702. },
  703. },
  704. },
  705. }
  706. this.res = {
  707. sendStatus() {
  708. return done()
  709. },
  710. }
  711. sinon.spy(this.res, 'sendStatus')
  712. return this.SubscriptionController.recurlyCallback(this.req, this.res)
  713. })
  714. it('should not call the subscriptionshandler', function () {
  715. this.SubscriptionHandler.syncSubscription.called.should.equal(false)
  716. this.SubscriptionHandler.attemptPaypalInvoiceCollection.called.should.equal(
  717. false
  718. )
  719. })
  720. it('should respond with a 200 status', function () {
  721. return this.res.sendStatus.calledWith(200)
  722. })
  723. })
  724. })
  725. describe('renderUpgradeToAnnualPlanPage', function () {
  726. it('should redirect to the plans page if the user does not have a subscription', function (done) {
  727. this.LimitationsManager.userHasV2Subscription.callsArgWith(1, null, false)
  728. this.res.redirect = function (url) {
  729. url.should.equal('/user/subscription/plans')
  730. return done()
  731. }
  732. return this.SubscriptionController.renderUpgradeToAnnualPlanPage(
  733. this.req,
  734. this.res
  735. )
  736. })
  737. it('should pass the plan code to the view - student', function (done) {
  738. this.LimitationsManager.userHasV2Subscription.callsArgWith(
  739. 1,
  740. null,
  741. true,
  742. { planCode: 'Student free trial 14 days' }
  743. )
  744. this.res.render = function (view, opts) {
  745. view.should.equal('subscriptions/upgradeToAnnual')
  746. opts.planName.should.equal('student')
  747. return done()
  748. }
  749. return this.SubscriptionController.renderUpgradeToAnnualPlanPage(
  750. this.req,
  751. this.res
  752. )
  753. })
  754. it('should pass the plan code to the view - collaborator', function (done) {
  755. this.LimitationsManager.userHasV2Subscription.callsArgWith(
  756. 1,
  757. null,
  758. true,
  759. { planCode: 'free trial for Collaborator free trial 14 days' }
  760. )
  761. this.res.render = function (view, opts) {
  762. opts.planName.should.equal('collaborator')
  763. return done()
  764. }
  765. return this.SubscriptionController.renderUpgradeToAnnualPlanPage(
  766. this.req,
  767. this.res
  768. )
  769. })
  770. it('should pass annual as the plan name if the user is already on an annual plan', function (done) {
  771. this.LimitationsManager.userHasV2Subscription.callsArgWith(
  772. 1,
  773. null,
  774. true,
  775. { planCode: 'student annual with free trial' }
  776. )
  777. this.res.render = function (view, opts) {
  778. opts.planName.should.equal('annual')
  779. return done()
  780. }
  781. return this.SubscriptionController.renderUpgradeToAnnualPlanPage(
  782. this.req,
  783. this.res
  784. )
  785. })
  786. })
  787. describe('processUpgradeToAnnualPlan', function () {
  788. beforeEach(function () {})
  789. it('should tell the subscription handler to update the subscription with the annual plan and apply a coupon code', function (done) {
  790. this.req.body = { planName: 'student' }
  791. this.res.sendStatus = () => {
  792. this.SubscriptionHandler.updateSubscription
  793. .calledWith(this.user, 'student-annual', 'STUDENTCODEHERE')
  794. .should.equal(true)
  795. return done()
  796. }
  797. return this.SubscriptionController.processUpgradeToAnnualPlan(
  798. this.req,
  799. this.res
  800. )
  801. })
  802. it('should get the collaborator coupon code', function (done) {
  803. this.req.body = { planName: 'collaborator' }
  804. this.res.sendStatus = url => {
  805. this.SubscriptionHandler.updateSubscription
  806. .calledWith(this.user, 'collaborator-annual', 'COLLABORATORCODEHERE')
  807. .should.equal(true)
  808. return done()
  809. }
  810. return this.SubscriptionController.processUpgradeToAnnualPlan(
  811. this.req,
  812. this.res
  813. )
  814. })
  815. })
  816. })