SubscriptionControllerTests.js 26 KB

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