RecurlyWrapperTests.js 46 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456
  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. this.settings = {
  108. plans: [
  109. {
  110. planCode: 'collaborator',
  111. name: 'Collaborator',
  112. features: {
  113. collaborators: -1,
  114. versioning: true,
  115. },
  116. },
  117. ],
  118. defaultPlanCode: {
  119. collaborators: 0,
  120. versioning: false,
  121. },
  122. apis: {
  123. recurly: {
  124. apiKey: 'nonsense',
  125. privateKey: 'private_nonsense',
  126. },
  127. },
  128. }
  129. this.fetchUtils = {
  130. fetchStringWithResponse: sinon.stub(),
  131. RequestFailedError,
  132. }
  133. tk.freeze(Date.now()) // freeze the time for these tests
  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. ITMCampaign: 'itm-campaign-value',
  535. ITMContent: 'itm-content-value',
  536. ITMReferrer: 'itm-referrer-value',
  537. }
  538. this.subscription = {}
  539. this.recurlyTokenIds = {
  540. billing: 'a-token-id',
  541. threeDSecureActionResult: 'a-3d-token-id',
  542. }
  543. this.apiRequest = sinon.stub(this.RecurlyWrapper.promises, 'apiRequest')
  544. this.response = { status: 200 }
  545. this.body = '<xml>is_bad</xml>'
  546. this.apiRequest.resolves({
  547. response: this.response,
  548. body: this.body,
  549. })
  550. this._parseSubscriptionXml = sinon.stub(
  551. this.RecurlyWrapper.promises,
  552. '_parseSubscriptionXml'
  553. )
  554. this._parseSubscriptionXml.resolves(this.subscription)
  555. this.call = () => {
  556. return this.RecurlyWrapper.promises._createCreditCardSubscription(
  557. this.user,
  558. this.subscriptionDetails,
  559. this.recurlyTokenIds
  560. )
  561. }
  562. })
  563. afterEach(function () {
  564. this.apiRequest.restore()
  565. this._parseSubscriptionXml.restore()
  566. })
  567. it('sends correct XML', async function () {
  568. await this.call()
  569. const { body } = this.apiRequest.lastCall.args[0]
  570. expect(body).to.equal(`\
  571. <subscription>
  572. <plan_code>some_plan_code</plan_code>
  573. <currency>EUR</currency>
  574. <coupon_code/>
  575. <account>
  576. <account_code>some_id</account_code>
  577. <email>user@example.com</email>
  578. <first_name>Prairie</first_name>
  579. <last_name>Johnson</last_name>
  580. <billing_info>
  581. <token_id>a-token-id</token_id>
  582. <three_d_secure_action_result_token_id>a-3d-token-id</three_d_secure_action_result_token_id>
  583. </billing_info>
  584. </account>
  585. <custom_fields>
  586. <custom_field>
  587. <name>itm_campaign</name>
  588. <value>itm-campaign-value</value>
  589. </custom_field>
  590. <custom_field>
  591. <name>itm_content</name>
  592. <value>itm-content-value</value>
  593. </custom_field>
  594. <custom_field>
  595. <name>itm_referrer</name>
  596. <value>itm-referrer-value</value>
  597. </custom_field>
  598. </custom_fields>
  599. </subscription>\
  600. `)
  601. })
  602. it('should not produce an error', async function () {
  603. await expect(this.call()).to.be.fulfilled
  604. })
  605. it('should produce a subscription', async function () {
  606. const sub = await this.call()
  607. expect(sub).to.equal(this.subscription)
  608. })
  609. it('should call apiRequest', async function () {
  610. await this.call()
  611. this.apiRequest.callCount.should.equal(1)
  612. })
  613. it('should call _parseSubscriptionXml', async function () {
  614. await this.call()
  615. this._parseSubscriptionXml.callCount.should.equal(1)
  616. })
  617. describe('when api request returns 422', function () {
  618. beforeEach(function () {
  619. const body = `\
  620. <?xml version="1.0" encoding="UTF-8"?>
  621. <errors>
  622. <transaction_error>
  623. <error_code>three_d_secure_action_required</error_code>
  624. <error_category>3d_secure_action_required</error_category>
  625. <merchant_message>Your payment gateway is requesting that the transaction be completed with 3D Secure in accordance with PSD2.</merchant_message>
  626. <customer_message>Your card must be authenticated with 3D Secure before continuing.</customer_message>
  627. <gateway_error_code nil="nil"></gateway_error_code>
  628. <three_d_secure_action_token_id>mock_three_d_secure_action_token</three_d_secure_action_token_id>
  629. </transaction_error>
  630. <error field="subscription.account.base" symbol="three_d_secure_action_required">Your card must be authenticated with 3D Secure before continuing.</error>
  631. </errors>
  632. `
  633. // this.apiRequest.yields(null, { statusCode: 422 }, body)
  634. this.apiRequest.resolves({
  635. response: { status: 422 },
  636. body,
  637. })
  638. })
  639. it('should produce an error', function (done) {
  640. this.call().catch(err => {
  641. expect(err).to.be.instanceof(
  642. SubscriptionErrors.RecurlyTransactionError
  643. )
  644. expect(err.info.public.message).to.be.equal(
  645. 'Your card must be authenticated with 3D Secure before continuing.'
  646. )
  647. expect(err.info.public.threeDSecureActionTokenId).to.be.equal(
  648. 'mock_three_d_secure_action_token'
  649. )
  650. done()
  651. })
  652. })
  653. })
  654. describe('when api request produces an error', function () {
  655. beforeEach(function () {
  656. this.apiRequest.rejects(new Error('woops'))
  657. })
  658. it('should produce an error', async function () {
  659. await expect(this.call()).to.be.rejectedWith('woops')
  660. })
  661. it('should call apiRequest', async function () {
  662. await expect(this.call()).to.be.rejected
  663. this.apiRequest.callCount.should.equal(1)
  664. })
  665. it('should not _parseSubscriptionXml', async function () {
  666. await expect(this.call()).to.be.rejected
  667. this._parseSubscriptionXml.callCount.should.equal(0)
  668. })
  669. })
  670. describe('when parse xml produces an error', function () {
  671. beforeEach(function () {
  672. this._parseSubscriptionXml.rejects(new Error('woops xml'))
  673. })
  674. it('should produce an error', async function () {
  675. await expect(this.call()).to.be.rejectedWith('woops xml')
  676. })
  677. })
  678. })
  679. describe('_createPaypalSubscription', function () {
  680. beforeEach(function () {
  681. this.checkAccountExists = sinon.stub(
  682. this.RecurlyWrapper.promises._paypal,
  683. 'checkAccountExists'
  684. )
  685. this.createAccount = sinon.stub(
  686. this.RecurlyWrapper.promises._paypal,
  687. 'createAccount'
  688. )
  689. this.createBillingInfo = sinon.stub(
  690. this.RecurlyWrapper.promises._paypal,
  691. 'createBillingInfo'
  692. )
  693. this.setAddressAndCompanyBillingInfo = sinon.stub(
  694. this.RecurlyWrapper.promises._paypal,
  695. 'setAddressAndCompanyBillingInfo'
  696. )
  697. this.createSubscription = sinon.stub(
  698. this.RecurlyWrapper.promises._paypal,
  699. 'createSubscription'
  700. )
  701. this.user = {
  702. _id: 'some_id',
  703. email: 'user@example.com',
  704. }
  705. this.subscriptionDetails = {
  706. currencyCode: 'EUR',
  707. plan_code: 'some_plan_code',
  708. coupon_code: '',
  709. isPaypal: true,
  710. address: {
  711. address1: 'addr_one',
  712. address2: 'addr_two',
  713. country: 'some_country',
  714. state: 'some_state',
  715. zip: 'some_zip',
  716. },
  717. }
  718. this.subscription = {}
  719. this.recurlyTokenIds = {
  720. billing: 'a-token-id',
  721. threeDSecureActionResult: 'a-3d-token-id',
  722. }
  723. // set up data callbacks
  724. const { user } = this
  725. const { subscriptionDetails } = this
  726. const { recurlyTokenIds } = this
  727. this.checkAccountExists.resolves({
  728. user,
  729. subscriptionDetails,
  730. recurlyTokenIds,
  731. userExists: false,
  732. account: { accountCode: 'xx' },
  733. })
  734. this.createAccount.resolves({
  735. user,
  736. subscriptionDetails,
  737. recurlyTokenIds,
  738. userExists: false,
  739. account: { accountCode: 'xx' },
  740. })
  741. this.createBillingInfo.resolves({
  742. user,
  743. subscriptionDetails,
  744. recurlyTokenIds,
  745. userExists: false,
  746. account: { accountCode: 'xx' },
  747. billingInfo: { token_id: 'abc' },
  748. })
  749. this.setAddressAndCompanyBillingInfo.resolves({
  750. user,
  751. subscriptionDetails,
  752. recurlyTokenIds,
  753. userExists: false,
  754. account: { accountCode: 'xx' },
  755. billingInfo: { token_id: 'abc' },
  756. })
  757. this.createSubscription.resolves({
  758. user,
  759. subscriptionDetails,
  760. recurlyTokenIds,
  761. userExists: false,
  762. account: { accountCode: 'xx' },
  763. billingInfo: { token_id: 'abc' },
  764. subscription: this.subscription,
  765. })
  766. this.call = () => {
  767. return this.RecurlyWrapper.promises._createPaypalSubscription(
  768. this.user,
  769. this.subscriptionDetails,
  770. this.recurlyTokenIds
  771. )
  772. }
  773. })
  774. afterEach(function () {
  775. this.checkAccountExists.restore()
  776. this.createAccount.restore()
  777. this.createBillingInfo.restore()
  778. this.setAddressAndCompanyBillingInfo.restore()
  779. this.createSubscription.restore()
  780. })
  781. it('should not produce an error', async function () {
  782. await expect(this.call()).to.be.fulfilled
  783. })
  784. it('should produce a subscription object', async function () {
  785. const sub = await this.call()
  786. expect(sub).to.not.equal(null)
  787. expect(sub).to.equal(this.subscription)
  788. })
  789. it('should call each of the paypal stages', async function () {
  790. await this.call()
  791. this.checkAccountExists.callCount.should.equal(1)
  792. this.createAccount.callCount.should.equal(1)
  793. this.createBillingInfo.callCount.should.equal(1)
  794. this.setAddressAndCompanyBillingInfo.callCount.should.equal(1)
  795. this.createSubscription.callCount.should.equal(1)
  796. })
  797. describe('when one of the paypal stages produces an error', function () {
  798. beforeEach(function () {
  799. this.createAccount.rejects(new Error('woops'))
  800. })
  801. it('should produce an error', async function () {
  802. await expect(this.call()).to.be.rejectedWith('woops')
  803. })
  804. it('should stop calling the paypal stages after the error', async function () {
  805. await expect(this.call()).to.be.rejected
  806. this.checkAccountExists.callCount.should.equal(1)
  807. this.createAccount.callCount.should.equal(1)
  808. this.createBillingInfo.callCount.should.equal(0)
  809. this.setAddressAndCompanyBillingInfo.callCount.should.equal(0)
  810. this.createSubscription.callCount.should.equal(0)
  811. })
  812. })
  813. })
  814. describe('paypal actions', function () {
  815. beforeEach(function () {
  816. this.apiRequest = sinon.stub(this.RecurlyWrapper.promises, 'apiRequest')
  817. this._parseAccountXml = sinon.spy(
  818. this.RecurlyWrapper.promises,
  819. '_parseAccountXml'
  820. )
  821. this._parseBillingInfoXml = sinon.spy(
  822. this.RecurlyWrapper.promises,
  823. '_parseBillingInfoXml'
  824. )
  825. this._parseSubscriptionXml = sinon.spy(
  826. this.RecurlyWrapper.promises,
  827. '_parseSubscriptionXml'
  828. )
  829. this.cache = {
  830. user: (this.user = {
  831. _id: 'some_id',
  832. email: 'foo@bar.com',
  833. first_name: 'Foo',
  834. last_name: 'Bar',
  835. }),
  836. recurlyTokenIds: (this.recurlyTokenIds = {
  837. billing: 'a-token-id',
  838. threeDSecureActionResult: 'a-3d-token-id',
  839. }),
  840. subscriptionDetails: (this.subscriptionDetails = {
  841. currencyCode: 'EUR',
  842. plan_code: 'some_plan_code',
  843. coupon_code: '',
  844. isPaypal: true,
  845. address: {
  846. address1: 'addr_one',
  847. address2: 'addr_two',
  848. city: 'some_city',
  849. country: 'some_country',
  850. state: 'some_state',
  851. zip: 'some_zip',
  852. },
  853. ITMCampaign: 'itm-campaign-value',
  854. ITMContent: 'itm-content-value',
  855. }),
  856. }
  857. })
  858. afterEach(function () {
  859. this.apiRequest.restore()
  860. this._parseAccountXml.restore()
  861. this._parseBillingInfoXml.restore()
  862. this._parseSubscriptionXml.restore()
  863. })
  864. describe('_paypal.checkAccountExists', function () {
  865. beforeEach(function () {
  866. this.call = () => {
  867. return this.RecurlyWrapper.promises._paypal.checkAccountExists(
  868. this.cache
  869. )
  870. }
  871. })
  872. describe('when the account exists', function () {
  873. beforeEach(function () {
  874. const resultXml =
  875. '<account><account_code>abc</account_code></account>'
  876. this.apiRequest.resolves({
  877. response: { status: 200 },
  878. body: resultXml,
  879. })
  880. })
  881. it('should not produce an error', async function () {
  882. await expect(this.call()).to.be.fulfilled
  883. })
  884. it('should call apiRequest', async function () {
  885. await this.call()
  886. this.apiRequest.callCount.should.equal(1)
  887. })
  888. it('should call _parseAccountXml', async function () {
  889. await this.call()
  890. this.RecurlyWrapper.promises._parseAccountXml.callCount.should.equal(
  891. 1
  892. )
  893. })
  894. it('should add the account to the cumulative result', async function () {
  895. const result = await this.call()
  896. expect(result.account).to.not.equal(null)
  897. expect(result.account).to.not.equal(undefined)
  898. expect(result.account).to.deep.equal({
  899. account_code: 'abc',
  900. })
  901. })
  902. it('should set userExists to true', async function () {
  903. const result = await this.call()
  904. expect(result.userExists).to.equal(true)
  905. })
  906. })
  907. describe('when the account does not exist', function () {
  908. beforeEach(function () {
  909. this.apiRequest.resolves({
  910. response: { status: 404 },
  911. body: '',
  912. })
  913. })
  914. it('should not produce an error', async function () {
  915. await expect(this.call()).to.be.fulfilled
  916. })
  917. it('should call apiRequest', async function () {
  918. await this.call()
  919. this.apiRequest.callCount.should.equal(1)
  920. this.apiRequest.firstCall.args[0].method.should.equal('GET')
  921. })
  922. it('should not call _parseAccountXml', async function () {
  923. await this.call()
  924. this.RecurlyWrapper.promises._parseAccountXml.callCount.should.equal(
  925. 0
  926. )
  927. })
  928. it('should not add the account to result', async function () {
  929. const result = await this.call()
  930. expect(result.account).to.equal(undefined)
  931. })
  932. it('should set userExists to false', async function () {
  933. const result = await this.call()
  934. expect(result.userExists).to.equal(false)
  935. })
  936. })
  937. describe('when apiRequest produces an error', function () {
  938. beforeEach(function () {
  939. this.apiRequest.rejects(new Error('woops'))
  940. })
  941. it('should produce an error', async function () {
  942. await expect(this.call()).to.be.rejected
  943. })
  944. })
  945. })
  946. describe('_paypal.createAccount', function () {
  947. beforeEach(function () {
  948. this.call = () => {
  949. return this.RecurlyWrapper.promises._paypal.createAccount(this.cache)
  950. }
  951. })
  952. describe('when address is missing from subscriptionDetails', function () {
  953. beforeEach(function () {
  954. this.cache.subscriptionDetails.address = null
  955. })
  956. it('should produce an error', async function () {
  957. await expect(this.call()).to.be.rejected
  958. })
  959. })
  960. describe('when country is missing from address', function () {
  961. beforeEach(function () {
  962. this.cache.subscriptionDetails.address = {}
  963. })
  964. it('should produce an error', async function () {
  965. await expect(this.call()).to.be.rejectedWith(Errors.InvalidError)
  966. })
  967. })
  968. describe('when account already exists', function () {
  969. beforeEach(function () {
  970. this.cache.userExists = true
  971. this.cache.account = { account_code: 'abc' }
  972. })
  973. it('should not produce an error', async function () {
  974. await expect(this.call()).to.be.fulfilled
  975. })
  976. it('should produce cache object', async function () {
  977. const result = await this.call()
  978. expect(result).to.deep.equal(this.cache)
  979. expect(result.account).to.deep.equal({
  980. account_code: 'abc',
  981. })
  982. })
  983. it('should not call apiRequest', async function () {
  984. await expect(this.call()).to.be.fulfilled
  985. this.apiRequest.callCount.should.equal(0)
  986. })
  987. it('should not call _parseAccountXml', async function () {
  988. await this.call()
  989. this.RecurlyWrapper.promises._parseAccountXml.callCount.should.equal(
  990. 0
  991. )
  992. })
  993. })
  994. describe('when account does not exist', function () {
  995. beforeEach(function () {
  996. this.cache.userExists = false
  997. const resultXml =
  998. '<account><account_code>abc</account_code></account>'
  999. this.apiRequest.resolves({
  1000. response: { status: 200 },
  1001. body: resultXml,
  1002. })
  1003. })
  1004. it('sends correct XML', async function () {
  1005. await this.call()
  1006. const { body } = this.apiRequest.lastCall.args[0]
  1007. expect(body).to.equal(`\
  1008. <account>
  1009. <account_code>some_id</account_code>
  1010. <email>foo@bar.com</email>
  1011. <first_name>Foo</first_name>
  1012. <last_name>Bar</last_name>
  1013. <address>
  1014. <address1>addr_one</address1>
  1015. <address2>addr_two</address2>
  1016. <city>some_city</city>
  1017. <state>some_state</state>
  1018. <zip>some_zip</zip>
  1019. <country>some_country</country>
  1020. </address>
  1021. </account>\
  1022. `)
  1023. })
  1024. it('should not produce an error', async function () {
  1025. await expect(this.call()).to.be.fulfilled
  1026. })
  1027. it('should call apiRequest', async function () {
  1028. await this.call()
  1029. this.apiRequest.callCount.should.equal(1)
  1030. this.apiRequest.firstCall.args[0].method.should.equal('POST')
  1031. })
  1032. it('should call _parseAccountXml', async function () {
  1033. await this.call()
  1034. this.RecurlyWrapper.promises._parseAccountXml.callCount.should.equal(
  1035. 1
  1036. )
  1037. })
  1038. describe('when apiRequest produces an error', function () {
  1039. beforeEach(function () {
  1040. this.apiRequest.rejects(new Error('woops'))
  1041. })
  1042. it('should produce an error', async function () {
  1043. await expect(this.call()).to.be.rejectedWith('woops')
  1044. })
  1045. })
  1046. })
  1047. })
  1048. describe('_paypal.createBillingInfo', function () {
  1049. beforeEach(function () {
  1050. this.cache.account = { account_code: 'abc' }
  1051. this.call = () => {
  1052. return this.RecurlyWrapper.promises._paypal.createBillingInfo(
  1053. this.cache
  1054. )
  1055. }
  1056. })
  1057. describe('when account_code is missing from cache', function () {
  1058. beforeEach(function () {
  1059. this.cache.account.account_code = null
  1060. })
  1061. it('should produce an error', async function () {
  1062. await expect(this.call()).to.be.rejected
  1063. })
  1064. })
  1065. describe('when all goes well', function () {
  1066. beforeEach(function () {
  1067. const resultXml = '<billing_info><a>1</a></billing_info>'
  1068. this.apiRequest.resolves({
  1069. response: { status: 200 },
  1070. body: resultXml,
  1071. })
  1072. })
  1073. it('sends correct XML', async function () {
  1074. await this.call()
  1075. const { body } = this.apiRequest.lastCall.args[0]
  1076. expect(body).to.equal(`\
  1077. <billing_info>
  1078. <token_id>a-token-id</token_id>
  1079. </billing_info>\
  1080. `)
  1081. })
  1082. it('should not produce an error', async function () {
  1083. await expect(this.call()).to.be.fulfilled
  1084. })
  1085. it('should call apiRequest', async function () {
  1086. await this.call()
  1087. this.apiRequest.callCount.should.equal(1)
  1088. this.apiRequest.firstCall.args[0].method.should.equal('POST')
  1089. })
  1090. it('should call _parseBillingInfoXml', async function () {
  1091. await this.call()
  1092. this.RecurlyWrapper.promises._parseBillingInfoXml.callCount.should.equal(
  1093. 1
  1094. )
  1095. })
  1096. it('should set billingInfo on cache', async function () {
  1097. const result = await this.call()
  1098. expect(result.billingInfo).to.deep.equal({
  1099. a: '1',
  1100. })
  1101. })
  1102. })
  1103. describe('when apiRequest produces an error', function () {
  1104. beforeEach(function () {
  1105. this.apiRequest.resolves(new Error('woops'))
  1106. })
  1107. it('should produce an error', async function () {
  1108. await expect(this.call()).to.be.rejected
  1109. })
  1110. })
  1111. })
  1112. describe('_paypal.setAddressAndCompanyBillingInfo', function () {
  1113. beforeEach(function () {
  1114. this.cache.account = { account_code: 'abc' }
  1115. this.cache.billingInfo = {}
  1116. this.call = () => {
  1117. return this.RecurlyWrapper.promises._paypal.setAddressAndCompanyBillingInfo(
  1118. this.cache
  1119. )
  1120. }
  1121. })
  1122. describe('when account_code is missing from cache', function () {
  1123. beforeEach(function () {
  1124. this.cache.account.account_code = null
  1125. })
  1126. it('should produce an error', async function () {
  1127. await expect(this.call()).to.be.rejected
  1128. })
  1129. })
  1130. describe('when country is missing', function () {
  1131. beforeEach(function () {
  1132. this.cache.subscriptionDetails.address = { country: '' }
  1133. })
  1134. it('should produce an error', async function () {
  1135. await expect(this.call()).to.be.rejectedWith(Errors.InvalidError)
  1136. })
  1137. })
  1138. describe('when all goes well', function () {
  1139. beforeEach(function () {
  1140. const resultXml = '<billing_info><city>London</city></billing_info>'
  1141. this.apiRequest.resolves({
  1142. response: { status: 200 },
  1143. body: resultXml,
  1144. })
  1145. })
  1146. it('sends correct XML', async function () {
  1147. await this.call()
  1148. const { body } = this.apiRequest.lastCall.args[0]
  1149. expect(body).to.equal(`\
  1150. <billing_info>
  1151. <address1>addr_one</address1>
  1152. <address2>addr_two</address2>
  1153. <city>some_city</city>
  1154. <state>some_state</state>
  1155. <zip>some_zip</zip>
  1156. <country>some_country</country>
  1157. </billing_info>\
  1158. `)
  1159. })
  1160. it('should not produce an error', async function () {
  1161. await expect(this.call()).to.be.fulfilled
  1162. })
  1163. it('should call apiRequest', async function () {
  1164. await this.call()
  1165. this.apiRequest.callCount.should.equal(1)
  1166. this.apiRequest.firstCall.args[0].method.should.equal('PUT')
  1167. })
  1168. it('should call _parseBillingInfoXml', async function () {
  1169. await this.call()
  1170. this.RecurlyWrapper.promises._parseBillingInfoXml.callCount.should.equal(
  1171. 1
  1172. )
  1173. })
  1174. it('should set billingInfo on cache', async function () {
  1175. const result = await this.call()
  1176. expect(result.billingInfo).to.deep.equal({
  1177. city: 'London',
  1178. })
  1179. })
  1180. })
  1181. describe('when apiRequest produces an error', function () {
  1182. beforeEach(function () {
  1183. this.apiRequest.rejects(new Error('woops'))
  1184. })
  1185. it('should produce an error', async function () {
  1186. await expect(this.call()).to.be.rejected
  1187. })
  1188. })
  1189. })
  1190. describe('_paypal.createSubscription', function () {
  1191. beforeEach(function () {
  1192. this.cache.account = { account_code: 'abc' }
  1193. this.cache.billingInfo = {}
  1194. this.call = () =>
  1195. this.RecurlyWrapper.promises._paypal.createSubscription(this.cache)
  1196. })
  1197. describe('when all goes well', function () {
  1198. beforeEach(function () {
  1199. const resultXml = '<subscription><a>1</a></subscription>'
  1200. this.apiRequest.resolves({
  1201. response: { status: 200 },
  1202. body: resultXml,
  1203. })
  1204. })
  1205. it('sends correct XML', async function () {
  1206. await this.call()
  1207. const { body } = this.apiRequest.lastCall.args[0]
  1208. expect(body).to.equal(`\
  1209. <subscription>
  1210. <plan_code>some_plan_code</plan_code>
  1211. <currency>EUR</currency>
  1212. <coupon_code/>
  1213. <account>
  1214. <account_code>some_id</account_code>
  1215. </account>
  1216. <custom_fields>
  1217. <custom_field>
  1218. <name>itm_campaign</name>
  1219. <value>itm-campaign-value</value>
  1220. </custom_field>
  1221. <custom_field>
  1222. <name>itm_content</name>
  1223. <value>itm-content-value</value>
  1224. </custom_field>
  1225. </custom_fields>
  1226. </subscription>\
  1227. `)
  1228. })
  1229. it('should not produce an error', async function () {
  1230. await expect(this.call()).to.be.fulfilled
  1231. })
  1232. it('should call apiRequest', async function () {
  1233. await this.call()
  1234. this.apiRequest.callCount.should.equal(1)
  1235. this.apiRequest.firstCall.args[0].method.should.equal('POST')
  1236. })
  1237. it('should call _parseSubscriptionXml', async function () {
  1238. await this.call()
  1239. this.RecurlyWrapper.promises._parseSubscriptionXml.callCount.should.equal(
  1240. 1
  1241. )
  1242. })
  1243. it('should set subscription on cache', async function () {
  1244. const result = await this.call()
  1245. expect(result.subscription).to.deep.equal({
  1246. a: '1',
  1247. })
  1248. })
  1249. })
  1250. describe('when apiRequest produces an error', function () {
  1251. beforeEach(function () {
  1252. this.apiRequest.rejects(new Error('woops'))
  1253. })
  1254. it('should produce an error', async function () {
  1255. await expect(this.call()).to.be.rejected
  1256. })
  1257. })
  1258. })
  1259. })
  1260. describe('listAccountActiveSubscriptions', function () {
  1261. beforeEach(function () {
  1262. this.user_id = 'mock-user-id'
  1263. this.response = { mock: 'response' }
  1264. this.body = '<mock body/>'
  1265. this.RecurlyWrapper.promises.apiRequest = sinon.stub().resolves({
  1266. response: this.response,
  1267. body: this.body,
  1268. })
  1269. this.subscriptions = ['mock', 'subscriptions']
  1270. this.RecurlyWrapper.promises._parseSubscriptionsXml = sinon
  1271. .stub()
  1272. .resolves(this.subscriptions)
  1273. })
  1274. describe('with an account', function () {
  1275. beforeEach(async function () {
  1276. this.result =
  1277. await this.RecurlyWrapper.promises.listAccountActiveSubscriptions(
  1278. this.user_id
  1279. )
  1280. })
  1281. it('should send a request to Recurly', async function () {
  1282. this.RecurlyWrapper.promises.apiRequest
  1283. .calledWith({
  1284. url: `accounts/${this.user_id}/subscriptions`,
  1285. qs: {
  1286. state: 'active',
  1287. },
  1288. expect404: true,
  1289. })
  1290. .should.equal(true)
  1291. })
  1292. it('should return the subscriptions', async function () {
  1293. expect(this.result).to.deep.equal(this.subscriptions)
  1294. })
  1295. })
  1296. describe('without an account', function () {
  1297. beforeEach(async function () {
  1298. this.response.status = 404
  1299. this.accountActiveSubscriptions =
  1300. await this.RecurlyWrapper.promises.listAccountActiveSubscriptions(
  1301. this.user_id
  1302. )
  1303. })
  1304. it('should return an empty array of subscriptions', function () {
  1305. expect(this.accountActiveSubscriptions).to.deep.equal([])
  1306. })
  1307. })
  1308. })
  1309. })