RecurlyWrapperTests.js 50 KB

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