SubscriptionControllerTests.js 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257
  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. formatCurrencyLocalized: 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('showInrGeoBanner data', function () {
  262. it('should return true for Indian Users', function (done) {
  263. this.res.render = (page, opts) => {
  264. page.should.equal('subscriptions/plans')
  265. opts.showInrGeoBanner.should.equal(true)
  266. done()
  267. }
  268. this.GeoIpLookup.promises.getCurrencyCode.resolves({
  269. countryCode: 'IN',
  270. })
  271. this.SubscriptionController.plansPage(this.req, this.res)
  272. })
  273. it('should return false for US Users', function (done) {
  274. this.res.render = (page, opts) => {
  275. page.should.equal('subscriptions/plans')
  276. opts.showInrGeoBanner.should.equal(false)
  277. done()
  278. }
  279. this.GeoIpLookup.promises.getCurrencyCode.resolves({
  280. countryCode: 'US',
  281. })
  282. this.SubscriptionController.plansPage(this.req, this.res)
  283. })
  284. })
  285. describe('showBrlGeoBanner data', function () {
  286. it('should return true for Brazilian users', function (done) {
  287. this.res.render = (page, opts) => {
  288. page.should.equal('subscriptions/plans')
  289. opts.showBrlGeoBanner.should.equal(true)
  290. done()
  291. }
  292. this.GeoIpLookup.promises.getCurrencyCode.resolves({
  293. countryCode: 'BR',
  294. })
  295. this.SubscriptionController.plansPage(this.req, this.res)
  296. })
  297. it('should return false for US users', function (done) {
  298. this.res.render = (page, opts) => {
  299. page.should.equal('subscriptions/plans')
  300. opts.showBrlGeoBanner.should.equal(false)
  301. done()
  302. }
  303. this.GeoIpLookup.promises.getCurrencyCode.resolves({
  304. countryCode: 'US',
  305. })
  306. this.SubscriptionController.plansPage(this.req, this.res)
  307. })
  308. })
  309. describe('showLATAMBanner', function () {
  310. it('should return true for Mexican users', function (done) {
  311. this.res.render = (page, opts) => {
  312. page.should.equal('subscriptions/plans')
  313. opts.showLATAMBanner.should.equal(true)
  314. done()
  315. }
  316. this.GeoIpLookup.promises.getCurrencyCode.resolves({
  317. countryCode: 'MX',
  318. })
  319. this.SubscriptionController.plansPage(this.req, this.res)
  320. })
  321. it('should return true for Colombian users', function (done) {
  322. this.res.render = (page, opts) => {
  323. page.should.equal('subscriptions/plans')
  324. opts.showLATAMBanner.should.equal(true)
  325. done()
  326. }
  327. this.GeoIpLookup.promises.getCurrencyCode.resolves({
  328. countryCode: 'CO',
  329. })
  330. this.SubscriptionController.plansPage(this.req, this.res)
  331. })
  332. it('should return true for Chilean users', function (done) {
  333. this.res.render = (page, opts) => {
  334. page.should.equal('subscriptions/plans')
  335. opts.showLATAMBanner.should.equal(true)
  336. done()
  337. }
  338. this.GeoIpLookup.promises.getCurrencyCode.resolves({
  339. countryCode: 'CL',
  340. })
  341. this.SubscriptionController.plansPage(this.req, this.res)
  342. })
  343. it('should return true for Peruvian users', function (done) {
  344. this.res.render = (page, opts) => {
  345. page.should.equal('subscriptions/plans')
  346. opts.showLATAMBanner.should.equal(true)
  347. done()
  348. }
  349. this.GeoIpLookup.promises.getCurrencyCode.resolves({
  350. countryCode: 'PE',
  351. })
  352. this.SubscriptionController.plansPage(this.req, this.res)
  353. })
  354. it('should return true for US users', function (done) {
  355. this.res.render = (page, opts) => {
  356. page.should.equal('subscriptions/plans')
  357. opts.showLATAMBanner.should.equal(false)
  358. done()
  359. }
  360. this.GeoIpLookup.promises.getCurrencyCode.resolves({
  361. countryCode: 'US',
  362. })
  363. this.SubscriptionController.plansPage(this.req, this.res)
  364. })
  365. })
  366. describe('localCcyAssignment', function () {
  367. it('uses formatCurrencyLocalized when variant is enabled', function (done) {
  368. this.SplitTestV2Hander.promises.getAssignment
  369. .withArgs(this.req, this.res, 'local-ccy-format-v2')
  370. .resolves({
  371. variant: 'enabled',
  372. })
  373. this.res.render = (page, opts) => {
  374. expect(opts.formatCurrency).to.equal(
  375. this.currency.formatCurrencyLocalized
  376. )
  377. done()
  378. }
  379. this.SubscriptionController.plansPage(this.req, this.res)
  380. })
  381. it('uses formatCurrencyDefault when variant is default', function (done) {
  382. this.res.render = (page, opts) => {
  383. expect(opts.formatCurrency).to.equal(
  384. this.SubscriptionHelper.formatCurrencyDefault
  385. )
  386. done()
  387. }
  388. this.SubscriptionController.plansPage(this.req, this.res)
  389. })
  390. })
  391. describe('website-redesign-plans test', function () {
  392. beforeEach(function () {
  393. this.req.query = {}
  394. })
  395. describe('"default" variant', function () {
  396. // note: if test is not active, default variant is assigned
  397. beforeEach(function () {
  398. this.SplitTestV2Hander.promises.getAssignment
  399. .withArgs(this.req, this.res, 'website-redesign-plans')
  400. .resolves({
  401. variant: 'default',
  402. })
  403. })
  404. it('renders "default" variant', function (done) {
  405. this.res.render = page => {
  406. page.should.equal('subscriptions/plans')
  407. expect(this.res.redirect).to.not.have.been.called
  408. done()
  409. }
  410. this.SubscriptionController.plansPage(this.req, this.res)
  411. })
  412. })
  413. describe('"new-design" variant', function () {
  414. beforeEach(function () {
  415. this.SplitTestV2Hander.promises.getAssignment
  416. .withArgs(this.req, this.res, 'website-redesign-plans')
  417. .resolves({
  418. variant: 'new-design',
  419. })
  420. })
  421. it('redirects to "new-design" variant', function (done) {
  422. this.res.callback = () => {
  423. expect(this.res.redirect).to.have.been.calledWith(
  424. 302,
  425. '/user/subscription/plans-2'
  426. )
  427. done()
  428. }
  429. this.SubscriptionController.plansPage(this.req, this.res)
  430. })
  431. it('passes query params when redirecting to new design variant', function (done) {
  432. this.req.query = { currency: 'USD' }
  433. this.res.callback = () => {
  434. expect(this.res.redirect).to.have.been.calledWith(
  435. 302,
  436. '/user/subscription/plans-2?currency=USD'
  437. )
  438. done()
  439. }
  440. this.SubscriptionController.plansPage(this.req, this.res)
  441. })
  442. })
  443. describe('"light-design" variant', function () {
  444. beforeEach(function () {
  445. this.SplitTestV2Hander.promises.getAssignment
  446. .withArgs(this.req, this.res, 'website-redesign-plans')
  447. .resolves({
  448. variant: 'light-design',
  449. })
  450. })
  451. it('renders "light-design" variant', function (done) {
  452. this.res.callback = () => {
  453. expect(this.res.redirect).to.have.been.calledWith(
  454. 302,
  455. '/user/subscription/plans-3'
  456. )
  457. done()
  458. }
  459. this.SubscriptionController.plansPage(this.req, this.res)
  460. })
  461. it('passes query params when redirecting to new design variant', function (done) {
  462. this.req.query = { currency: 'USD' }
  463. this.res.callback = () => {
  464. expect(this.res.redirect).to.have.been.calledWith(
  465. 302,
  466. '/user/subscription/plans-3?currency=USD'
  467. )
  468. done()
  469. }
  470. this.SubscriptionController.plansPage(this.req, this.res)
  471. })
  472. })
  473. })
  474. it('should return correct countryCode', function (done) {
  475. this.GeoIpLookup.promises.getCurrencyCode.resolves({
  476. countryCode: 'MX',
  477. })
  478. this.res.render = (page, opts) => {
  479. expect(opts.countryCode).to.equal('MX')
  480. done()
  481. }
  482. this.SubscriptionController.plansPage(this.req, this.res)
  483. })
  484. })
  485. describe('plansPage light touch redesign', function () {
  486. beforeEach(function () {
  487. this.req.ip = '1234.3123.3131.333 313.133.445.666 653.5345.5345.534'
  488. this.GeoIpLookup.promises.getCurrencyCode.resolves({
  489. currencyCode: this.stubbedCurrencyCode,
  490. })
  491. })
  492. describe('ip override', function () {
  493. beforeEach(function () {
  494. this.req.ip = '1.2.3.4'
  495. this.req.query = { ip: '5.6.7.8' }
  496. this.GeoIpLookup.promises.getCurrencyCode.withArgs('1.2.3.4').resolves({
  497. currencyCode: 'GBP',
  498. })
  499. this.GeoIpLookup.promises.getCurrencyCode.withArgs('5.6.7.8').resolves({
  500. currencyCode: 'USD',
  501. })
  502. })
  503. it('should ignore override for non admin', function (done) {
  504. this.res.render = (page, opts) => {
  505. opts.recommendedCurrency.should.equal('GBP')
  506. done()
  507. }
  508. this.AuthorizationManager.promises.isUserSiteAdmin.resolves(false)
  509. this.SubscriptionController.plansPageLightDesign(this.req, this.res)
  510. })
  511. it('should accept override for admin', function (done) {
  512. this.res.render = (page, opts) => {
  513. opts.recommendedCurrency.should.equal('USD')
  514. done()
  515. }
  516. this.AuthorizationManager.promises.isUserSiteAdmin.resolves(true)
  517. this.SubscriptionController.plansPageLightDesign(this.req, this.res)
  518. })
  519. })
  520. describe('groupPlanModal data', function () {
  521. it('should pass local currency if valid', function (done) {
  522. this.res.render = (page, opts) => {
  523. page.should.equal('subscriptions/plans-light-design')
  524. opts.groupPlanModalDefaults.currency.should.equal('GBP')
  525. done()
  526. }
  527. this.GeoIpLookup.promises.getCurrencyCode.resolves({
  528. currencyCode: 'GBP',
  529. })
  530. this.SubscriptionController.plansPageLightDesign(this.req, this.res)
  531. })
  532. it('should fallback to USD when valid', function (done) {
  533. this.res.render = (page, opts) => {
  534. page.should.equal('subscriptions/plans-light-design')
  535. opts.groupPlanModalDefaults.currency.should.equal('USD')
  536. done()
  537. }
  538. this.GeoIpLookup.promises.getCurrencyCode.resolves({
  539. currencyCode: 'FOO',
  540. })
  541. this.SubscriptionController.plansPageLightDesign(this.req, this.res)
  542. })
  543. it('should pass valid options for group plan modal and discard invalid', function (done) {
  544. this.res.render = (page, opts) => {
  545. page.should.equal('subscriptions/plans-light-design')
  546. opts.groupPlanModalDefaults.size.should.equal('42')
  547. opts.groupPlanModalDefaults.plan_code.should.equal('collaborator')
  548. opts.groupPlanModalDefaults.currency.should.equal('GBP')
  549. opts.groupPlanModalDefaults.usage.should.equal('foo')
  550. done()
  551. }
  552. this.GeoIpLookup.isValidCurrencyParam.returns(false)
  553. this.req.query = {
  554. number: '42',
  555. currency: 'ABC',
  556. plan: 'does-not-exist',
  557. usage: 'foo',
  558. }
  559. this.SubscriptionController.plansPageLightDesign(this.req, this.res)
  560. })
  561. })
  562. describe('showInrGeoBanner data', function () {
  563. it('should return true for Indian Users', function (done) {
  564. this.res.render = (page, opts) => {
  565. page.should.equal('subscriptions/plans-light-design')
  566. opts.showInrGeoBanner.should.equal(true)
  567. done()
  568. }
  569. this.GeoIpLookup.promises.getCurrencyCode.resolves({
  570. countryCode: 'IN',
  571. })
  572. this.SubscriptionController.plansPageLightDesign(this.req, this.res)
  573. })
  574. it('should return false for US Users', function (done) {
  575. this.res.render = (page, opts) => {
  576. page.should.equal('subscriptions/plans-light-design')
  577. opts.showInrGeoBanner.should.equal(false)
  578. done()
  579. }
  580. this.GeoIpLookup.promises.getCurrencyCode.resolves({
  581. countryCode: 'US',
  582. })
  583. this.SubscriptionController.plansPageLightDesign(this.req, this.res)
  584. })
  585. })
  586. describe('showBrlGeoBanner data', function () {
  587. it('should return true for Brazilian users', function (done) {
  588. this.res.render = (page, opts) => {
  589. page.should.equal('subscriptions/plans-light-design')
  590. opts.showBrlGeoBanner.should.equal(true)
  591. done()
  592. }
  593. this.GeoIpLookup.promises.getCurrencyCode.resolves({
  594. countryCode: 'BR',
  595. })
  596. this.SubscriptionController.plansPageLightDesign(this.req, this.res)
  597. })
  598. it('should return false for US users', function (done) {
  599. this.res.render = (page, opts) => {
  600. page.should.equal('subscriptions/plans-light-design')
  601. opts.showBrlGeoBanner.should.equal(false)
  602. done()
  603. }
  604. this.GeoIpLookup.promises.getCurrencyCode.resolves({
  605. countryCode: 'US',
  606. })
  607. this.SubscriptionController.plansPageLightDesign(this.req, this.res)
  608. })
  609. })
  610. describe('showLATAMBanner', function () {
  611. it('should return true for Mexican users', function (done) {
  612. this.res.render = (page, opts) => {
  613. page.should.equal('subscriptions/plans-light-design')
  614. opts.showLATAMBanner.should.equal(true)
  615. done()
  616. }
  617. this.GeoIpLookup.promises.getCurrencyCode.resolves({
  618. countryCode: 'MX',
  619. })
  620. this.SubscriptionController.plansPageLightDesign(this.req, this.res)
  621. })
  622. it('should return true for Colombian users', function (done) {
  623. this.res.render = (page, opts) => {
  624. page.should.equal('subscriptions/plans-light-design')
  625. opts.showLATAMBanner.should.equal(true)
  626. done()
  627. }
  628. this.GeoIpLookup.promises.getCurrencyCode.resolves({
  629. countryCode: 'CO',
  630. })
  631. this.SubscriptionController.plansPageLightDesign(this.req, this.res)
  632. })
  633. it('should return true for Chilean users', function (done) {
  634. this.res.render = (page, opts) => {
  635. page.should.equal('subscriptions/plans-light-design')
  636. opts.showLATAMBanner.should.equal(true)
  637. done()
  638. }
  639. this.GeoIpLookup.promises.getCurrencyCode.resolves({
  640. countryCode: 'CL',
  641. })
  642. this.SubscriptionController.plansPageLightDesign(this.req, this.res)
  643. })
  644. it('should return true for Peruvian users', function (done) {
  645. this.res.render = (page, opts) => {
  646. page.should.equal('subscriptions/plans-light-design')
  647. opts.showLATAMBanner.should.equal(true)
  648. done()
  649. }
  650. this.GeoIpLookup.promises.getCurrencyCode.resolves({
  651. countryCode: 'PE',
  652. })
  653. this.SubscriptionController.plansPageLightDesign(this.req, this.res)
  654. })
  655. it('should return true for US users', function (done) {
  656. this.res.render = (page, opts) => {
  657. page.should.equal('subscriptions/plans-light-design')
  658. opts.showLATAMBanner.should.equal(false)
  659. done()
  660. }
  661. this.GeoIpLookup.promises.getCurrencyCode.resolves({
  662. countryCode: 'US',
  663. })
  664. this.SubscriptionController.plansPageLightDesign(this.req, this.res)
  665. })
  666. })
  667. describe('localCcyAssignment', function () {
  668. it('uses formatCurrencyLocalized when variant is enabled', function (done) {
  669. this.SplitTestV2Hander.promises.getAssignment
  670. .withArgs(this.req, this.res, 'local-ccy-format-v2')
  671. .resolves({
  672. variant: 'enabled',
  673. })
  674. this.res.render = (page, opts) => {
  675. expect(opts.formatCurrency).to.equal(
  676. this.currency.formatCurrencyLocalized
  677. )
  678. done()
  679. }
  680. this.SubscriptionController.plansPageLightDesign(this.req, this.res)
  681. })
  682. it('uses formatCurrencyDefault when variant is default', function (done) {
  683. this.res.render = (page, opts) => {
  684. expect(opts.formatCurrency).to.equal(
  685. this.SubscriptionHelper.formatCurrencyDefault
  686. )
  687. done()
  688. }
  689. this.SubscriptionController.plansPageLightDesign(this.req, this.res)
  690. })
  691. })
  692. it('should return correct countryCode', function (done) {
  693. this.GeoIpLookup.promises.getCurrencyCode.resolves({
  694. countryCode: 'MX',
  695. })
  696. this.res.render = (page, opts) => {
  697. expect(opts.countryCode).to.equal('MX')
  698. done()
  699. }
  700. this.SubscriptionController.plansPageLightDesign(this.req, this.res)
  701. })
  702. })
  703. describe('interstitialPaymentPage', function () {
  704. beforeEach(function () {
  705. this.req.ip = '1234.3123.3131.333 313.133.445.666 653.5345.5345.534'
  706. this.GeoIpLookup.promises.getCurrencyCode.resolves({
  707. currencyCode: this.stubbedCurrencyCode,
  708. })
  709. })
  710. describe('with a user without subscription', function () {
  711. it('should render the interstitial payment page', function (done) {
  712. this.res.render = (page, opts) => {
  713. page.should.equal('subscriptions/interstitial-payment')
  714. done()
  715. }
  716. this.SubscriptionController.interstitialPaymentPage(this.req, this.res)
  717. })
  718. })
  719. describe('with a user with subscription', function () {
  720. it('should redirect to the subscription dashboard', function (done) {
  721. this.LimitationsManager.promises.userHasSubscription.resolves({
  722. hasSubscription: true,
  723. })
  724. this.res.redirect = url => {
  725. url.should.equal('/user/subscription?hasSubscription=true')
  726. done()
  727. }
  728. this.SubscriptionController.interstitialPaymentPage(this.req, this.res)
  729. })
  730. })
  731. describe('showInrGeoBanner data', function () {
  732. it('should return true for Indian users', function (done) {
  733. this.res.render = (page, opts) => {
  734. page.should.equal('subscriptions/interstitial-payment')
  735. opts.showInrGeoBanner.should.equal(true)
  736. done()
  737. }
  738. this.GeoIpLookup.promises.getCurrencyCode.resolves({
  739. countryCode: 'IN',
  740. })
  741. this.SubscriptionController.interstitialPaymentPage(this.req, this.res)
  742. })
  743. it('should be false for US users', function (done) {
  744. this.GeoIpLookup.promises.getCurrencyCode.resolves({
  745. countryCode: 'US',
  746. })
  747. this.res.render = (page, opts) => {
  748. page.should.equal('subscriptions/interstitial-payment')
  749. opts.showInrGeoBanner.should.equal(false)
  750. done()
  751. }
  752. this.SubscriptionController.interstitialPaymentPage(this.req, this.res)
  753. })
  754. })
  755. describe('showBrlGeoBanner data', function () {
  756. it('should return true for Brazilian users', function (done) {
  757. this.res.render = (page, opts) => {
  758. page.should.equal('subscriptions/interstitial-payment')
  759. opts.showBrlGeoBanner.should.equal(true)
  760. done()
  761. }
  762. this.GeoIpLookup.promises.getCurrencyCode.resolves({
  763. countryCode: 'BR',
  764. })
  765. this.SubscriptionController.interstitialPaymentPage(this.req, this.res)
  766. })
  767. it('should return false for US users', function (done) {
  768. this.res.render = (page, opts) => {
  769. page.should.equal('subscriptions/interstitial-payment')
  770. opts.showBrlGeoBanner.should.equal(false)
  771. done()
  772. }
  773. this.GeoIpLookup.promises.getCurrencyCode.resolves({
  774. countryCode: 'US',
  775. })
  776. this.SubscriptionController.interstitialPaymentPage(this.req, this.res)
  777. })
  778. })
  779. describe('showLATAMBanner', function () {
  780. it('should return true for Mexican users', function (done) {
  781. this.res.render = (page, opts) => {
  782. page.should.equal('subscriptions/interstitial-payment')
  783. opts.showLATAMBanner.should.equal(true)
  784. done()
  785. }
  786. this.GeoIpLookup.promises.getCurrencyCode.resolves({
  787. countryCode: 'MX',
  788. })
  789. this.SubscriptionController.interstitialPaymentPage(this.req, this.res)
  790. })
  791. it('should return true for Colombian users', function (done) {
  792. this.res.render = (page, opts) => {
  793. page.should.equal('subscriptions/interstitial-payment')
  794. opts.showLATAMBanner.should.equal(true)
  795. done()
  796. }
  797. this.GeoIpLookup.promises.getCurrencyCode.resolves({
  798. countryCode: 'CO',
  799. })
  800. this.SubscriptionController.interstitialPaymentPage(this.req, this.res)
  801. })
  802. it('should return true for Chilean users', function (done) {
  803. this.res.render = (page, opts) => {
  804. page.should.equal('subscriptions/interstitial-payment')
  805. opts.showLATAMBanner.should.equal(true)
  806. done()
  807. }
  808. this.GeoIpLookup.promises.getCurrencyCode.resolves({
  809. countryCode: 'CL',
  810. })
  811. this.SubscriptionController.interstitialPaymentPage(this.req, this.res)
  812. })
  813. it('should return true for Peruvian users', function (done) {
  814. this.res.render = (page, opts) => {
  815. page.should.equal('subscriptions/interstitial-payment')
  816. opts.showLATAMBanner.should.equal(true)
  817. done()
  818. }
  819. this.GeoIpLookup.promises.getCurrencyCode.resolves({
  820. countryCode: 'PE',
  821. })
  822. this.SubscriptionController.interstitialPaymentPage(this.req, this.res)
  823. })
  824. it('should return true for US users', function (done) {
  825. this.res.render = (page, opts) => {
  826. page.should.equal('subscriptions/interstitial-payment')
  827. opts.showLATAMBanner.should.equal(false)
  828. done()
  829. }
  830. this.GeoIpLookup.promises.getCurrencyCode.resolves({
  831. countryCode: 'US',
  832. })
  833. this.SubscriptionController.interstitialPaymentPage(this.req, this.res)
  834. })
  835. })
  836. it('should return correct countryCode', function (done) {
  837. this.GeoIpLookup.promises.getCurrencyCode.resolves({
  838. countryCode: 'MX',
  839. })
  840. this.res.render = (page, opts) => {
  841. expect(opts.countryCode).to.equal('MX')
  842. done()
  843. }
  844. this.SubscriptionController.interstitialPaymentPage(this.req, this.res)
  845. })
  846. })
  847. describe('successfulSubscription', function () {
  848. it('without a personal subscription', function (done) {
  849. this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel.resolves(
  850. {}
  851. )
  852. this.res.redirect = url => {
  853. url.should.equal('/user/subscription/plans')
  854. done()
  855. }
  856. this.SubscriptionController.successfulSubscription(this.req, this.res)
  857. })
  858. it('with a personal subscription', function (done) {
  859. this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel.resolves(
  860. {
  861. personalSubscription: 'foo',
  862. }
  863. )
  864. this.res.render = (url, variables) => {
  865. url.should.equal('subscriptions/successful-subscription-react')
  866. assert.deepEqual(variables, {
  867. title: 'thank_you',
  868. personalSubscription: 'foo',
  869. postCheckoutRedirect: undefined,
  870. user: this.user,
  871. })
  872. done()
  873. }
  874. this.SubscriptionController.successfulSubscription(this.req, this.res)
  875. })
  876. it('with an error', function (done) {
  877. this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel.resolves(
  878. undefined
  879. )
  880. this.SubscriptionController.successfulSubscription(
  881. this.req,
  882. this.res,
  883. error => {
  884. assert.isNotNull(error)
  885. done()
  886. }
  887. )
  888. })
  889. })
  890. describe('userSubscriptionPage', function () {
  891. beforeEach(function (done) {
  892. this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel.resolves(
  893. {
  894. personalSubscription: (this.personalSubscription = {
  895. 'personal-subscription': 'mock',
  896. }),
  897. memberGroupSubscriptions: (this.memberGroupSubscriptions = {
  898. 'group-subscriptions': 'mock',
  899. }),
  900. }
  901. )
  902. this.SubscriptionViewModelBuilder.buildPlansList.returns(
  903. (this.plans = { plans: 'mock' })
  904. )
  905. this.SubscriptionViewModelBuilder.buildPlansListForSubscriptionDash.returns(
  906. {
  907. plans: this.plans,
  908. planCodesChangingAtTermEnd: [],
  909. }
  910. )
  911. this.LimitationsManager.promises.userHasSubscription.resolves({
  912. hasSubscription: false,
  913. })
  914. this.res.render = (view, data) => {
  915. this.data = data
  916. expect(view).to.equal('subscriptions/dashboard-react')
  917. done()
  918. }
  919. this.SubscriptionController.userSubscriptionPage(this.req, this.res, done)
  920. })
  921. it('should load the personal, groups and v1 subscriptions', function () {
  922. expect(this.data.personalSubscription).to.deep.equal(
  923. this.personalSubscription
  924. )
  925. expect(this.data.memberGroupSubscriptions).to.deep.equal(
  926. this.memberGroupSubscriptions
  927. )
  928. })
  929. it('should load the user', function () {
  930. expect(this.data.user).to.deep.equal(this.user)
  931. })
  932. it('should load the plans', function () {
  933. expect(this.data.plans).to.deep.equal(this.plans)
  934. })
  935. it('should load an empty list of groups with settings available', function () {
  936. expect(this.data.groupSettingsEnabledFor).to.deep.equal([])
  937. })
  938. })
  939. describe('updateSubscription via post', function () {
  940. beforeEach(function (done) {
  941. this.res = {
  942. redirect() {
  943. done()
  944. },
  945. }
  946. sinon.spy(this.res, 'redirect')
  947. this.plan_code = '1234'
  948. this.req.body.plan_code = this.plan_code
  949. this.SubscriptionController.updateSubscription(this.req, this.res)
  950. })
  951. it('should send the user and subscriptionId to the handler', function (done) {
  952. this.SubscriptionHandler.updateSubscription
  953. .calledWith(this.user, this.plan_code)
  954. .should.equal(true)
  955. done()
  956. })
  957. it('should redurect to the subscription page', function (done) {
  958. this.res.redirect.calledWith('/user/subscription').should.equal(true)
  959. done()
  960. })
  961. })
  962. describe('updateAccountEmailAddress via put', function () {
  963. it('should send the user and subscriptionId to RecurlyWrapper', function () {
  964. this.res.sendStatus = sinon.spy()
  965. this.SubscriptionController.updateAccountEmailAddress(this.req, this.res)
  966. this.RecurlyWrapper.updateAccountEmailAddress
  967. .calledWith(this.user._id, this.user.email)
  968. .should.equal(true)
  969. })
  970. it('should respond with 200', function () {
  971. this.res.sendStatus = sinon.spy()
  972. this.SubscriptionController.updateAccountEmailAddress(this.req, this.res)
  973. this.res.sendStatus.calledWith(200).should.equal(true)
  974. })
  975. it('should send the error to the next handler when updating recurly account email fails', function (done) {
  976. this.RecurlyWrapper.updateAccountEmailAddress.yields(new Error())
  977. this.next = sinon.spy(error => {
  978. expect(error).instanceOf(Error)
  979. done()
  980. })
  981. this.SubscriptionController.updateAccountEmailAddress(
  982. this.req,
  983. this.res,
  984. this.next
  985. )
  986. })
  987. })
  988. describe('reactivateSubscription', function () {
  989. describe('when the user has permission', function () {
  990. beforeEach(function (done) {
  991. this.res = {
  992. redirect() {
  993. done()
  994. },
  995. }
  996. this.req.assertPermission = sinon.stub()
  997. this.next = sinon.stub().callsFake(error => {
  998. done(error)
  999. })
  1000. sinon.spy(this.res, 'redirect')
  1001. this.SubscriptionController.reactivateSubscription(
  1002. this.req,
  1003. this.res,
  1004. this.next
  1005. )
  1006. })
  1007. it('should assert the user has permission to reactivate their subscription', function (done) {
  1008. this.req.assertPermission
  1009. .calledWith('reactivate-subscription')
  1010. .should.equal(true)
  1011. done()
  1012. })
  1013. it('should tell the handler to reactivate this user', function (done) {
  1014. this.SubscriptionHandler.reactivateSubscription
  1015. .calledWith(this.user)
  1016. .should.equal(true)
  1017. done()
  1018. })
  1019. it('should redurect to the subscription page', function (done) {
  1020. this.res.redirect.calledWith('/user/subscription').should.equal(true)
  1021. done()
  1022. })
  1023. })
  1024. describe('when the user does not have permission', function () {
  1025. beforeEach(function (done) {
  1026. this.res = {
  1027. redirect() {
  1028. done()
  1029. },
  1030. }
  1031. this.req.assertPermission = sinon.stub().throws()
  1032. this.next = sinon.stub().callsFake(() => {
  1033. done()
  1034. })
  1035. sinon.spy(this.res, 'redirect')
  1036. this.SubscriptionController.reactivateSubscription(
  1037. this.req,
  1038. this.res,
  1039. this.next
  1040. )
  1041. })
  1042. it('should not reactivate the user', function (done) {
  1043. this.req.assertPermission = sinon.stub().throws()
  1044. this.SubscriptionHandler.reactivateSubscription.called.should.equal(
  1045. false
  1046. )
  1047. done()
  1048. })
  1049. it('should call next with an error', function (done) {
  1050. this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
  1051. done()
  1052. })
  1053. })
  1054. })
  1055. describe('cancelSubscription', function () {
  1056. beforeEach(function (done) {
  1057. this.res = {
  1058. redirect() {
  1059. done()
  1060. },
  1061. }
  1062. sinon.spy(this.res, 'redirect')
  1063. this.SubscriptionController.cancelSubscription(this.req, this.res)
  1064. })
  1065. it('should tell the handler to cancel this user', function (done) {
  1066. this.SubscriptionHandler.cancelSubscription
  1067. .calledWith(this.user)
  1068. .should.equal(true)
  1069. done()
  1070. })
  1071. it('should redurect to the subscription page', function (done) {
  1072. this.res.redirect
  1073. .calledWith('/user/subscription/canceled')
  1074. .should.equal(true)
  1075. done()
  1076. })
  1077. })
  1078. describe('recurly callback', function () {
  1079. describe('with a sync subscription request', function () {
  1080. beforeEach(function (done) {
  1081. this.req = {
  1082. body: {
  1083. expired_subscription_notification: {
  1084. account: {
  1085. account_code: this.user._id,
  1086. },
  1087. subscription: {
  1088. uuid: this.activeRecurlySubscription.uuid,
  1089. plan: {
  1090. plan_code: 'collaborator',
  1091. state: 'active',
  1092. },
  1093. },
  1094. },
  1095. },
  1096. }
  1097. this.res = {
  1098. sendStatus() {
  1099. done()
  1100. },
  1101. }
  1102. sinon.spy(this.res, 'sendStatus')
  1103. this.SubscriptionController.recurlyCallback(this.req, this.res)
  1104. })
  1105. it('should tell the SubscriptionHandler to process the recurly callback', function (done) {
  1106. this.SubscriptionHandler.syncSubscription.called.should.equal(true)
  1107. done()
  1108. })
  1109. it('should send a 200', function (done) {
  1110. this.res.sendStatus.calledWith(200)
  1111. done()
  1112. })
  1113. })
  1114. describe('with a billing info updated request', function () {
  1115. beforeEach(function (done) {
  1116. this.req = {
  1117. body: {
  1118. billing_info_updated_notification: {
  1119. account: {
  1120. account_code: 'mock-account-code',
  1121. },
  1122. },
  1123. },
  1124. }
  1125. this.res = {
  1126. sendStatus() {
  1127. done()
  1128. },
  1129. }
  1130. sinon.spy(this.res, 'sendStatus')
  1131. this.SubscriptionController.recurlyCallback(this.req, this.res)
  1132. })
  1133. it('should call attemptPaypalInvoiceCollection', function (done) {
  1134. this.SubscriptionHandler.attemptPaypalInvoiceCollection
  1135. .calledWith('mock-account-code')
  1136. .should.equal(true)
  1137. done()
  1138. })
  1139. it('should send a 200', function (done) {
  1140. this.res.sendStatus.calledWith(200)
  1141. done()
  1142. })
  1143. })
  1144. describe('with a non-actionable request', function () {
  1145. beforeEach(function (done) {
  1146. this.user.id = this.activeRecurlySubscription.account.account_code
  1147. this.req = {
  1148. body: {
  1149. renewed_subscription_notification: {
  1150. account: {
  1151. account_code: this.user._id,
  1152. },
  1153. subscription: {
  1154. uuid: this.activeRecurlySubscription.uuid,
  1155. plan: {
  1156. plan_code: 'collaborator',
  1157. state: 'active',
  1158. },
  1159. },
  1160. },
  1161. },
  1162. }
  1163. this.res = {
  1164. sendStatus() {
  1165. done()
  1166. },
  1167. }
  1168. sinon.spy(this.res, 'sendStatus')
  1169. this.SubscriptionController.recurlyCallback(this.req, this.res)
  1170. })
  1171. it('should not call the subscriptionshandler', function () {
  1172. this.SubscriptionHandler.syncSubscription.called.should.equal(false)
  1173. this.SubscriptionHandler.attemptPaypalInvoiceCollection.called.should.equal(
  1174. false
  1175. )
  1176. })
  1177. it('should respond with a 200 status', function () {
  1178. this.res.sendStatus.calledWith(200)
  1179. })
  1180. })
  1181. })
  1182. })