RecurlyWrapperTests.js 49 KB

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