SubscriptionControllerTests.js 41 KB

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