RecurlyWrapperTests.js 46 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475
  1. const { assert, 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 () {
  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. })
  290. afterEach(function () {
  291. this.RecurlyWrapper.promises.apiRequest.restore()
  292. })
  293. it('should produce an error', async function () {
  294. try {
  295. await this.RecurlyWrapper.promises.updateAccountEmailAddress(
  296. this.recurlyAccountId,
  297. this.newEmail
  298. )
  299. assert.fail('Expected error not thrown')
  300. } catch (error) {
  301. expect(error).to.have.property('message')
  302. expect(error.message.startsWith('Invalid character')).to.be.true
  303. expect(this.apiRequest.called).to.equal(false)
  304. }
  305. })
  306. })
  307. describe('updateSubscription', function () {
  308. beforeEach(async function () {
  309. this.recurlySubscriptionId = 'subscription-id-123'
  310. this.apiRequest = sinon
  311. .stub(this.RecurlyWrapper.promises, 'apiRequest')
  312. .callsFake(options => {
  313. this.requestOptions = options
  314. return {
  315. error: null,
  316. response: {},
  317. body: fixtures['subscriptions/44f83d7cba354d5b84812419f923ea96'],
  318. }
  319. })
  320. this.recurlySubscription =
  321. await this.RecurlyWrapper.promises.updateSubscription(
  322. this.recurlySubscriptionId,
  323. { plan_code: 'silver', timeframe: 'now' }
  324. )
  325. })
  326. afterEach(function () {
  327. this.RecurlyWrapper.promises.apiRequest.restore()
  328. })
  329. it('sends correct XML', function () {
  330. this.apiRequest.called.should.equal(true)
  331. const { body } = this.apiRequest.lastCall.args[0]
  332. expect(body).to.equal(`\
  333. <subscription>
  334. <plan_code>silver</plan_code>
  335. <timeframe>now</timeframe>
  336. </subscription>\
  337. `)
  338. this.requestOptions.url.should.equal(
  339. `subscriptions/${this.recurlySubscriptionId}`
  340. )
  341. this.requestOptions.method.should.equal('PUT')
  342. })
  343. it('should return the updated subscription', function () {
  344. expect(this.recurlySubscription).to.exist
  345. this.recurlySubscription.plan.plan_code.should.equal('gold')
  346. })
  347. })
  348. describe('redeemCoupon', function () {
  349. beforeEach(async function () {
  350. this.recurlyAccountId = 'account-id-123'
  351. this.coupon_code = '312321312'
  352. this.apiRequest = sinon
  353. .stub(this.RecurlyWrapper.promises, 'apiRequest')
  354. .callsFake(options => {
  355. options.url.should.equal(`coupons/${this.coupon_code}/redeem`)
  356. options.method.should.equal('POST')
  357. return {}
  358. })
  359. await this.RecurlyWrapper.promises.redeemCoupon(
  360. this.recurlyAccountId,
  361. this.coupon_code
  362. )
  363. })
  364. afterEach(function () {
  365. this.RecurlyWrapper.promises.apiRequest.restore()
  366. })
  367. it('sends correct XML', function () {
  368. this.apiRequest.called.should.equal(true)
  369. const { body } = this.apiRequest.lastCall.args[0]
  370. expect(body).to.equal(`\
  371. <redemption>
  372. <account_code>account-id-123</account_code>
  373. <currency>USD</currency>
  374. </redemption>\
  375. `)
  376. })
  377. })
  378. describe('createFixedAmountCoupon', function () {
  379. beforeEach(async function () {
  380. this.couponCode = 'a-coupon-code'
  381. this.couponName = 'a-coupon-name'
  382. this.currencyCode = 'EUR'
  383. this.discount = 1337
  384. this.planCode = 'a-plan-code'
  385. this.apiRequest = sinon
  386. .stub(this.RecurlyWrapper.promises, 'apiRequest')
  387. .resolves()
  388. await this.RecurlyWrapper.promises.createFixedAmountCoupon(
  389. this.couponCode,
  390. this.couponName,
  391. this.currencyCode,
  392. this.discount,
  393. this.planCode
  394. )
  395. })
  396. afterEach(function () {
  397. this.RecurlyWrapper.promises.apiRequest.restore()
  398. })
  399. it('sends correct XML', function () {
  400. this.apiRequest.called.should.equal(true)
  401. const { body } = this.apiRequest.lastCall.args[0]
  402. expect(body).to.equal(`\
  403. <coupon>
  404. <coupon_code>a-coupon-code</coupon_code>
  405. <name>a-coupon-name</name>
  406. <discount_type>dollars</discount_type>
  407. <discount_in_cents>
  408. <EUR>1337</EUR>
  409. </discount_in_cents>
  410. <plan_codes>
  411. <plan_code>a-plan-code</plan_code>
  412. </plan_codes>
  413. <applies_to_all_plans>false</applies_to_all_plans>
  414. </coupon>\
  415. `)
  416. })
  417. })
  418. describe('createSubscription', function () {
  419. beforeEach(function () {
  420. this.user = {
  421. _id: 'some_id',
  422. email: 'user@example.com',
  423. }
  424. this.subscriptionDetails = {
  425. currencyCode: 'EUR',
  426. plan_code: 'some_plan_code',
  427. coupon_code: '',
  428. isPaypal: true,
  429. address: {
  430. address1: 'addr_one',
  431. address2: 'addr_two',
  432. country: 'some_country',
  433. state: 'some_state',
  434. zip: 'some_zip',
  435. },
  436. }
  437. this.subscription = {}
  438. this.recurlyTokenIds = {
  439. billing: 'a-token-id',
  440. threeDSecureActionResult: 'a-3d-token-id',
  441. }
  442. this.call = () => {
  443. return this.RecurlyWrapper.promises.createSubscription(
  444. this.user,
  445. this.subscriptionDetails,
  446. this.recurlyTokenIds
  447. )
  448. }
  449. })
  450. describe('when paypal', function () {
  451. beforeEach(function () {
  452. this.subscriptionDetails.isPaypal = true
  453. this._createPaypalSubscription = sinon.stub(
  454. this.RecurlyWrapper.promises,
  455. '_createPaypalSubscription'
  456. )
  457. this._createPaypalSubscription.resolves(this.subscription)
  458. })
  459. afterEach(function () {
  460. this._createPaypalSubscription.restore()
  461. })
  462. it('should not produce an error', async function () {
  463. await expect(this.call()).to.be.fulfilled
  464. })
  465. it('should produce a subscription object', async function () {
  466. const sub = await this.call()
  467. expect(sub).to.deep.equal(this.subscription)
  468. })
  469. it('should call _createPaypalSubscription', async function () {
  470. await this.call()
  471. this._createPaypalSubscription.callCount.should.equal(1)
  472. })
  473. describe('when _createPaypalSubscription produces an error', function () {
  474. beforeEach(function () {
  475. this._createPaypalSubscription.rejects(new Error('woops'))
  476. })
  477. it('should produce an error', async function () {
  478. await expect(this.call()).to.be.rejectedWith('woops')
  479. })
  480. })
  481. })
  482. describe('when not paypal', function () {
  483. beforeEach(function () {
  484. this.subscriptionDetails.isPaypal = false
  485. this._createCreditCardSubscription = sinon.stub(
  486. this.RecurlyWrapper.promises,
  487. '_createCreditCardSubscription'
  488. )
  489. this._createCreditCardSubscription.resolves(this.subscription)
  490. })
  491. afterEach(function () {
  492. this._createCreditCardSubscription.restore()
  493. })
  494. it('should not produce an error', async function () {
  495. await this.call()
  496. })
  497. it('should produce a subscription object', async function () {
  498. const sub = await this.call()
  499. expect(sub).to.deep.equal(this.subscription)
  500. })
  501. it('should call _createCreditCardSubscription', async function () {
  502. await this.call()
  503. this._createCreditCardSubscription.callCount.should.equal(1)
  504. })
  505. describe('when _createCreditCardSubscription produces an error', function () {
  506. beforeEach(function () {
  507. this._createCreditCardSubscription.rejects(new Error('woops'))
  508. })
  509. it('should produce an error', async function () {
  510. await expect(this.call()).to.be.rejectedWith('woops')
  511. })
  512. })
  513. })
  514. })
  515. describe('_createCreditCardSubscription', function () {
  516. beforeEach(function () {
  517. this.user = {
  518. _id: 'some_id',
  519. email: 'user@example.com',
  520. first_name: 'Foo',
  521. last_name: 'Johnson',
  522. }
  523. this.subscriptionDetails = {
  524. currencyCode: 'EUR',
  525. plan_code: 'some_plan_code',
  526. coupon_code: '',
  527. isPaypal: true,
  528. first_name: 'Prairie',
  529. address: {
  530. address1: 'addr_one',
  531. address2: 'addr_two',
  532. country: 'some_country',
  533. state: 'some_state',
  534. zip: 'some_zip',
  535. },
  536. subscription_add_ons: [
  537. { subscription_add_on: { add_on_code: 'test_add_on', quantity: 2 } },
  538. ],
  539. ITMCampaign: 'itm-campaign-value',
  540. ITMContent: 'itm-content-value',
  541. ITMReferrer: 'itm-referrer-value',
  542. }
  543. this.subscription = {}
  544. this.recurlyTokenIds = {
  545. billing: 'a-token-id',
  546. threeDSecureActionResult: 'a-3d-token-id',
  547. }
  548. this.apiRequest = sinon.stub(this.RecurlyWrapper.promises, 'apiRequest')
  549. this.response = { status: 200 }
  550. this.body = '<xml>is_bad</xml>'
  551. this.apiRequest.resolves({
  552. response: this.response,
  553. body: this.body,
  554. })
  555. this._parseSubscriptionXml = sinon.stub(
  556. this.RecurlyWrapper.promises,
  557. '_parseSubscriptionXml'
  558. )
  559. this._parseSubscriptionXml.resolves(this.subscription)
  560. this.call = () => {
  561. return this.RecurlyWrapper.promises._createCreditCardSubscription(
  562. this.user,
  563. this.subscriptionDetails,
  564. this.recurlyTokenIds
  565. )
  566. }
  567. })
  568. afterEach(function () {
  569. this.apiRequest.restore()
  570. this._parseSubscriptionXml.restore()
  571. })
  572. it('sends correct XML', async function () {
  573. await this.call()
  574. const { body } = this.apiRequest.lastCall.args[0]
  575. expect(body).to.equal(`\
  576. <subscription>
  577. <plan_code>some_plan_code</plan_code>
  578. <currency>EUR</currency>
  579. <coupon_code/>
  580. <account>
  581. <account_code>some_id</account_code>
  582. <email>user@example.com</email>
  583. <first_name>Prairie</first_name>
  584. <last_name>Johnson</last_name>
  585. <billing_info>
  586. <token_id>a-token-id</token_id>
  587. <three_d_secure_action_result_token_id>a-3d-token-id</three_d_secure_action_result_token_id>
  588. </billing_info>
  589. </account>
  590. <subscription_add_ons>
  591. <subscription_add_on>
  592. <add_on_code>test_add_on</add_on_code>
  593. <quantity>2</quantity>
  594. </subscription_add_on>
  595. </subscription_add_ons>
  596. <custom_fields>
  597. <custom_field>
  598. <name>itm_campaign</name>
  599. <value>itm-campaign-value</value>
  600. </custom_field>
  601. <custom_field>
  602. <name>itm_content</name>
  603. <value>itm-content-value</value>
  604. </custom_field>
  605. <custom_field>
  606. <name>itm_referrer</name>
  607. <value>itm-referrer-value</value>
  608. </custom_field>
  609. </custom_fields>
  610. </subscription>\
  611. `)
  612. })
  613. it('should not produce an error', async function () {
  614. await expect(this.call()).to.be.fulfilled
  615. })
  616. it('should produce a subscription', async function () {
  617. const sub = await this.call()
  618. expect(sub).to.equal(this.subscription)
  619. })
  620. it('should call apiRequest', async function () {
  621. await this.call()
  622. this.apiRequest.callCount.should.equal(1)
  623. })
  624. it('should call _parseSubscriptionXml', async function () {
  625. await this.call()
  626. this._parseSubscriptionXml.callCount.should.equal(1)
  627. })
  628. describe('when api request returns 422', function () {
  629. beforeEach(function () {
  630. const body = `\
  631. <?xml version="1.0" encoding="UTF-8"?>
  632. <errors>
  633. <transaction_error>
  634. <error_code>three_d_secure_action_required</error_code>
  635. <error_category>3d_secure_action_required</error_category>
  636. <merchant_message>Your payment gateway is requesting that the transaction be completed with 3D Secure in accordance with PSD2.</merchant_message>
  637. <customer_message>Your card must be authenticated with 3D Secure before continuing.</customer_message>
  638. <gateway_error_code nil="nil"></gateway_error_code>
  639. <three_d_secure_action_token_id>mock_three_d_secure_action_token</three_d_secure_action_token_id>
  640. </transaction_error>
  641. <error field="subscription.account.base" symbol="three_d_secure_action_required">Your card must be authenticated with 3D Secure before continuing.</error>
  642. </errors>
  643. `
  644. // this.apiRequest.yields(null, { statusCode: 422 }, body)
  645. this.apiRequest.resolves({
  646. response: { status: 422 },
  647. body,
  648. })
  649. })
  650. it('should produce an error', async function () {
  651. const promise = this.call()
  652. let error
  653. try {
  654. await promise
  655. } catch (err) {
  656. error = err
  657. }
  658. expect(error).to.be.instanceOf(
  659. SubscriptionErrors.RecurlyTransactionError
  660. )
  661. expect(error).to.have.nested.property(
  662. 'info.public.message',
  663. 'Your card must be authenticated with 3D Secure before continuing.'
  664. )
  665. expect(error).to.have.nested.property(
  666. 'info.public.threeDSecureActionTokenId',
  667. 'mock_three_d_secure_action_token'
  668. )
  669. })
  670. })
  671. describe('when api request produces an error', function () {
  672. beforeEach(function () {
  673. this.apiRequest.rejects(new Error('woops'))
  674. })
  675. it('should produce an error', async function () {
  676. await expect(this.call()).to.be.rejectedWith('woops')
  677. })
  678. it('should call apiRequest', async function () {
  679. await expect(this.call()).to.be.rejected
  680. this.apiRequest.callCount.should.equal(1)
  681. })
  682. it('should not _parseSubscriptionXml', async function () {
  683. await expect(this.call()).to.be.rejected
  684. this._parseSubscriptionXml.callCount.should.equal(0)
  685. })
  686. })
  687. describe('when parse xml produces an error', function () {
  688. beforeEach(function () {
  689. this._parseSubscriptionXml.rejects(new Error('woops xml'))
  690. })
  691. it('should produce an error', async function () {
  692. await expect(this.call()).to.be.rejectedWith('woops xml')
  693. })
  694. })
  695. })
  696. describe('_createPaypalSubscription', function () {
  697. beforeEach(function () {
  698. this.checkAccountExists = sinon.stub(
  699. this.RecurlyWrapper.promises._paypal,
  700. 'checkAccountExists'
  701. )
  702. this.createAccount = sinon.stub(
  703. this.RecurlyWrapper.promises._paypal,
  704. 'createAccount'
  705. )
  706. this.createBillingInfo = sinon.stub(
  707. this.RecurlyWrapper.promises._paypal,
  708. 'createBillingInfo'
  709. )
  710. this.setAddressAndCompanyBillingInfo = sinon.stub(
  711. this.RecurlyWrapper.promises._paypal,
  712. 'setAddressAndCompanyBillingInfo'
  713. )
  714. this.createSubscription = sinon.stub(
  715. this.RecurlyWrapper.promises._paypal,
  716. 'createSubscription'
  717. )
  718. this.user = {
  719. _id: 'some_id',
  720. email: 'user@example.com',
  721. }
  722. this.subscriptionDetails = {
  723. currencyCode: 'EUR',
  724. plan_code: 'some_plan_code',
  725. coupon_code: '',
  726. isPaypal: true,
  727. address: {
  728. address1: 'addr_one',
  729. address2: 'addr_two',
  730. country: 'some_country',
  731. state: 'some_state',
  732. zip: 'some_zip',
  733. },
  734. }
  735. this.subscription = {}
  736. this.recurlyTokenIds = {
  737. billing: 'a-token-id',
  738. threeDSecureActionResult: 'a-3d-token-id',
  739. }
  740. // set up data callbacks
  741. const { user } = this
  742. const { subscriptionDetails } = this
  743. const { recurlyTokenIds } = this
  744. this.checkAccountExists.resolves({
  745. user,
  746. subscriptionDetails,
  747. recurlyTokenIds,
  748. userExists: false,
  749. account: { accountCode: 'xx' },
  750. })
  751. this.createAccount.resolves({
  752. user,
  753. subscriptionDetails,
  754. recurlyTokenIds,
  755. userExists: false,
  756. account: { accountCode: 'xx' },
  757. })
  758. this.createBillingInfo.resolves({
  759. user,
  760. subscriptionDetails,
  761. recurlyTokenIds,
  762. userExists: false,
  763. account: { accountCode: 'xx' },
  764. billingInfo: { token_id: 'abc' },
  765. })
  766. this.setAddressAndCompanyBillingInfo.resolves({
  767. user,
  768. subscriptionDetails,
  769. recurlyTokenIds,
  770. userExists: false,
  771. account: { accountCode: 'xx' },
  772. billingInfo: { token_id: 'abc' },
  773. })
  774. this.createSubscription.resolves({
  775. user,
  776. subscriptionDetails,
  777. recurlyTokenIds,
  778. userExists: false,
  779. account: { accountCode: 'xx' },
  780. billingInfo: { token_id: 'abc' },
  781. subscription: this.subscription,
  782. })
  783. this.call = () => {
  784. return this.RecurlyWrapper.promises._createPaypalSubscription(
  785. this.user,
  786. this.subscriptionDetails,
  787. this.recurlyTokenIds
  788. )
  789. }
  790. })
  791. afterEach(function () {
  792. this.checkAccountExists.restore()
  793. this.createAccount.restore()
  794. this.createBillingInfo.restore()
  795. this.setAddressAndCompanyBillingInfo.restore()
  796. this.createSubscription.restore()
  797. })
  798. it('should not produce an error', async function () {
  799. await expect(this.call()).to.be.fulfilled
  800. })
  801. it('should produce a subscription object', async function () {
  802. const sub = await this.call()
  803. expect(sub).to.not.equal(null)
  804. expect(sub).to.equal(this.subscription)
  805. })
  806. it('should call each of the paypal stages', async function () {
  807. await this.call()
  808. this.checkAccountExists.callCount.should.equal(1)
  809. this.createAccount.callCount.should.equal(1)
  810. this.createBillingInfo.callCount.should.equal(1)
  811. this.setAddressAndCompanyBillingInfo.callCount.should.equal(1)
  812. this.createSubscription.callCount.should.equal(1)
  813. })
  814. describe('when one of the paypal stages produces an error', function () {
  815. beforeEach(function () {
  816. this.createAccount.rejects(new Error('woops'))
  817. })
  818. it('should produce an error', async function () {
  819. await expect(this.call()).to.be.rejectedWith('woops')
  820. })
  821. it('should stop calling the paypal stages after the error', async function () {
  822. await expect(this.call()).to.be.rejected
  823. this.checkAccountExists.callCount.should.equal(1)
  824. this.createAccount.callCount.should.equal(1)
  825. this.createBillingInfo.callCount.should.equal(0)
  826. this.setAddressAndCompanyBillingInfo.callCount.should.equal(0)
  827. this.createSubscription.callCount.should.equal(0)
  828. })
  829. })
  830. })
  831. describe('paypal actions', function () {
  832. beforeEach(function () {
  833. this.apiRequest = sinon.stub(this.RecurlyWrapper.promises, 'apiRequest')
  834. this._parseAccountXml = sinon.spy(
  835. this.RecurlyWrapper.promises,
  836. '_parseAccountXml'
  837. )
  838. this._parseBillingInfoXml = sinon.spy(
  839. this.RecurlyWrapper.promises,
  840. '_parseBillingInfoXml'
  841. )
  842. this._parseSubscriptionXml = sinon.spy(
  843. this.RecurlyWrapper.promises,
  844. '_parseSubscriptionXml'
  845. )
  846. this.cache = {
  847. user: (this.user = {
  848. _id: 'some_id',
  849. email: 'foo@bar.com',
  850. first_name: 'Foo',
  851. last_name: 'Bar',
  852. }),
  853. recurlyTokenIds: (this.recurlyTokenIds = {
  854. billing: 'a-token-id',
  855. threeDSecureActionResult: 'a-3d-token-id',
  856. }),
  857. subscriptionDetails: (this.subscriptionDetails = {
  858. currencyCode: 'EUR',
  859. plan_code: 'some_plan_code',
  860. coupon_code: '',
  861. isPaypal: true,
  862. address: {
  863. address1: 'addr_one',
  864. address2: 'addr_two',
  865. city: 'some_city',
  866. country: 'some_country',
  867. state: 'some_state',
  868. zip: 'some_zip',
  869. },
  870. ITMCampaign: 'itm-campaign-value',
  871. ITMContent: 'itm-content-value',
  872. }),
  873. }
  874. })
  875. afterEach(function () {
  876. this.apiRequest.restore()
  877. this._parseAccountXml.restore()
  878. this._parseBillingInfoXml.restore()
  879. this._parseSubscriptionXml.restore()
  880. })
  881. describe('_paypal.checkAccountExists', function () {
  882. beforeEach(function () {
  883. this.call = () => {
  884. return this.RecurlyWrapper.promises._paypal.checkAccountExists(
  885. this.cache
  886. )
  887. }
  888. })
  889. describe('when the account exists', function () {
  890. beforeEach(function () {
  891. const resultXml =
  892. '<account><account_code>abc</account_code></account>'
  893. this.apiRequest.resolves({
  894. response: { status: 200 },
  895. body: resultXml,
  896. })
  897. })
  898. it('should not produce an error', async function () {
  899. await expect(this.call()).to.be.fulfilled
  900. })
  901. it('should call apiRequest', async function () {
  902. await this.call()
  903. this.apiRequest.callCount.should.equal(1)
  904. })
  905. it('should call _parseAccountXml', async function () {
  906. await this.call()
  907. this.RecurlyWrapper.promises._parseAccountXml.callCount.should.equal(
  908. 1
  909. )
  910. })
  911. it('should add the account to the cumulative result', async function () {
  912. const result = await this.call()
  913. expect(result.account).to.not.equal(null)
  914. expect(result.account).to.not.equal(undefined)
  915. expect(result.account).to.deep.equal({
  916. account_code: 'abc',
  917. })
  918. })
  919. it('should set userExists to true', async function () {
  920. const result = await this.call()
  921. expect(result.userExists).to.equal(true)
  922. })
  923. })
  924. describe('when the account does not exist', function () {
  925. beforeEach(function () {
  926. this.apiRequest.resolves({
  927. response: { status: 404 },
  928. body: '',
  929. })
  930. })
  931. it('should not produce an error', async function () {
  932. await expect(this.call()).to.be.fulfilled
  933. })
  934. it('should call apiRequest', async function () {
  935. await this.call()
  936. this.apiRequest.callCount.should.equal(1)
  937. this.apiRequest.firstCall.args[0].method.should.equal('GET')
  938. })
  939. it('should not call _parseAccountXml', async function () {
  940. await this.call()
  941. this.RecurlyWrapper.promises._parseAccountXml.callCount.should.equal(
  942. 0
  943. )
  944. })
  945. it('should not add the account to result', async function () {
  946. const result = await this.call()
  947. expect(result.account).to.equal(undefined)
  948. })
  949. it('should set userExists to false', async function () {
  950. const result = await this.call()
  951. expect(result.userExists).to.equal(false)
  952. })
  953. })
  954. describe('when apiRequest produces an error', function () {
  955. beforeEach(function () {
  956. this.apiRequest.rejects(new Error('woops'))
  957. })
  958. it('should produce an error', async function () {
  959. await expect(this.call()).to.be.rejected
  960. })
  961. })
  962. })
  963. describe('_paypal.createAccount', function () {
  964. beforeEach(function () {
  965. this.call = () => {
  966. return this.RecurlyWrapper.promises._paypal.createAccount(this.cache)
  967. }
  968. })
  969. describe('when address is missing from subscriptionDetails', function () {
  970. beforeEach(function () {
  971. this.cache.subscriptionDetails.address = null
  972. })
  973. it('should produce an error', async function () {
  974. await expect(this.call()).to.be.rejected
  975. })
  976. })
  977. describe('when country is missing from address', function () {
  978. beforeEach(function () {
  979. this.cache.subscriptionDetails.address = {}
  980. })
  981. it('should produce an error', async function () {
  982. await expect(this.call()).to.be.rejectedWith(Errors.InvalidError)
  983. })
  984. })
  985. describe('when account already exists', function () {
  986. beforeEach(function () {
  987. this.cache.userExists = true
  988. this.cache.account = { account_code: 'abc' }
  989. })
  990. it('should not produce an error', async function () {
  991. await expect(this.call()).to.be.fulfilled
  992. })
  993. it('should produce cache object', async function () {
  994. const result = await this.call()
  995. expect(result).to.deep.equal(this.cache)
  996. expect(result.account).to.deep.equal({
  997. account_code: 'abc',
  998. })
  999. })
  1000. it('should not call apiRequest', async function () {
  1001. await expect(this.call()).to.be.fulfilled
  1002. this.apiRequest.callCount.should.equal(0)
  1003. })
  1004. it('should not call _parseAccountXml', async function () {
  1005. await this.call()
  1006. this.RecurlyWrapper.promises._parseAccountXml.callCount.should.equal(
  1007. 0
  1008. )
  1009. })
  1010. })
  1011. describe('when account does not exist', function () {
  1012. beforeEach(function () {
  1013. this.cache.userExists = false
  1014. const resultXml =
  1015. '<account><account_code>abc</account_code></account>'
  1016. this.apiRequest.resolves({
  1017. response: { status: 200 },
  1018. body: resultXml,
  1019. })
  1020. })
  1021. it('sends correct XML', async function () {
  1022. await this.call()
  1023. const { body } = this.apiRequest.lastCall.args[0]
  1024. expect(body).to.equal(`\
  1025. <account>
  1026. <account_code>some_id</account_code>
  1027. <email>foo@bar.com</email>
  1028. <first_name>Foo</first_name>
  1029. <last_name>Bar</last_name>
  1030. <address>
  1031. <address1>addr_one</address1>
  1032. <address2>addr_two</address2>
  1033. <city>some_city</city>
  1034. <state>some_state</state>
  1035. <zip>some_zip</zip>
  1036. <country>some_country</country>
  1037. </address>
  1038. </account>\
  1039. `)
  1040. })
  1041. it('should not produce an error', async function () {
  1042. await expect(this.call()).to.be.fulfilled
  1043. })
  1044. it('should call apiRequest', async function () {
  1045. await this.call()
  1046. this.apiRequest.callCount.should.equal(1)
  1047. this.apiRequest.firstCall.args[0].method.should.equal('POST')
  1048. })
  1049. it('should call _parseAccountXml', async function () {
  1050. await this.call()
  1051. this.RecurlyWrapper.promises._parseAccountXml.callCount.should.equal(
  1052. 1
  1053. )
  1054. })
  1055. describe('when apiRequest produces an error', function () {
  1056. beforeEach(function () {
  1057. this.apiRequest.rejects(new Error('woops'))
  1058. })
  1059. it('should produce an error', async function () {
  1060. await expect(this.call()).to.be.rejectedWith('woops')
  1061. })
  1062. })
  1063. })
  1064. })
  1065. describe('_paypal.createBillingInfo', function () {
  1066. beforeEach(function () {
  1067. this.cache.account = { account_code: 'abc' }
  1068. this.call = () => {
  1069. return this.RecurlyWrapper.promises._paypal.createBillingInfo(
  1070. this.cache
  1071. )
  1072. }
  1073. })
  1074. describe('when account_code is missing from cache', function () {
  1075. beforeEach(function () {
  1076. this.cache.account.account_code = null
  1077. })
  1078. it('should produce an error', async function () {
  1079. await expect(this.call()).to.be.rejected
  1080. })
  1081. })
  1082. describe('when all goes well', function () {
  1083. beforeEach(function () {
  1084. const resultXml = '<billing_info><a>1</a></billing_info>'
  1085. this.apiRequest.resolves({
  1086. response: { status: 200 },
  1087. body: resultXml,
  1088. })
  1089. })
  1090. it('sends correct XML', async function () {
  1091. await this.call()
  1092. const { body } = this.apiRequest.lastCall.args[0]
  1093. expect(body).to.equal(`\
  1094. <billing_info>
  1095. <token_id>a-token-id</token_id>
  1096. </billing_info>\
  1097. `)
  1098. })
  1099. it('should not produce an error', async function () {
  1100. await expect(this.call()).to.be.fulfilled
  1101. })
  1102. it('should call apiRequest', async function () {
  1103. await this.call()
  1104. this.apiRequest.callCount.should.equal(1)
  1105. this.apiRequest.firstCall.args[0].method.should.equal('POST')
  1106. })
  1107. it('should call _parseBillingInfoXml', async function () {
  1108. await this.call()
  1109. this.RecurlyWrapper.promises._parseBillingInfoXml.callCount.should.equal(
  1110. 1
  1111. )
  1112. })
  1113. it('should set billingInfo on cache', async function () {
  1114. const result = await this.call()
  1115. expect(result.billingInfo).to.deep.equal({
  1116. a: '1',
  1117. })
  1118. })
  1119. })
  1120. describe('when apiRequest produces an error', function () {
  1121. beforeEach(function () {
  1122. this.apiRequest.resolves(new Error('woops'))
  1123. })
  1124. it('should produce an error', async function () {
  1125. await expect(this.call()).to.be.rejected
  1126. })
  1127. })
  1128. })
  1129. describe('_paypal.setAddressAndCompanyBillingInfo', function () {
  1130. beforeEach(function () {
  1131. this.cache.account = { account_code: 'abc' }
  1132. this.cache.billingInfo = {}
  1133. this.call = () => {
  1134. return this.RecurlyWrapper.promises._paypal.setAddressAndCompanyBillingInfo(
  1135. this.cache
  1136. )
  1137. }
  1138. })
  1139. describe('when account_code is missing from cache', function () {
  1140. beforeEach(function () {
  1141. this.cache.account.account_code = null
  1142. })
  1143. it('should produce an error', async function () {
  1144. await expect(this.call()).to.be.rejected
  1145. })
  1146. })
  1147. describe('when country is missing', function () {
  1148. beforeEach(function () {
  1149. this.cache.subscriptionDetails.address = { country: '' }
  1150. })
  1151. it('should produce an error', async function () {
  1152. await expect(this.call()).to.be.rejectedWith(Errors.InvalidError)
  1153. })
  1154. })
  1155. describe('when all goes well', function () {
  1156. beforeEach(function () {
  1157. const resultXml = '<billing_info><city>London</city></billing_info>'
  1158. this.apiRequest.resolves({
  1159. response: { status: 200 },
  1160. body: resultXml,
  1161. })
  1162. })
  1163. it('sends correct XML', async function () {
  1164. await this.call()
  1165. const { body } = this.apiRequest.lastCall.args[0]
  1166. expect(body).to.equal(`\
  1167. <billing_info>
  1168. <address1>addr_one</address1>
  1169. <address2>addr_two</address2>
  1170. <city>some_city</city>
  1171. <state>some_state</state>
  1172. <zip>some_zip</zip>
  1173. <country>some_country</country>
  1174. </billing_info>\
  1175. `)
  1176. })
  1177. it('should not produce an error', async function () {
  1178. await expect(this.call()).to.be.fulfilled
  1179. })
  1180. it('should call apiRequest', async function () {
  1181. await this.call()
  1182. this.apiRequest.callCount.should.equal(1)
  1183. this.apiRequest.firstCall.args[0].method.should.equal('PUT')
  1184. })
  1185. it('should call _parseBillingInfoXml', async function () {
  1186. await this.call()
  1187. this.RecurlyWrapper.promises._parseBillingInfoXml.callCount.should.equal(
  1188. 1
  1189. )
  1190. })
  1191. it('should set billingInfo on cache', async function () {
  1192. const result = await this.call()
  1193. expect(result.billingInfo).to.deep.equal({
  1194. city: 'London',
  1195. })
  1196. })
  1197. })
  1198. describe('when apiRequest produces an error', function () {
  1199. beforeEach(function () {
  1200. this.apiRequest.rejects(new Error('woops'))
  1201. })
  1202. it('should produce an error', async function () {
  1203. await expect(this.call()).to.be.rejected
  1204. })
  1205. })
  1206. })
  1207. describe('_paypal.createSubscription', function () {
  1208. beforeEach(function () {
  1209. this.cache.account = { account_code: 'abc' }
  1210. this.cache.billingInfo = {}
  1211. this.call = () =>
  1212. this.RecurlyWrapper.promises._paypal.createSubscription(this.cache)
  1213. })
  1214. describe('when all goes well', function () {
  1215. beforeEach(function () {
  1216. const resultXml = '<subscription><a>1</a></subscription>'
  1217. this.apiRequest.resolves({
  1218. response: { status: 200 },
  1219. body: resultXml,
  1220. })
  1221. })
  1222. it('sends correct XML', async function () {
  1223. await this.call()
  1224. const { body } = this.apiRequest.lastCall.args[0]
  1225. expect(body).to.equal(`\
  1226. <subscription>
  1227. <plan_code>some_plan_code</plan_code>
  1228. <currency>EUR</currency>
  1229. <coupon_code/>
  1230. <account>
  1231. <account_code>some_id</account_code>
  1232. </account>
  1233. <custom_fields>
  1234. <custom_field>
  1235. <name>itm_campaign</name>
  1236. <value>itm-campaign-value</value>
  1237. </custom_field>
  1238. <custom_field>
  1239. <name>itm_content</name>
  1240. <value>itm-content-value</value>
  1241. </custom_field>
  1242. </custom_fields>
  1243. </subscription>\
  1244. `)
  1245. })
  1246. it('should not produce an error', async function () {
  1247. await expect(this.call()).to.be.fulfilled
  1248. })
  1249. it('should call apiRequest', async function () {
  1250. await this.call()
  1251. this.apiRequest.callCount.should.equal(1)
  1252. this.apiRequest.firstCall.args[0].method.should.equal('POST')
  1253. })
  1254. it('should call _parseSubscriptionXml', async function () {
  1255. await this.call()
  1256. this.RecurlyWrapper.promises._parseSubscriptionXml.callCount.should.equal(
  1257. 1
  1258. )
  1259. })
  1260. it('should set subscription on cache', async function () {
  1261. const result = await this.call()
  1262. expect(result.subscription).to.deep.equal({
  1263. a: '1',
  1264. })
  1265. })
  1266. })
  1267. describe('when apiRequest produces an error', function () {
  1268. beforeEach(function () {
  1269. this.apiRequest.rejects(new Error('woops'))
  1270. })
  1271. it('should produce an error', async function () {
  1272. await expect(this.call()).to.be.rejected
  1273. })
  1274. })
  1275. })
  1276. })
  1277. describe('listAccountActiveSubscriptions', function () {
  1278. beforeEach(function () {
  1279. this.user_id = 'mock-user-id'
  1280. this.response = { mock: 'response' }
  1281. this.body = '<mock body/>'
  1282. this.RecurlyWrapper.promises.apiRequest = sinon.stub().resolves({
  1283. response: this.response,
  1284. body: this.body,
  1285. })
  1286. this.subscriptions = ['mock', 'subscriptions']
  1287. this.RecurlyWrapper.promises._parseSubscriptionsXml = sinon
  1288. .stub()
  1289. .resolves(this.subscriptions)
  1290. })
  1291. describe('with an account', function () {
  1292. beforeEach(async function () {
  1293. this.result =
  1294. await this.RecurlyWrapper.promises.listAccountActiveSubscriptions(
  1295. this.user_id
  1296. )
  1297. })
  1298. it('should send a request to Recurly', async function () {
  1299. this.RecurlyWrapper.promises.apiRequest
  1300. .calledWith({
  1301. url: `accounts/${this.user_id}/subscriptions`,
  1302. qs: {
  1303. state: 'active',
  1304. },
  1305. expect404: true,
  1306. })
  1307. .should.equal(true)
  1308. })
  1309. it('should return the subscriptions', async function () {
  1310. expect(this.result).to.deep.equal(this.subscriptions)
  1311. })
  1312. })
  1313. describe('without an account', function () {
  1314. beforeEach(async function () {
  1315. this.response.status = 404
  1316. this.accountActiveSubscriptions =
  1317. await this.RecurlyWrapper.promises.listAccountActiveSubscriptions(
  1318. this.user_id
  1319. )
  1320. })
  1321. it('should return an empty array of subscriptions', function () {
  1322. expect(this.accountActiveSubscriptions).to.deep.equal([])
  1323. })
  1324. })
  1325. })
  1326. })