RecurlyWrapperTests.js 51 KB

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