RecurlyWrapperTests.js 46 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465
  1. const { expect } = require('chai')
  2. const sinon = require('sinon')
  3. const modulePath = '../../../../app/src/Features/Subscription/RecurlyWrapper'
  4. const SandboxedModule = require('sandboxed-module')
  5. const tk = require('timekeeper')
  6. const Errors = require('../../../../app/src/Features/Errors/Errors')
  7. const SubscriptionErrors = require('../../../../app/src/Features/Subscription/Errors')
  8. const { RequestFailedError } = require('@overleaf/fetch-utils')
  9. const fixtures = {
  10. 'subscriptions/44f83d7cba354d5b84812419f923ea96':
  11. '<?xml version="1.0" encoding="UTF-8"?>' +
  12. '<subscription href="https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96">' +
  13. ' <account href="https://api.recurly.com/v2/accounts/104"/>' +
  14. ' <plan href="https://api.recurly.com/v2/plans/gold">' +
  15. ' <plan_code>gold</plan_code>' +
  16. ' <name>Gold plan</name>' +
  17. ' </plan>' +
  18. ' <uuid>44f83d7cba354d5b84812419f923ea96</uuid>' +
  19. ' <state>active</state>' +
  20. ' <unit_amount_in_cents type="integer">800</unit_amount_in_cents>' +
  21. ' <currency>EUR</currency>' +
  22. ' <quantity type="integer">1</quantity>' +
  23. ' <activated_at type="datetime">2011-05-27T07:00:00Z</activated_at>' +
  24. ' <canceled_at nil="nil"></canceled_at>' +
  25. ' <expires_at nil="nil"></expires_at>' +
  26. ' <current_period_started_at type="datetime">2011-06-27T07:00:00Z</current_period_started_at>' +
  27. ' <current_period_ends_at type="datetime">2011-07-27T07:00:00Z</current_period_ends_at>' +
  28. ' <trial_started_at nil="nil"></trial_started_at>' +
  29. ' <trial_ends_at nil="nil"></trial_ends_at>' +
  30. ' <subscription_add_ons type="array">' +
  31. ' <subscription_add_on>' +
  32. ' <add_on_code>ipaddresses</add_on_code>' +
  33. ' <quantity>10</quantity>' +
  34. ' <unit_amount_in_cents>150</unit_amount_in_cents>' +
  35. ' </subscription_add_on>' +
  36. ' </subscription_add_ons>' +
  37. ' <a name="cancel" href="https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/cancel" method="put"/>' +
  38. ' <a name="terminate" href="https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/terminate" method="put"/>' +
  39. ' <a name="postpone" href="https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/postpone" method="put"/>' +
  40. '</subscription>',
  41. 'recurly_js/result/70db44b10f5f4b238669480c9903f6f5':
  42. '<?xml version="1.0" encoding="UTF-8"?>' +
  43. '<subscription href="https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96">' +
  44. ' <account href="https://api.recurly.com/v2/accounts/104"/>' +
  45. ' <plan href="https://api.recurly.com/v2/plans/gold">' +
  46. ' <plan_code>gold</plan_code>' +
  47. ' <name>Gold plan</name>' +
  48. ' </plan>' +
  49. ' <uuid>44f83d7cba354d5b84812419f923ea96</uuid>' +
  50. ' <state>active</state>' +
  51. ' <unit_amount_in_cents type="integer">800</unit_amount_in_cents>' +
  52. ' <currency>EUR</currency>' +
  53. ' <quantity type="integer">1</quantity>' +
  54. ' <activated_at type="datetime">2011-05-27T07:00:00Z</activated_at>' +
  55. ' <canceled_at nil="nil"></canceled_at>' +
  56. ' <expires_at nil="nil"></expires_at>' +
  57. ' <current_period_started_at type="datetime">2011-06-27T07:00:00Z</current_period_started_at>' +
  58. ' <current_period_ends_at type="datetime">2011-07-27T07:00:00Z</current_period_ends_at>' +
  59. ' <trial_started_at nil="nil"></trial_started_at>' +
  60. ' <trial_ends_at nil="nil"></trial_ends_at>' +
  61. ' <subscription_add_ons type="array">' +
  62. ' <subscription_add_on>' +
  63. ' <add_on_code>ipaddresses</add_on_code>' +
  64. ' <quantity>10</quantity>' +
  65. ' <unit_amount_in_cents>150</unit_amount_in_cents>' +
  66. ' </subscription_add_on>' +
  67. ' </subscription_add_ons>' +
  68. ' <a name="cancel" href="https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/cancel" method="put"/>' +
  69. ' <a name="terminate" href="https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/terminate" method="put"/>' +
  70. ' <a name="postpone" href="https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/postpone" method="put"/>' +
  71. '</subscription>',
  72. 'accounts/104':
  73. '<?xml version="1.0" encoding="UTF-8"?>' +
  74. '<account href="https://api.recurly.com/v2/accounts/104">' +
  75. ' <adjustments href="https://api.recurly.com/v2/accounts/1/adjustments"/>' +
  76. ' <billing_info href="https://api.recurly.com/v2/accounts/1/billing_info"/>' +
  77. ' <invoices href="https://api.recurly.com/v2/accounts/1/invoices"/>' +
  78. ' <redemption href="https://api.recurly.com/v2/accounts/1/redemption"/>' +
  79. ' <subscriptions href="https://api.recurly.com/v2/accounts/1/subscriptions"/>' +
  80. ' <transactions href="https://api.recurly.com/v2/accounts/1/transactions"/>' +
  81. ' <account_code>104</account_code>' +
  82. ' <state>active</state>' +
  83. ' <username nil="nil"></username>' +
  84. ' <email>verena@example.com</email>' +
  85. ' <first_name>Verena</first_name>' +
  86. ' <last_name>Example</last_name>' +
  87. ' <accept_language nil="nil"></accept_language>' +
  88. ' <hosted_login_token>a92468579e9c4231a6c0031c4716c01d</hosted_login_token>' +
  89. ' <created_at type="datetime">2011-10-25T12:00:00</created_at>' +
  90. '</account>',
  91. }
  92. const mockApiRequest = function (options) {
  93. if (fixtures[options.url]) {
  94. return {
  95. err: null,
  96. response: { status: 200 },
  97. body: fixtures[options.url],
  98. }
  99. } else {
  100. return {
  101. err: new Error('Not found'),
  102. }
  103. }
  104. }
  105. describe('RecurlyWrapper', function () {
  106. beforeEach(function () {
  107. tk.freeze(Date.now()) // freeze the time for these tests
  108. this.settings = {
  109. plans: [
  110. {
  111. planCode: 'collaborator',
  112. name: 'Collaborator',
  113. features: {
  114. collaborators: -1,
  115. versioning: true,
  116. },
  117. },
  118. ],
  119. defaultPlanCode: {
  120. collaborators: 0,
  121. versioning: false,
  122. },
  123. apis: {
  124. recurly: {
  125. apiKey: 'nonsense',
  126. privateKey: 'private_nonsense',
  127. },
  128. },
  129. }
  130. this.fetchUtils = {
  131. fetchStringWithResponse: sinon.stub(),
  132. RequestFailedError,
  133. }
  134. this.RecurlyWrapper = SandboxedModule.require(modulePath, {
  135. requires: {
  136. '@overleaf/settings': this.settings,
  137. '@overleaf/fetch-utils': this.fetchUtils,
  138. './Errors': SubscriptionErrors,
  139. },
  140. })
  141. })
  142. afterEach(function () {
  143. tk.reset()
  144. })
  145. describe('getSubscription', function () {
  146. for (const functionType of ['promise', 'callback']) {
  147. describe(`as ${functionType}`, function () {
  148. beforeEach(function () {
  149. this.recurlySubscription = 'RESET'
  150. this.getSubscription = (...params) => {
  151. if (functionType === 'promise') {
  152. return this.RecurlyWrapper.promises.getSubscription(...params)
  153. }
  154. if (functionType === 'callback') {
  155. return new Promise((resolve, reject) =>
  156. this.RecurlyWrapper.getSubscription(
  157. ...params,
  158. (err, subscription) =>
  159. err ? reject(err) : resolve(subscription)
  160. )
  161. )
  162. }
  163. throw Error('Invalid function type')
  164. }
  165. })
  166. describe('with proper subscription id', function () {
  167. beforeEach(async function () {
  168. this.apiRequest = sinon
  169. .stub(this.RecurlyWrapper.promises, 'apiRequest')
  170. .callsFake(mockApiRequest)
  171. this.recurlySubscription = await this.getSubscription(
  172. '44f83d7cba354d5b84812419f923ea96'
  173. )
  174. })
  175. afterEach(function () {
  176. this.RecurlyWrapper.promises.apiRequest.restore()
  177. })
  178. it('should look up the subscription at the normal API end point', function () {
  179. this.apiRequest.args[0][0].url.should.equal(
  180. 'subscriptions/44f83d7cba354d5b84812419f923ea96'
  181. )
  182. })
  183. it('should return the subscription', function () {
  184. this.recurlySubscription.uuid.should.equal(
  185. '44f83d7cba354d5b84812419f923ea96'
  186. )
  187. })
  188. })
  189. describe('with RecurlyJS token', function () {
  190. beforeEach(async function () {
  191. this.apiRequest = sinon
  192. .stub(this.RecurlyWrapper.promises, 'apiRequest')
  193. .callsFake(mockApiRequest)
  194. this.recurlySubscription = await this.getSubscription(
  195. '70db44b10f5f4b238669480c9903f6f5',
  196. { recurlyJsResult: true }
  197. )
  198. })
  199. afterEach(function () {
  200. this.RecurlyWrapper.promises.apiRequest.restore()
  201. })
  202. it('should return the subscription', function () {
  203. this.recurlySubscription.uuid.should.equal(
  204. '44f83d7cba354d5b84812419f923ea96'
  205. )
  206. })
  207. it('should look up the subscription at the RecurlyJS API end point', function () {
  208. this.apiRequest.args[0][0].url.should.equal(
  209. 'recurly_js/result/70db44b10f5f4b238669480c9903f6f5'
  210. )
  211. })
  212. })
  213. describe('with includeAccount', function () {
  214. beforeEach(async function () {
  215. this.apiRequest = sinon
  216. .stub(this.RecurlyWrapper.promises, 'apiRequest')
  217. .callsFake(mockApiRequest)
  218. this.recurlySubscription = await this.getSubscription(
  219. '44f83d7cba354d5b84812419f923ea96',
  220. { includeAccount: true }
  221. )
  222. })
  223. afterEach(function () {
  224. this.RecurlyWrapper.promises.apiRequest.restore()
  225. })
  226. it('should request the account from the API', function () {
  227. this.apiRequest.args[1][0].url.should.equal('accounts/104')
  228. })
  229. it('should populate the account attribute', function () {
  230. this.recurlySubscription.account.account_code.should.equal('104')
  231. })
  232. })
  233. })
  234. }
  235. })
  236. describe('updateAccountEmailAddress', function () {
  237. beforeEach(async function () {
  238. this.recurlyAccountId = 'account-id-123'
  239. this.newEmail = 'example@overleaf.com'
  240. this.apiRequest = sinon
  241. .stub(this.RecurlyWrapper.promises, 'apiRequest')
  242. .callsFake(options => {
  243. this.requestOptions = options
  244. return {
  245. err: null,
  246. response: {},
  247. body: fixtures['accounts/104'],
  248. }
  249. })
  250. this.recurlyAccount =
  251. await this.RecurlyWrapper.promises.updateAccountEmailAddress(
  252. this.recurlyAccountId,
  253. this.newEmail
  254. )
  255. })
  256. afterEach(function () {
  257. this.RecurlyWrapper.promises.apiRequest.restore()
  258. })
  259. it('sends correct XML', function () {
  260. this.apiRequest.called.should.equal(true)
  261. const { body } = this.apiRequest.lastCall.args[0]
  262. expect(body).to.equal(`\
  263. <account>
  264. <email>example@overleaf.com</email>
  265. </account>\
  266. `)
  267. this.requestOptions.url.should.equal(`accounts/${this.recurlyAccountId}`)
  268. this.requestOptions.method.should.equal('PUT')
  269. })
  270. it('should return the updated account', function () {
  271. expect(this.recurlyAccount).to.exist
  272. this.recurlyAccount.account_code.should.equal('104')
  273. })
  274. })
  275. describe('updateAccountEmailAddress, with invalid XML', function () {
  276. beforeEach(async function (done) {
  277. this.recurlyAccountId = 'account-id-123'
  278. this.newEmail = '\uD800@example.com'
  279. this.apiRequest = sinon
  280. .stub(this.RecurlyWrapper.promises, 'apiRequest')
  281. .callsFake(options => {
  282. this.requestOptions = options
  283. return {
  284. err: null,
  285. response: {},
  286. body: fixtures['accounts/104'],
  287. }
  288. })
  289. done()
  290. })
  291. afterEach(function () {
  292. this.RecurlyWrapper.promises.apiRequest.restore()
  293. })
  294. it('should produce an error', function (done) {
  295. this.RecurlyWrapper.promises
  296. .updateAccountEmailAddress(this.recurlyAccountId, this.newEmail)
  297. .catch(error => {
  298. expect(error).to.exist
  299. expect(error.message.startsWith('Invalid character')).to.equal(true)
  300. expect(this.apiRequest.called).to.equal(false)
  301. done()
  302. })
  303. })
  304. })
  305. describe('updateSubscription', function () {
  306. beforeEach(async function () {
  307. this.recurlySubscriptionId = 'subscription-id-123'
  308. this.apiRequest = sinon
  309. .stub(this.RecurlyWrapper.promises, 'apiRequest')
  310. .callsFake(options => {
  311. this.requestOptions = options
  312. return {
  313. error: null,
  314. response: {},
  315. body: fixtures['subscriptions/44f83d7cba354d5b84812419f923ea96'],
  316. }
  317. })
  318. this.recurlySubscription =
  319. await this.RecurlyWrapper.promises.updateSubscription(
  320. this.recurlySubscriptionId,
  321. { plan_code: 'silver', timeframe: 'now' }
  322. )
  323. })
  324. afterEach(function () {
  325. this.RecurlyWrapper.promises.apiRequest.restore()
  326. })
  327. it('sends correct XML', function () {
  328. this.apiRequest.called.should.equal(true)
  329. const { body } = this.apiRequest.lastCall.args[0]
  330. expect(body).to.equal(`\
  331. <subscription>
  332. <plan_code>silver</plan_code>
  333. <timeframe>now</timeframe>
  334. </subscription>\
  335. `)
  336. this.requestOptions.url.should.equal(
  337. `subscriptions/${this.recurlySubscriptionId}`
  338. )
  339. this.requestOptions.method.should.equal('PUT')
  340. })
  341. it('should return the updated subscription', function () {
  342. expect(this.recurlySubscription).to.exist
  343. this.recurlySubscription.plan.plan_code.should.equal('gold')
  344. })
  345. })
  346. describe('redeemCoupon', function () {
  347. beforeEach(async function () {
  348. this.recurlyAccountId = 'account-id-123'
  349. this.coupon_code = '312321312'
  350. this.apiRequest = sinon
  351. .stub(this.RecurlyWrapper.promises, 'apiRequest')
  352. .callsFake(options => {
  353. options.url.should.equal(`coupons/${this.coupon_code}/redeem`)
  354. options.method.should.equal('POST')
  355. return {}
  356. })
  357. await this.RecurlyWrapper.promises.redeemCoupon(
  358. this.recurlyAccountId,
  359. this.coupon_code
  360. )
  361. })
  362. afterEach(function () {
  363. this.RecurlyWrapper.promises.apiRequest.restore()
  364. })
  365. it('sends correct XML', function () {
  366. this.apiRequest.called.should.equal(true)
  367. const { body } = this.apiRequest.lastCall.args[0]
  368. expect(body).to.equal(`\
  369. <redemption>
  370. <account_code>account-id-123</account_code>
  371. <currency>USD</currency>
  372. </redemption>\
  373. `)
  374. })
  375. })
  376. describe('createFixedAmountCoupon', function () {
  377. beforeEach(async function () {
  378. this.couponCode = 'a-coupon-code'
  379. this.couponName = 'a-coupon-name'
  380. this.currencyCode = 'EUR'
  381. this.discount = 1337
  382. this.planCode = 'a-plan-code'
  383. this.apiRequest = sinon
  384. .stub(this.RecurlyWrapper.promises, 'apiRequest')
  385. .resolves()
  386. await this.RecurlyWrapper.promises.createFixedAmountCoupon(
  387. this.couponCode,
  388. this.couponName,
  389. this.currencyCode,
  390. this.discount,
  391. this.planCode
  392. )
  393. })
  394. afterEach(function () {
  395. this.RecurlyWrapper.promises.apiRequest.restore()
  396. })
  397. it('sends correct XML', function () {
  398. this.apiRequest.called.should.equal(true)
  399. const { body } = this.apiRequest.lastCall.args[0]
  400. expect(body).to.equal(`\
  401. <coupon>
  402. <coupon_code>a-coupon-code</coupon_code>
  403. <name>a-coupon-name</name>
  404. <discount_type>dollars</discount_type>
  405. <discount_in_cents>
  406. <EUR>1337</EUR>
  407. </discount_in_cents>
  408. <plan_codes>
  409. <plan_code>a-plan-code</plan_code>
  410. </plan_codes>
  411. <applies_to_all_plans>false</applies_to_all_plans>
  412. </coupon>\
  413. `)
  414. })
  415. })
  416. describe('createSubscription', function () {
  417. beforeEach(function () {
  418. this.user = {
  419. _id: 'some_id',
  420. email: 'user@example.com',
  421. }
  422. this.subscriptionDetails = {
  423. currencyCode: 'EUR',
  424. plan_code: 'some_plan_code',
  425. coupon_code: '',
  426. isPaypal: true,
  427. address: {
  428. address1: 'addr_one',
  429. address2: 'addr_two',
  430. country: 'some_country',
  431. state: 'some_state',
  432. zip: 'some_zip',
  433. },
  434. }
  435. this.subscription = {}
  436. this.recurlyTokenIds = {
  437. billing: 'a-token-id',
  438. threeDSecureActionResult: 'a-3d-token-id',
  439. }
  440. this.call = () => {
  441. return this.RecurlyWrapper.promises.createSubscription(
  442. this.user,
  443. this.subscriptionDetails,
  444. this.recurlyTokenIds
  445. )
  446. }
  447. })
  448. describe('when paypal', function () {
  449. beforeEach(function () {
  450. this.subscriptionDetails.isPaypal = true
  451. this._createPaypalSubscription = sinon.stub(
  452. this.RecurlyWrapper.promises,
  453. '_createPaypalSubscription'
  454. )
  455. this._createPaypalSubscription.resolves(this.subscription)
  456. })
  457. afterEach(function () {
  458. this._createPaypalSubscription.restore()
  459. })
  460. it('should not produce an error', async function () {
  461. await expect(this.call()).to.be.fulfilled
  462. })
  463. it('should produce a subscription object', async function () {
  464. const sub = await this.call()
  465. expect(sub).to.deep.equal(this.subscription)
  466. })
  467. it('should call _createPaypalSubscription', async function () {
  468. await this.call()
  469. this._createPaypalSubscription.callCount.should.equal(1)
  470. })
  471. describe('when _createPaypalSubscription produces an error', function () {
  472. beforeEach(function () {
  473. this._createPaypalSubscription.rejects(new Error('woops'))
  474. })
  475. it('should produce an error', async function () {
  476. await expect(this.call()).to.be.rejectedWith('woops')
  477. })
  478. })
  479. })
  480. describe('when not paypal', function () {
  481. beforeEach(function () {
  482. this.subscriptionDetails.isPaypal = false
  483. this._createCreditCardSubscription = sinon.stub(
  484. this.RecurlyWrapper.promises,
  485. '_createCreditCardSubscription'
  486. )
  487. this._createCreditCardSubscription.resolves(this.subscription)
  488. })
  489. afterEach(function () {
  490. this._createCreditCardSubscription.restore()
  491. })
  492. it('should not produce an error', async function () {
  493. await this.call()
  494. })
  495. it('should produce a subscription object', async function () {
  496. const sub = await this.call()
  497. expect(sub).to.deep.equal(this.subscription)
  498. })
  499. it('should call _createCreditCardSubscription', async function () {
  500. await this.call()
  501. this._createCreditCardSubscription.callCount.should.equal(1)
  502. })
  503. describe('when _createCreditCardSubscription produces an error', function () {
  504. beforeEach(function () {
  505. this._createCreditCardSubscription.rejects(new Error('woops'))
  506. })
  507. it('should produce an error', async function () {
  508. await expect(this.call()).to.be.rejectedWith('woops')
  509. })
  510. })
  511. })
  512. })
  513. describe('_createCreditCardSubscription', function () {
  514. beforeEach(function () {
  515. this.user = {
  516. _id: 'some_id',
  517. email: 'user@example.com',
  518. first_name: 'Foo',
  519. last_name: 'Johnson',
  520. }
  521. this.subscriptionDetails = {
  522. currencyCode: 'EUR',
  523. plan_code: 'some_plan_code',
  524. coupon_code: '',
  525. isPaypal: true,
  526. first_name: 'Prairie',
  527. address: {
  528. address1: 'addr_one',
  529. address2: 'addr_two',
  530. country: 'some_country',
  531. state: 'some_state',
  532. zip: 'some_zip',
  533. },
  534. subscription_add_ons: [
  535. { subscription_add_on: { add_on_code: 'test_add_on', quantity: 2 } },
  536. ],
  537. ITMCampaign: 'itm-campaign-value',
  538. ITMContent: 'itm-content-value',
  539. ITMReferrer: 'itm-referrer-value',
  540. }
  541. this.subscription = {}
  542. this.recurlyTokenIds = {
  543. billing: 'a-token-id',
  544. threeDSecureActionResult: 'a-3d-token-id',
  545. }
  546. this.apiRequest = sinon.stub(this.RecurlyWrapper.promises, 'apiRequest')
  547. this.response = { status: 200 }
  548. this.body = '<xml>is_bad</xml>'
  549. this.apiRequest.resolves({
  550. response: this.response,
  551. body: this.body,
  552. })
  553. this._parseSubscriptionXml = sinon.stub(
  554. this.RecurlyWrapper.promises,
  555. '_parseSubscriptionXml'
  556. )
  557. this._parseSubscriptionXml.resolves(this.subscription)
  558. this.call = () => {
  559. return this.RecurlyWrapper.promises._createCreditCardSubscription(
  560. this.user,
  561. this.subscriptionDetails,
  562. this.recurlyTokenIds
  563. )
  564. }
  565. })
  566. afterEach(function () {
  567. this.apiRequest.restore()
  568. this._parseSubscriptionXml.restore()
  569. })
  570. it('sends correct XML', async function () {
  571. await this.call()
  572. const { body } = this.apiRequest.lastCall.args[0]
  573. expect(body).to.equal(`\
  574. <subscription>
  575. <plan_code>some_plan_code</plan_code>
  576. <currency>EUR</currency>
  577. <coupon_code/>
  578. <account>
  579. <account_code>some_id</account_code>
  580. <email>user@example.com</email>
  581. <first_name>Prairie</first_name>
  582. <last_name>Johnson</last_name>
  583. <billing_info>
  584. <token_id>a-token-id</token_id>
  585. <three_d_secure_action_result_token_id>a-3d-token-id</three_d_secure_action_result_token_id>
  586. </billing_info>
  587. </account>
  588. <subscription_add_ons>
  589. <subscription_add_on>
  590. <add_on_code>test_add_on</add_on_code>
  591. <quantity>2</quantity>
  592. </subscription_add_on>
  593. </subscription_add_ons>
  594. <custom_fields>
  595. <custom_field>
  596. <name>itm_campaign</name>
  597. <value>itm-campaign-value</value>
  598. </custom_field>
  599. <custom_field>
  600. <name>itm_content</name>
  601. <value>itm-content-value</value>
  602. </custom_field>
  603. <custom_field>
  604. <name>itm_referrer</name>
  605. <value>itm-referrer-value</value>
  606. </custom_field>
  607. </custom_fields>
  608. </subscription>\
  609. `)
  610. })
  611. it('should not produce an error', async function () {
  612. await expect(this.call()).to.be.fulfilled
  613. })
  614. it('should produce a subscription', async function () {
  615. const sub = await this.call()
  616. expect(sub).to.equal(this.subscription)
  617. })
  618. it('should call apiRequest', async function () {
  619. await this.call()
  620. this.apiRequest.callCount.should.equal(1)
  621. })
  622. it('should call _parseSubscriptionXml', async function () {
  623. await this.call()
  624. this._parseSubscriptionXml.callCount.should.equal(1)
  625. })
  626. describe('when api request returns 422', function () {
  627. beforeEach(function () {
  628. const body = `\
  629. <?xml version="1.0" encoding="UTF-8"?>
  630. <errors>
  631. <transaction_error>
  632. <error_code>three_d_secure_action_required</error_code>
  633. <error_category>3d_secure_action_required</error_category>
  634. <merchant_message>Your payment gateway is requesting that the transaction be completed with 3D Secure in accordance with PSD2.</merchant_message>
  635. <customer_message>Your card must be authenticated with 3D Secure before continuing.</customer_message>
  636. <gateway_error_code nil="nil"></gateway_error_code>
  637. <three_d_secure_action_token_id>mock_three_d_secure_action_token</three_d_secure_action_token_id>
  638. </transaction_error>
  639. <error field="subscription.account.base" symbol="three_d_secure_action_required">Your card must be authenticated with 3D Secure before continuing.</error>
  640. </errors>
  641. `
  642. // this.apiRequest.yields(null, { statusCode: 422 }, body)
  643. this.apiRequest.resolves({
  644. response: { status: 422 },
  645. body,
  646. })
  647. })
  648. it('should produce an error', function (done) {
  649. this.call().catch(err => {
  650. expect(err).to.be.instanceof(
  651. SubscriptionErrors.RecurlyTransactionError
  652. )
  653. expect(err.info.public.message).to.be.equal(
  654. 'Your card must be authenticated with 3D Secure before continuing.'
  655. )
  656. expect(err.info.public.threeDSecureActionTokenId).to.be.equal(
  657. 'mock_three_d_secure_action_token'
  658. )
  659. done()
  660. })
  661. })
  662. })
  663. describe('when api request produces an error', function () {
  664. beforeEach(function () {
  665. this.apiRequest.rejects(new Error('woops'))
  666. })
  667. it('should produce an error', async function () {
  668. await expect(this.call()).to.be.rejectedWith('woops')
  669. })
  670. it('should call apiRequest', async function () {
  671. await expect(this.call()).to.be.rejected
  672. this.apiRequest.callCount.should.equal(1)
  673. })
  674. it('should not _parseSubscriptionXml', async function () {
  675. await expect(this.call()).to.be.rejected
  676. this._parseSubscriptionXml.callCount.should.equal(0)
  677. })
  678. })
  679. describe('when parse xml produces an error', function () {
  680. beforeEach(function () {
  681. this._parseSubscriptionXml.rejects(new Error('woops xml'))
  682. })
  683. it('should produce an error', async function () {
  684. await expect(this.call()).to.be.rejectedWith('woops xml')
  685. })
  686. })
  687. })
  688. describe('_createPaypalSubscription', function () {
  689. beforeEach(function () {
  690. this.checkAccountExists = sinon.stub(
  691. this.RecurlyWrapper.promises._paypal,
  692. 'checkAccountExists'
  693. )
  694. this.createAccount = sinon.stub(
  695. this.RecurlyWrapper.promises._paypal,
  696. 'createAccount'
  697. )
  698. this.createBillingInfo = sinon.stub(
  699. this.RecurlyWrapper.promises._paypal,
  700. 'createBillingInfo'
  701. )
  702. this.setAddressAndCompanyBillingInfo = sinon.stub(
  703. this.RecurlyWrapper.promises._paypal,
  704. 'setAddressAndCompanyBillingInfo'
  705. )
  706. this.createSubscription = sinon.stub(
  707. this.RecurlyWrapper.promises._paypal,
  708. 'createSubscription'
  709. )
  710. this.user = {
  711. _id: 'some_id',
  712. email: 'user@example.com',
  713. }
  714. this.subscriptionDetails = {
  715. currencyCode: 'EUR',
  716. plan_code: 'some_plan_code',
  717. coupon_code: '',
  718. isPaypal: true,
  719. address: {
  720. address1: 'addr_one',
  721. address2: 'addr_two',
  722. country: 'some_country',
  723. state: 'some_state',
  724. zip: 'some_zip',
  725. },
  726. }
  727. this.subscription = {}
  728. this.recurlyTokenIds = {
  729. billing: 'a-token-id',
  730. threeDSecureActionResult: 'a-3d-token-id',
  731. }
  732. // set up data callbacks
  733. const { user } = this
  734. const { subscriptionDetails } = this
  735. const { recurlyTokenIds } = this
  736. this.checkAccountExists.resolves({
  737. user,
  738. subscriptionDetails,
  739. recurlyTokenIds,
  740. userExists: false,
  741. account: { accountCode: 'xx' },
  742. })
  743. this.createAccount.resolves({
  744. user,
  745. subscriptionDetails,
  746. recurlyTokenIds,
  747. userExists: false,
  748. account: { accountCode: 'xx' },
  749. })
  750. this.createBillingInfo.resolves({
  751. user,
  752. subscriptionDetails,
  753. recurlyTokenIds,
  754. userExists: false,
  755. account: { accountCode: 'xx' },
  756. billingInfo: { token_id: 'abc' },
  757. })
  758. this.setAddressAndCompanyBillingInfo.resolves({
  759. user,
  760. subscriptionDetails,
  761. recurlyTokenIds,
  762. userExists: false,
  763. account: { accountCode: 'xx' },
  764. billingInfo: { token_id: 'abc' },
  765. })
  766. this.createSubscription.resolves({
  767. user,
  768. subscriptionDetails,
  769. recurlyTokenIds,
  770. userExists: false,
  771. account: { accountCode: 'xx' },
  772. billingInfo: { token_id: 'abc' },
  773. subscription: this.subscription,
  774. })
  775. this.call = () => {
  776. return this.RecurlyWrapper.promises._createPaypalSubscription(
  777. this.user,
  778. this.subscriptionDetails,
  779. this.recurlyTokenIds
  780. )
  781. }
  782. })
  783. afterEach(function () {
  784. this.checkAccountExists.restore()
  785. this.createAccount.restore()
  786. this.createBillingInfo.restore()
  787. this.setAddressAndCompanyBillingInfo.restore()
  788. this.createSubscription.restore()
  789. })
  790. it('should not produce an error', async function () {
  791. await expect(this.call()).to.be.fulfilled
  792. })
  793. it('should produce a subscription object', async function () {
  794. const sub = await this.call()
  795. expect(sub).to.not.equal(null)
  796. expect(sub).to.equal(this.subscription)
  797. })
  798. it('should call each of the paypal stages', async function () {
  799. await this.call()
  800. this.checkAccountExists.callCount.should.equal(1)
  801. this.createAccount.callCount.should.equal(1)
  802. this.createBillingInfo.callCount.should.equal(1)
  803. this.setAddressAndCompanyBillingInfo.callCount.should.equal(1)
  804. this.createSubscription.callCount.should.equal(1)
  805. })
  806. describe('when one of the paypal stages produces an error', function () {
  807. beforeEach(function () {
  808. this.createAccount.rejects(new Error('woops'))
  809. })
  810. it('should produce an error', async function () {
  811. await expect(this.call()).to.be.rejectedWith('woops')
  812. })
  813. it('should stop calling the paypal stages after the error', async function () {
  814. await expect(this.call()).to.be.rejected
  815. this.checkAccountExists.callCount.should.equal(1)
  816. this.createAccount.callCount.should.equal(1)
  817. this.createBillingInfo.callCount.should.equal(0)
  818. this.setAddressAndCompanyBillingInfo.callCount.should.equal(0)
  819. this.createSubscription.callCount.should.equal(0)
  820. })
  821. })
  822. })
  823. describe('paypal actions', function () {
  824. beforeEach(function () {
  825. this.apiRequest = sinon.stub(this.RecurlyWrapper.promises, 'apiRequest')
  826. this._parseAccountXml = sinon.spy(
  827. this.RecurlyWrapper.promises,
  828. '_parseAccountXml'
  829. )
  830. this._parseBillingInfoXml = sinon.spy(
  831. this.RecurlyWrapper.promises,
  832. '_parseBillingInfoXml'
  833. )
  834. this._parseSubscriptionXml = sinon.spy(
  835. this.RecurlyWrapper.promises,
  836. '_parseSubscriptionXml'
  837. )
  838. this.cache = {
  839. user: (this.user = {
  840. _id: 'some_id',
  841. email: 'foo@bar.com',
  842. first_name: 'Foo',
  843. last_name: 'Bar',
  844. }),
  845. recurlyTokenIds: (this.recurlyTokenIds = {
  846. billing: 'a-token-id',
  847. threeDSecureActionResult: 'a-3d-token-id',
  848. }),
  849. subscriptionDetails: (this.subscriptionDetails = {
  850. currencyCode: 'EUR',
  851. plan_code: 'some_plan_code',
  852. coupon_code: '',
  853. isPaypal: true,
  854. address: {
  855. address1: 'addr_one',
  856. address2: 'addr_two',
  857. city: 'some_city',
  858. country: 'some_country',
  859. state: 'some_state',
  860. zip: 'some_zip',
  861. },
  862. ITMCampaign: 'itm-campaign-value',
  863. ITMContent: 'itm-content-value',
  864. }),
  865. }
  866. })
  867. afterEach(function () {
  868. this.apiRequest.restore()
  869. this._parseAccountXml.restore()
  870. this._parseBillingInfoXml.restore()
  871. this._parseSubscriptionXml.restore()
  872. })
  873. describe('_paypal.checkAccountExists', function () {
  874. beforeEach(function () {
  875. this.call = () => {
  876. return this.RecurlyWrapper.promises._paypal.checkAccountExists(
  877. this.cache
  878. )
  879. }
  880. })
  881. describe('when the account exists', function () {
  882. beforeEach(function () {
  883. const resultXml =
  884. '<account><account_code>abc</account_code></account>'
  885. this.apiRequest.resolves({
  886. response: { status: 200 },
  887. body: resultXml,
  888. })
  889. })
  890. it('should not produce an error', async function () {
  891. await expect(this.call()).to.be.fulfilled
  892. })
  893. it('should call apiRequest', async function () {
  894. await this.call()
  895. this.apiRequest.callCount.should.equal(1)
  896. })
  897. it('should call _parseAccountXml', async function () {
  898. await this.call()
  899. this.RecurlyWrapper.promises._parseAccountXml.callCount.should.equal(
  900. 1
  901. )
  902. })
  903. it('should add the account to the cumulative result', async function () {
  904. const result = await this.call()
  905. expect(result.account).to.not.equal(null)
  906. expect(result.account).to.not.equal(undefined)
  907. expect(result.account).to.deep.equal({
  908. account_code: 'abc',
  909. })
  910. })
  911. it('should set userExists to true', async function () {
  912. const result = await this.call()
  913. expect(result.userExists).to.equal(true)
  914. })
  915. })
  916. describe('when the account does not exist', function () {
  917. beforeEach(function () {
  918. this.apiRequest.resolves({
  919. response: { status: 404 },
  920. body: '',
  921. })
  922. })
  923. it('should not produce an error', async function () {
  924. await expect(this.call()).to.be.fulfilled
  925. })
  926. it('should call apiRequest', async function () {
  927. await this.call()
  928. this.apiRequest.callCount.should.equal(1)
  929. this.apiRequest.firstCall.args[0].method.should.equal('GET')
  930. })
  931. it('should not call _parseAccountXml', async function () {
  932. await this.call()
  933. this.RecurlyWrapper.promises._parseAccountXml.callCount.should.equal(
  934. 0
  935. )
  936. })
  937. it('should not add the account to result', async function () {
  938. const result = await this.call()
  939. expect(result.account).to.equal(undefined)
  940. })
  941. it('should set userExists to false', async function () {
  942. const result = await this.call()
  943. expect(result.userExists).to.equal(false)
  944. })
  945. })
  946. describe('when apiRequest produces an error', function () {
  947. beforeEach(function () {
  948. this.apiRequest.rejects(new Error('woops'))
  949. })
  950. it('should produce an error', async function () {
  951. await expect(this.call()).to.be.rejected
  952. })
  953. })
  954. })
  955. describe('_paypal.createAccount', function () {
  956. beforeEach(function () {
  957. this.call = () => {
  958. return this.RecurlyWrapper.promises._paypal.createAccount(this.cache)
  959. }
  960. })
  961. describe('when address is missing from subscriptionDetails', function () {
  962. beforeEach(function () {
  963. this.cache.subscriptionDetails.address = null
  964. })
  965. it('should produce an error', async function () {
  966. await expect(this.call()).to.be.rejected
  967. })
  968. })
  969. describe('when country is missing from address', function () {
  970. beforeEach(function () {
  971. this.cache.subscriptionDetails.address = {}
  972. })
  973. it('should produce an error', async function () {
  974. await expect(this.call()).to.be.rejectedWith(Errors.InvalidError)
  975. })
  976. })
  977. describe('when account already exists', function () {
  978. beforeEach(function () {
  979. this.cache.userExists = true
  980. this.cache.account = { account_code: 'abc' }
  981. })
  982. it('should not produce an error', async function () {
  983. await expect(this.call()).to.be.fulfilled
  984. })
  985. it('should produce cache object', async function () {
  986. const result = await this.call()
  987. expect(result).to.deep.equal(this.cache)
  988. expect(result.account).to.deep.equal({
  989. account_code: 'abc',
  990. })
  991. })
  992. it('should not call apiRequest', async function () {
  993. await expect(this.call()).to.be.fulfilled
  994. this.apiRequest.callCount.should.equal(0)
  995. })
  996. it('should not call _parseAccountXml', async function () {
  997. await this.call()
  998. this.RecurlyWrapper.promises._parseAccountXml.callCount.should.equal(
  999. 0
  1000. )
  1001. })
  1002. })
  1003. describe('when account does not exist', function () {
  1004. beforeEach(function () {
  1005. this.cache.userExists = false
  1006. const resultXml =
  1007. '<account><account_code>abc</account_code></account>'
  1008. this.apiRequest.resolves({
  1009. response: { status: 200 },
  1010. body: resultXml,
  1011. })
  1012. })
  1013. it('sends correct XML', async function () {
  1014. await this.call()
  1015. const { body } = this.apiRequest.lastCall.args[0]
  1016. expect(body).to.equal(`\
  1017. <account>
  1018. <account_code>some_id</account_code>
  1019. <email>foo@bar.com</email>
  1020. <first_name>Foo</first_name>
  1021. <last_name>Bar</last_name>
  1022. <address>
  1023. <address1>addr_one</address1>
  1024. <address2>addr_two</address2>
  1025. <city>some_city</city>
  1026. <state>some_state</state>
  1027. <zip>some_zip</zip>
  1028. <country>some_country</country>
  1029. </address>
  1030. </account>\
  1031. `)
  1032. })
  1033. it('should not produce an error', async function () {
  1034. await expect(this.call()).to.be.fulfilled
  1035. })
  1036. it('should call apiRequest', async function () {
  1037. await this.call()
  1038. this.apiRequest.callCount.should.equal(1)
  1039. this.apiRequest.firstCall.args[0].method.should.equal('POST')
  1040. })
  1041. it('should call _parseAccountXml', async function () {
  1042. await this.call()
  1043. this.RecurlyWrapper.promises._parseAccountXml.callCount.should.equal(
  1044. 1
  1045. )
  1046. })
  1047. describe('when apiRequest produces an error', function () {
  1048. beforeEach(function () {
  1049. this.apiRequest.rejects(new Error('woops'))
  1050. })
  1051. it('should produce an error', async function () {
  1052. await expect(this.call()).to.be.rejectedWith('woops')
  1053. })
  1054. })
  1055. })
  1056. })
  1057. describe('_paypal.createBillingInfo', function () {
  1058. beforeEach(function () {
  1059. this.cache.account = { account_code: 'abc' }
  1060. this.call = () => {
  1061. return this.RecurlyWrapper.promises._paypal.createBillingInfo(
  1062. this.cache
  1063. )
  1064. }
  1065. })
  1066. describe('when account_code is missing from cache', function () {
  1067. beforeEach(function () {
  1068. this.cache.account.account_code = null
  1069. })
  1070. it('should produce an error', async function () {
  1071. await expect(this.call()).to.be.rejected
  1072. })
  1073. })
  1074. describe('when all goes well', function () {
  1075. beforeEach(function () {
  1076. const resultXml = '<billing_info><a>1</a></billing_info>'
  1077. this.apiRequest.resolves({
  1078. response: { status: 200 },
  1079. body: resultXml,
  1080. })
  1081. })
  1082. it('sends correct XML', async function () {
  1083. await this.call()
  1084. const { body } = this.apiRequest.lastCall.args[0]
  1085. expect(body).to.equal(`\
  1086. <billing_info>
  1087. <token_id>a-token-id</token_id>
  1088. </billing_info>\
  1089. `)
  1090. })
  1091. it('should not produce an error', async function () {
  1092. await expect(this.call()).to.be.fulfilled
  1093. })
  1094. it('should call apiRequest', async function () {
  1095. await this.call()
  1096. this.apiRequest.callCount.should.equal(1)
  1097. this.apiRequest.firstCall.args[0].method.should.equal('POST')
  1098. })
  1099. it('should call _parseBillingInfoXml', async function () {
  1100. await this.call()
  1101. this.RecurlyWrapper.promises._parseBillingInfoXml.callCount.should.equal(
  1102. 1
  1103. )
  1104. })
  1105. it('should set billingInfo on cache', async function () {
  1106. const result = await this.call()
  1107. expect(result.billingInfo).to.deep.equal({
  1108. a: '1',
  1109. })
  1110. })
  1111. })
  1112. describe('when apiRequest produces an error', function () {
  1113. beforeEach(function () {
  1114. this.apiRequest.resolves(new Error('woops'))
  1115. })
  1116. it('should produce an error', async function () {
  1117. await expect(this.call()).to.be.rejected
  1118. })
  1119. })
  1120. })
  1121. describe('_paypal.setAddressAndCompanyBillingInfo', function () {
  1122. beforeEach(function () {
  1123. this.cache.account = { account_code: 'abc' }
  1124. this.cache.billingInfo = {}
  1125. this.call = () => {
  1126. return this.RecurlyWrapper.promises._paypal.setAddressAndCompanyBillingInfo(
  1127. this.cache
  1128. )
  1129. }
  1130. })
  1131. describe('when account_code is missing from cache', function () {
  1132. beforeEach(function () {
  1133. this.cache.account.account_code = null
  1134. })
  1135. it('should produce an error', async function () {
  1136. await expect(this.call()).to.be.rejected
  1137. })
  1138. })
  1139. describe('when country is missing', function () {
  1140. beforeEach(function () {
  1141. this.cache.subscriptionDetails.address = { country: '' }
  1142. })
  1143. it('should produce an error', async function () {
  1144. await expect(this.call()).to.be.rejectedWith(Errors.InvalidError)
  1145. })
  1146. })
  1147. describe('when all goes well', function () {
  1148. beforeEach(function () {
  1149. const resultXml = '<billing_info><city>London</city></billing_info>'
  1150. this.apiRequest.resolves({
  1151. response: { status: 200 },
  1152. body: resultXml,
  1153. })
  1154. })
  1155. it('sends correct XML', async function () {
  1156. await this.call()
  1157. const { body } = this.apiRequest.lastCall.args[0]
  1158. expect(body).to.equal(`\
  1159. <billing_info>
  1160. <address1>addr_one</address1>
  1161. <address2>addr_two</address2>
  1162. <city>some_city</city>
  1163. <state>some_state</state>
  1164. <zip>some_zip</zip>
  1165. <country>some_country</country>
  1166. </billing_info>\
  1167. `)
  1168. })
  1169. it('should not produce an error', async function () {
  1170. await expect(this.call()).to.be.fulfilled
  1171. })
  1172. it('should call apiRequest', async function () {
  1173. await this.call()
  1174. this.apiRequest.callCount.should.equal(1)
  1175. this.apiRequest.firstCall.args[0].method.should.equal('PUT')
  1176. })
  1177. it('should call _parseBillingInfoXml', async function () {
  1178. await this.call()
  1179. this.RecurlyWrapper.promises._parseBillingInfoXml.callCount.should.equal(
  1180. 1
  1181. )
  1182. })
  1183. it('should set billingInfo on cache', async function () {
  1184. const result = await this.call()
  1185. expect(result.billingInfo).to.deep.equal({
  1186. city: 'London',
  1187. })
  1188. })
  1189. })
  1190. describe('when apiRequest produces an error', function () {
  1191. beforeEach(function () {
  1192. this.apiRequest.rejects(new Error('woops'))
  1193. })
  1194. it('should produce an error', async function () {
  1195. await expect(this.call()).to.be.rejected
  1196. })
  1197. })
  1198. })
  1199. describe('_paypal.createSubscription', function () {
  1200. beforeEach(function () {
  1201. this.cache.account = { account_code: 'abc' }
  1202. this.cache.billingInfo = {}
  1203. this.call = () =>
  1204. this.RecurlyWrapper.promises._paypal.createSubscription(this.cache)
  1205. })
  1206. describe('when all goes well', function () {
  1207. beforeEach(function () {
  1208. const resultXml = '<subscription><a>1</a></subscription>'
  1209. this.apiRequest.resolves({
  1210. response: { status: 200 },
  1211. body: resultXml,
  1212. })
  1213. })
  1214. it('sends correct XML', async function () {
  1215. await this.call()
  1216. const { body } = this.apiRequest.lastCall.args[0]
  1217. expect(body).to.equal(`\
  1218. <subscription>
  1219. <plan_code>some_plan_code</plan_code>
  1220. <currency>EUR</currency>
  1221. <coupon_code/>
  1222. <account>
  1223. <account_code>some_id</account_code>
  1224. </account>
  1225. <custom_fields>
  1226. <custom_field>
  1227. <name>itm_campaign</name>
  1228. <value>itm-campaign-value</value>
  1229. </custom_field>
  1230. <custom_field>
  1231. <name>itm_content</name>
  1232. <value>itm-content-value</value>
  1233. </custom_field>
  1234. </custom_fields>
  1235. </subscription>\
  1236. `)
  1237. })
  1238. it('should not produce an error', async function () {
  1239. await expect(this.call()).to.be.fulfilled
  1240. })
  1241. it('should call apiRequest', async function () {
  1242. await this.call()
  1243. this.apiRequest.callCount.should.equal(1)
  1244. this.apiRequest.firstCall.args[0].method.should.equal('POST')
  1245. })
  1246. it('should call _parseSubscriptionXml', async function () {
  1247. await this.call()
  1248. this.RecurlyWrapper.promises._parseSubscriptionXml.callCount.should.equal(
  1249. 1
  1250. )
  1251. })
  1252. it('should set subscription on cache', async function () {
  1253. const result = await this.call()
  1254. expect(result.subscription).to.deep.equal({
  1255. a: '1',
  1256. })
  1257. })
  1258. })
  1259. describe('when apiRequest produces an error', function () {
  1260. beforeEach(function () {
  1261. this.apiRequest.rejects(new Error('woops'))
  1262. })
  1263. it('should produce an error', async function () {
  1264. await expect(this.call()).to.be.rejected
  1265. })
  1266. })
  1267. })
  1268. })
  1269. describe('listAccountActiveSubscriptions', function () {
  1270. beforeEach(function () {
  1271. this.user_id = 'mock-user-id'
  1272. this.response = { mock: 'response' }
  1273. this.body = '<mock body/>'
  1274. this.RecurlyWrapper.promises.apiRequest = sinon.stub().resolves({
  1275. response: this.response,
  1276. body: this.body,
  1277. })
  1278. this.subscriptions = ['mock', 'subscriptions']
  1279. this.RecurlyWrapper.promises._parseSubscriptionsXml = sinon
  1280. .stub()
  1281. .resolves(this.subscriptions)
  1282. })
  1283. describe('with an account', function () {
  1284. beforeEach(async function () {
  1285. this.result =
  1286. await this.RecurlyWrapper.promises.listAccountActiveSubscriptions(
  1287. this.user_id
  1288. )
  1289. })
  1290. it('should send a request to Recurly', async function () {
  1291. this.RecurlyWrapper.promises.apiRequest
  1292. .calledWith({
  1293. url: `accounts/${this.user_id}/subscriptions`,
  1294. qs: {
  1295. state: 'active',
  1296. },
  1297. expect404: true,
  1298. })
  1299. .should.equal(true)
  1300. })
  1301. it('should return the subscriptions', async function () {
  1302. expect(this.result).to.deep.equal(this.subscriptions)
  1303. })
  1304. })
  1305. describe('without an account', function () {
  1306. beforeEach(async function () {
  1307. this.response.status = 404
  1308. this.accountActiveSubscriptions =
  1309. await this.RecurlyWrapper.promises.listAccountActiveSubscriptions(
  1310. this.user_id
  1311. )
  1312. })
  1313. it('should return an empty array of subscriptions', function () {
  1314. expect(this.accountActiveSubscriptions).to.deep.equal([])
  1315. })
  1316. })
  1317. })
  1318. })