RecurlyWrapperTests.js 49 KB

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