RecurlyWrapperTests.coffee 34 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019
  1. should = require('chai').should()
  2. expect = require('chai').expect
  3. sinon = require 'sinon'
  4. crypto = require 'crypto'
  5. querystring = require 'querystring'
  6. modulePath = "../../../../app/js/Features/Subscription/RecurlyWrapper"
  7. SandboxedModule = require('sandboxed-module')
  8. tk = require("timekeeper")
  9. fixtures =
  10. "subscriptions/44f83d7cba354d5b84812419f923ea96":
  11. "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
  12. "<subscription href=\"https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96\">" +
  13. " <account href=\"https://api.recurly.com/v2/accounts/104\"/>" +
  14. " <plan href=\"https://api.recurly.com/v2/plans/gold\">" +
  15. " <plan_code>gold</plan_code>" +
  16. " <name>Gold plan</name>" +
  17. " </plan>" +
  18. " <uuid>44f83d7cba354d5b84812419f923ea96</uuid>" +
  19. " <state>active</state>" +
  20. " <unit_amount_in_cents type=\"integer\">800</unit_amount_in_cents>" +
  21. " <currency>EUR</currency>" +
  22. " <quantity type=\"integer\">1</quantity>" +
  23. " <activated_at type=\"datetime\">2011-05-27T07:00:00Z</activated_at>" +
  24. " <canceled_at nil=\"nil\"></canceled_at>" +
  25. " <expires_at nil=\"nil\"></expires_at>" +
  26. " <current_period_started_at type=\"datetime\">2011-06-27T07:00:00Z</current_period_started_at>" +
  27. " <current_period_ends_at type=\"datetime\">2011-07-27T07:00:00Z</current_period_ends_at>" +
  28. " <trial_started_at nil=\"nil\"></trial_started_at>" +
  29. " <trial_ends_at nil=\"nil\"></trial_ends_at>" +
  30. " <subscription_add_ons type=\"array\">" +
  31. " <subscription_add_on>" +
  32. " <add_on_code>ipaddresses</add_on_code>" +
  33. " <quantity>10</quantity>" +
  34. " <unit_amount_in_cents>150</unit_amount_in_cents>" +
  35. " </subscription_add_on>" +
  36. " </subscription_add_ons>" +
  37. " <a name=\"cancel\" href=\"https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/cancel\" method=\"put\"/>" +
  38. " <a name=\"terminate\" href=\"https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/terminate\" method=\"put\"/>" +
  39. " <a name=\"postpone\" href=\"https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/postpone\" method=\"put\"/>" +
  40. "</subscription>"
  41. "recurly_js/result/70db44b10f5f4b238669480c9903f6f5":
  42. "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
  43. "<subscription href=\"https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96\">" +
  44. " <account href=\"https://api.recurly.com/v2/accounts/104\"/>" +
  45. " <plan href=\"https://api.recurly.com/v2/plans/gold\">" +
  46. " <plan_code>gold</plan_code>" +
  47. " <name>Gold plan</name>" +
  48. " </plan>" +
  49. " <uuid>44f83d7cba354d5b84812419f923ea96</uuid>" +
  50. " <state>active</state>" +
  51. " <unit_amount_in_cents type=\"integer\">800</unit_amount_in_cents>" +
  52. " <currency>EUR</currency>" +
  53. " <quantity type=\"integer\">1</quantity>" +
  54. " <activated_at type=\"datetime\">2011-05-27T07:00:00Z</activated_at>" +
  55. " <canceled_at nil=\"nil\"></canceled_at>" +
  56. " <expires_at nil=\"nil\"></expires_at>" +
  57. " <current_period_started_at type=\"datetime\">2011-06-27T07:00:00Z</current_period_started_at>" +
  58. " <current_period_ends_at type=\"datetime\">2011-07-27T07:00:00Z</current_period_ends_at>" +
  59. " <trial_started_at nil=\"nil\"></trial_started_at>" +
  60. " <trial_ends_at nil=\"nil\"></trial_ends_at>" +
  61. " <subscription_add_ons type=\"array\">" +
  62. " <subscription_add_on>" +
  63. " <add_on_code>ipaddresses</add_on_code>" +
  64. " <quantity>10</quantity>" +
  65. " <unit_amount_in_cents>150</unit_amount_in_cents>" +
  66. " </subscription_add_on>" +
  67. " </subscription_add_ons>" +
  68. " <a name=\"cancel\" href=\"https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/cancel\" method=\"put\"/>" +
  69. " <a name=\"terminate\" href=\"https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/terminate\" method=\"put\"/>" +
  70. " <a name=\"postpone\" href=\"https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/postpone\" method=\"put\"/>" +
  71. "</subscription>"
  72. "accounts/104":
  73. "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
  74. "<account href=\"https://api.recurly.com/v2/accounts/104\">" +
  75. " <adjustments href=\"https://api.recurly.com/v2/accounts/1/adjustments\"/>" +
  76. " <billing_info href=\"https://api.recurly.com/v2/accounts/1/billing_info\"/>" +
  77. " <invoices href=\"https://api.recurly.com/v2/accounts/1/invoices\"/>" +
  78. " <redemption href=\"https://api.recurly.com/v2/accounts/1/redemption\"/>" +
  79. " <subscriptions href=\"https://api.recurly.com/v2/accounts/1/subscriptions\"/>" +
  80. " <transactions href=\"https://api.recurly.com/v2/accounts/1/transactions\"/>" +
  81. " <account_code>104</account_code>" +
  82. " <state>active</state>" +
  83. " <username nil=\"nil\"></username>" +
  84. " <email>verena@example.com</email>" +
  85. " <first_name>Verena</first_name>" +
  86. " <last_name>Example</last_name>" +
  87. " <accept_language nil=\"nil\"></accept_language>" +
  88. " <hosted_login_token>a92468579e9c4231a6c0031c4716c01d</hosted_login_token>" +
  89. " <created_at type=\"datetime\">2011-10-25T12:00:00</created_at>" +
  90. "</account>"
  91. mockApiRequest = (options, callback) ->
  92. if fixtures[options.url]
  93. callback(null, {statusCode : 200}, fixtures[options.url])
  94. else
  95. callback("Not found", {statusCode : 404})
  96. describe "RecurlyWrapper", ->
  97. before ->
  98. @settings =
  99. plans: [{
  100. planCode: "collaborator"
  101. name: "Collaborator"
  102. features:
  103. collaborators: -1
  104. versioning: true
  105. }]
  106. defaultPlanCode:
  107. collaborators: 0
  108. versioning: false
  109. apis:
  110. recurly:
  111. apiKey: 'nonsense'
  112. privateKey: 'private_nonsense'
  113. @RecurlyWrapper = RecurlyWrapper = SandboxedModule.require modulePath, requires:
  114. "settings-sharelatex": @settings
  115. "logger-sharelatex":
  116. err: sinon.stub()
  117. error: sinon.stub()
  118. log: sinon.stub()
  119. "request": sinon.stub()
  120. describe "sign", ->
  121. before (done) ->
  122. tk.freeze Date.now() # freeze the time for these tests
  123. @RecurlyWrapper.sign({
  124. subscription :
  125. plan_code : "gold"
  126. name : "$$$"
  127. }, (error, signature) =>
  128. @signature = signature
  129. done()
  130. )
  131. after ->
  132. tk.reset()
  133. it "should be signed correctly", ->
  134. signed = @signature.split("|")[0]
  135. query = @signature.split("|")[1]
  136. crypto.createHmac("sha1", @settings.apis.recurly.privateKey).update(query).digest("hex").should.equal signed
  137. it "should be url escaped", ->
  138. query = @signature.split("|")[1]
  139. should.equal query.match(/\[/), null
  140. query.match(/\%5B/).should.not.equal null
  141. it "should contain the passed data", ->
  142. query = querystring.parse @signature.split("|")[1]
  143. query["subscription[plan_code]"].should.equal "gold"
  144. query["subscription[name]"].should.equal "$$$"
  145. it "should contain a nonce", ->
  146. query = querystring.parse @signature.split("|")[1]
  147. should.exist query["nonce"]
  148. it "should contain a timestamp", ->
  149. query = querystring.parse @signature.split("|")[1]
  150. query["timestamp"].should.equal Math.round(Date.now() / 1000) + ""
  151. describe "_parseXml", ->
  152. it "should convert different data types into correct representations", (done) ->
  153. xml = """
  154. <?xml version="1.0" encoding="UTF-8"?>
  155. <subscription href="https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96">
  156. <account href="https://api.recurly.com/v2/accounts/1"/>
  157. <plan href="https://api.recurly.com/v2/plans/gold">
  158. <plan_code>gold</plan_code>
  159. <name>Gold plan</name>
  160. </plan>
  161. <uuid>44f83d7cba354d5b84812419f923ea96</uuid>
  162. <state>active</state>
  163. <unit_amount_in_cents type="integer">800</unit_amount_in_cents>
  164. <currency>EUR</currency>
  165. <quantity type="integer">1</quantity>
  166. <activated_at type="datetime">2011-05-27T07:00:00Z</activated_at>
  167. <canceled_at nil="nil"></canceled_at>
  168. <expires_at nil="nil"></expires_at>
  169. <current_period_started_at type="datetime">2011-06-27T07:00:00Z</current_period_started_at>
  170. <current_period_ends_at type="datetime">2011-07-27T07:00:00Z</current_period_ends_at>
  171. <trial_started_at nil="nil"></trial_started_at>
  172. <trial_ends_at nil="nil"></trial_ends_at>
  173. <subscription_add_ons type="array">
  174. <subscription_add_on>
  175. <add_on_code>ipaddresses</add_on_code>
  176. <quantity>10</quantity>
  177. <unit_amount_in_cents>150</unit_amount_in_cents>
  178. </subscription_add_on>
  179. </subscription_add_ons>
  180. <a name="cancel" href="https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/cancel" method="put"/>
  181. <a name="terminate" href="https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/terminate" method="put"/>
  182. <a name="postpone" href="https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/postpone" method="put"/>
  183. </subscription>
  184. """
  185. @RecurlyWrapper._parseXml xml, (error, data) ->
  186. data.subscription.plan.plan_code.should.equal "gold"
  187. data.subscription.plan.name.should.equal "Gold plan"
  188. data.subscription.uuid.should.equal "44f83d7cba354d5b84812419f923ea96"
  189. data.subscription.state.should.equal "active"
  190. data.subscription.unit_amount_in_cents.should.equal 800
  191. data.subscription.currency.should.equal "EUR"
  192. data.subscription.quantity.should.equal 1
  193. data.subscription.activated_at.should.deep.equal new Date("2011-05-27T07:00:00Z")
  194. should.equal data.subscription.canceled_at, null
  195. should.equal data.subscription.expires_at, null
  196. data.subscription.current_period_started_at.should.deep.equal new Date("2011-06-27T07:00:00Z")
  197. data.subscription.current_period_ends_at.should.deep.equal new Date("2011-07-27T07:00:00Z")
  198. should.equal data.subscription.trial_started_at, null
  199. should.equal data.subscription.trial_ends_at, null
  200. data.subscription.subscription_add_ons[0].should.deep.equal {
  201. add_on_code: "ipaddresses"
  202. quantity: "10"
  203. unit_amount_in_cents: "150"
  204. }
  205. data.subscription.account.url.should.equal "https://api.recurly.com/v2/accounts/1"
  206. data.subscription.url.should.equal "https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96"
  207. data.subscription.plan.url.should.equal "https://api.recurly.com/v2/plans/gold"
  208. done()
  209. describe "getSubscription", ->
  210. describe "with proper subscription id", ->
  211. before ->
  212. @apiRequest = sinon.stub(@RecurlyWrapper, "apiRequest", mockApiRequest)
  213. @RecurlyWrapper.getSubscription "44f83d7cba354d5b84812419f923ea96", (error, recurlySubscription) =>
  214. @recurlySubscription = recurlySubscription
  215. after ->
  216. @RecurlyWrapper.apiRequest.restore()
  217. it "should look up the subscription at the normal API end point", ->
  218. @apiRequest.args[0][0].url.should.equal "subscriptions/44f83d7cba354d5b84812419f923ea96"
  219. it "should return the subscription", ->
  220. @recurlySubscription.uuid.should.equal "44f83d7cba354d5b84812419f923ea96"
  221. describe "with ReculyJS token", ->
  222. before ->
  223. @apiRequest = sinon.stub(@RecurlyWrapper, "apiRequest", mockApiRequest)
  224. @RecurlyWrapper.getSubscription "70db44b10f5f4b238669480c9903f6f5", {recurlyJsResult: true}, (error, recurlySubscription) =>
  225. @recurlySubscription = recurlySubscription
  226. after ->
  227. @RecurlyWrapper.apiRequest.restore()
  228. it "should return the subscription", ->
  229. @recurlySubscription.uuid.should.equal "44f83d7cba354d5b84812419f923ea96"
  230. it "should look up the subscription at the RecurlyJS API end point", ->
  231. @apiRequest.args[0][0].url.should.equal "recurly_js/result/70db44b10f5f4b238669480c9903f6f5"
  232. describe "with includeAccount", ->
  233. beforeEach ->
  234. @apiRequest = sinon.stub(@RecurlyWrapper, "apiRequest", mockApiRequest)
  235. @RecurlyWrapper.getSubscription "44f83d7cba354d5b84812419f923ea96", {includeAccount: true}, (error, recurlySubscription) =>
  236. @recurlySubscription = recurlySubscription
  237. afterEach ->
  238. @RecurlyWrapper.apiRequest.restore()
  239. it "should request the account from the API", ->
  240. @apiRequest.args[1][0].url.should.equal "accounts/104"
  241. it "should populate the account attribute", ->
  242. @recurlySubscription.account.account_code.should.equal "104"
  243. describe "updateSubscription", ->
  244. beforeEach (done) ->
  245. @recurlySubscriptionId = "subscription-id-123"
  246. @apiRequest = sinon.stub @RecurlyWrapper, "apiRequest", (options, callback) =>
  247. @requestOptions = options
  248. callback null, {}, fixtures["subscriptions/44f83d7cba354d5b84812419f923ea96"]
  249. @RecurlyWrapper.updateSubscription @recurlySubscriptionId, { plan_code : "silver", timeframe: "now" }, (error, recurlySubscription) =>
  250. @recurlySubscription = recurlySubscription
  251. done()
  252. afterEach ->
  253. @RecurlyWrapper.apiRequest.restore()
  254. it "should send an update request to the API", ->
  255. @apiRequest.called.should.equal true
  256. @requestOptions.body.should.equal """
  257. <subscription>
  258. <plan_code>silver</plan_code>
  259. <timeframe>now</timeframe>
  260. </subscription>
  261. """
  262. @requestOptions.url.should.equal "subscriptions/#{@recurlySubscriptionId}"
  263. @requestOptions.method.should.equal "put"
  264. it "should return the updated subscription", ->
  265. should.exist @recurlySubscription
  266. @recurlySubscription.plan.plan_code.should.equal "gold"
  267. describe "cancelSubscription", ->
  268. beforeEach (done) ->
  269. @recurlySubscriptionId = "subscription-id-123"
  270. @apiRequest = sinon.stub @RecurlyWrapper, "apiRequest", (options, callback) =>
  271. options.url.should.equal "subscriptions/#{@recurlySubscriptionId}/cancel"
  272. options.method.should.equal "put"
  273. callback()
  274. @RecurlyWrapper.cancelSubscription(@recurlySubscriptionId, done)
  275. afterEach ->
  276. @RecurlyWrapper.apiRequest.restore()
  277. it "should send a cancel request to the API", ->
  278. @apiRequest.called.should.equal true
  279. describe "reactivateSubscription", ->
  280. beforeEach (done) ->
  281. @recurlySubscriptionId = "subscription-id-123"
  282. @apiRequest = sinon.stub @RecurlyWrapper, "apiRequest", (options, callback) =>
  283. options.url.should.equal "subscriptions/#{@recurlySubscriptionId}/reactivate"
  284. options.method.should.equal "put"
  285. callback()
  286. @RecurlyWrapper.reactivateSubscription(@recurlySubscriptionId, done)
  287. afterEach ->
  288. @RecurlyWrapper.apiRequest.restore()
  289. it "should send a cancel request to the API", ->
  290. @apiRequest.called.should.equal true
  291. describe "redeemCoupon", ->
  292. beforeEach (done) ->
  293. @recurlyAccountId = "account-id-123"
  294. @coupon_code = "312321312"
  295. @apiRequest = sinon.stub @RecurlyWrapper, "apiRequest", (options, callback) =>
  296. options.url.should.equal "coupons/#{@coupon_code}/redeem"
  297. options.body.indexOf("<account_code>#{@recurlyAccountId}</account_code>").should.not.equal -1
  298. options.body.indexOf("<currency>USD</currency>").should.not.equal -1
  299. options.method.should.equal "post"
  300. callback()
  301. @RecurlyWrapper.redeemCoupon(@recurlyAccountId, @coupon_code, done)
  302. afterEach ->
  303. @RecurlyWrapper.apiRequest.restore()
  304. it "should send the request to redem the coupon", ->
  305. @apiRequest.called.should.equal true
  306. describe "_addressToXml", ->
  307. beforeEach ->
  308. @address =
  309. address1: "addr_one"
  310. address2: "addr_two"
  311. country: "some_country"
  312. state: "some_state"
  313. postal_code: "some_zip"
  314. nonsenseKey: "rubbish"
  315. it 'should generate the correct xml', () ->
  316. result = @RecurlyWrapper._addressToXml @address
  317. should.equal(
  318. result,
  319. """
  320. <billing_info>
  321. <address1>addr_one</address1>
  322. <address2 nil="nil">addr_two</address2>
  323. <country>some_country</country>
  324. <state>some_state</state>
  325. <zip>some_zip</zip>
  326. </billing_info>\n
  327. """
  328. )
  329. describe 'createSubscription', ->
  330. beforeEach ->
  331. @user =
  332. _id: 'some_id'
  333. email: 'user@example.com'
  334. @subscriptionDetails =
  335. currencyCode: "EUR"
  336. plan_code: "some_plan_code"
  337. coupon_code: ""
  338. isPaypal: true
  339. address:
  340. address1: "addr_one"
  341. address2: "addr_two"
  342. country: "some_country"
  343. state: "some_state"
  344. zip: "some_zip"
  345. @subscription = {}
  346. @recurly_token_id = "a-token-id"
  347. @call = (callback) =>
  348. @RecurlyWrapper.createSubscription(@user, @subscriptionDetails, @recurly_token_id, callback)
  349. describe 'when paypal', ->
  350. beforeEach ->
  351. @subscriptionDetails.isPaypal = true
  352. @_createPaypalSubscription = sinon.stub(@RecurlyWrapper, '_createPaypalSubscription')
  353. @_createPaypalSubscription.callsArgWith(3, null, @subscription)
  354. afterEach ->
  355. @_createPaypalSubscription.restore()
  356. it 'should not produce an error', (done) ->
  357. @call (err, sub) =>
  358. expect(err).to.equal null
  359. expect(err).to.not.be.instanceof Error
  360. done()
  361. it 'should produce a subscription object', (done) ->
  362. @call (err, sub) =>
  363. expect(sub).to.deep.equal @subscription
  364. done()
  365. it 'should call _createPaypalSubscription', (done) ->
  366. @call (err, sub) =>
  367. @_createPaypalSubscription.callCount.should.equal 1
  368. done()
  369. describe "when _createPaypalSubscription produces an error", ->
  370. beforeEach ->
  371. @_createPaypalSubscription.callsArgWith(3, new Error('woops'))
  372. it 'should produce an error', (done) ->
  373. @call (err, sub) =>
  374. expect(err).to.be.instanceof Error
  375. done()
  376. describe 'when not paypal', ->
  377. beforeEach ->
  378. @subscriptionDetails.isPaypal = false
  379. @_createCreditCardSubscription = sinon.stub(@RecurlyWrapper, '_createCreditCardSubscription')
  380. @_createCreditCardSubscription.callsArgWith(3, null, @subscription)
  381. afterEach ->
  382. @_createCreditCardSubscription.restore()
  383. it 'should not produce an error', (done) ->
  384. @call (err, sub) =>
  385. expect(err).to.equal null
  386. expect(err).to.not.be.instanceof Error
  387. done()
  388. it 'should produce a subscription object', (done) ->
  389. @call (err, sub) =>
  390. expect(sub).to.deep.equal @subscription
  391. done()
  392. it 'should call _createCreditCardSubscription', (done) ->
  393. @call (err, sub) =>
  394. @_createCreditCardSubscription.callCount.should.equal 1
  395. done()
  396. describe "when _createCreditCardSubscription produces an error", ->
  397. beforeEach ->
  398. @_createCreditCardSubscription.callsArgWith(3, new Error('woops'))
  399. it 'should produce an error', (done) ->
  400. @call (err, sub) =>
  401. expect(err).to.be.instanceof Error
  402. done()
  403. describe '_createCreditCardSubscription', ->
  404. beforeEach ->
  405. @user =
  406. _id: 'some_id'
  407. email: 'user@example.com'
  408. @subscriptionDetails =
  409. currencyCode: "EUR"
  410. plan_code: "some_plan_code"
  411. coupon_code: ""
  412. isPaypal: true
  413. address:
  414. address1: "addr_one"
  415. address2: "addr_two"
  416. country: "some_country"
  417. state: "some_state"
  418. zip: "some_zip"
  419. @subscription = {}
  420. @recurly_token_id = "a-token-id"
  421. @apiRequest = sinon.stub(@RecurlyWrapper, 'apiRequest')
  422. @response =
  423. statusCode: 200
  424. @body = "<xml>is_bad</xml>"
  425. @apiRequest.callsArgWith(1, null, @response, @body)
  426. @_parseSubscriptionXml = sinon.stub(@RecurlyWrapper, '_parseSubscriptionXml')
  427. @_parseSubscriptionXml.callsArgWith(1, null, @subscription)
  428. @call = (callback) =>
  429. @RecurlyWrapper._createCreditCardSubscription(@user, @subscriptionDetails, @recurly_token_id, callback)
  430. afterEach ->
  431. @apiRequest.restore()
  432. @_parseSubscriptionXml.restore()
  433. it 'should not produce an error', (done) ->
  434. @call (err, sub) =>
  435. expect(err).to.not.be.instanceof Error
  436. expect(err).to.equal null
  437. done()
  438. it 'should produce a subscription', (done) ->
  439. @call (err, sub) =>
  440. expect(sub).to.equal @subscription
  441. done()
  442. it 'should call apiRequest', (done) ->
  443. @call (err, sub) =>
  444. @apiRequest.callCount.should.equal 1
  445. done()
  446. it 'should call _parseSubscriptionXml', (done) ->
  447. @call (err, sub) =>
  448. @_parseSubscriptionXml.callCount.should.equal 1
  449. done()
  450. describe 'when api request produces an error', ->
  451. beforeEach ->
  452. @apiRequest.callsArgWith(1, new Error('woops'))
  453. it 'should produce an error', (done) ->
  454. @call (err, sub) =>
  455. expect(err).to.be.instanceof Error
  456. done()
  457. it 'should call apiRequest', (done) ->
  458. @call (err, sub) =>
  459. @apiRequest.callCount.should.equal 1
  460. done()
  461. it 'should not _parseSubscriptionXml', (done) ->
  462. @call (err, sub) =>
  463. @_parseSubscriptionXml.callCount.should.equal 0
  464. done()
  465. describe 'when parse xml produces an error', ->
  466. beforeEach ->
  467. @_parseSubscriptionXml.callsArgWith(1, new Error('woops'))
  468. it 'should produce an error', (done) ->
  469. @call (err, sub) =>
  470. expect(err).to.be.instanceof Error
  471. done()
  472. describe '_createPaypalSubscription', ->
  473. beforeEach ->
  474. @checkAccountExists = sinon.stub(@RecurlyWrapper._paypal, 'checkAccountExists')
  475. @createAccount = sinon.stub(@RecurlyWrapper._paypal, 'createAccount')
  476. @createBillingInfo = sinon.stub(@RecurlyWrapper._paypal, 'createBillingInfo')
  477. @setAddress = sinon.stub(@RecurlyWrapper._paypal, 'setAddress')
  478. @createSubscription = sinon.stub(@RecurlyWrapper._paypal, 'createSubscription')
  479. @user =
  480. _id: 'some_id'
  481. email: 'user@example.com'
  482. @subscriptionDetails =
  483. currencyCode: "EUR"
  484. plan_code: "some_plan_code"
  485. coupon_code: ""
  486. isPaypal: true
  487. address:
  488. address1: "addr_one"
  489. address2: "addr_two"
  490. country: "some_country"
  491. state: "some_state"
  492. zip: "some_zip"
  493. @subscription = {}
  494. @recurly_token_id = "a-token-id"
  495. # set up data callbacks
  496. user = @user
  497. subscriptionDetails = @subscriptionDetails
  498. recurly_token_id = @recurly_token_id
  499. @checkAccountExists.callsArgWith(1, null,
  500. {user, subscriptionDetails, recurly_token_id,
  501. userExists: false, account: {accountCode: 'xx'}}
  502. )
  503. @createAccount.callsArgWith(1, null,
  504. {user, subscriptionDetails, recurly_token_id,
  505. userExists: false, account: {accountCode: 'xx'}}
  506. )
  507. @createBillingInfo.callsArgWith(1, null,
  508. {user, subscriptionDetails, recurly_token_id,
  509. userExists: false, account: {accountCode: 'xx'}, billingInfo: {token_id: 'abc'}}
  510. )
  511. @setAddress.callsArgWith(1, null,
  512. {user, subscriptionDetails, recurly_token_id,
  513. userExists: false, account: {accountCode: 'xx'}, billingInfo: {token_id: 'abc'}}
  514. )
  515. @createSubscription.callsArgWith(1, null,
  516. {user, subscriptionDetails, recurly_token_id,
  517. userExists: false, account: {accountCode: 'xx'}, billingInfo: {token_id: 'abc'}, subscription: @subscription}
  518. )
  519. @call = (callback) =>
  520. @RecurlyWrapper._createPaypalSubscription @user, @subscriptionDetails, @recurly_token_id, callback
  521. afterEach ->
  522. @checkAccountExists.restore()
  523. @createAccount.restore()
  524. @createBillingInfo.restore()
  525. @setAddress.restore()
  526. @createSubscription.restore()
  527. it 'should not produce an error', (done) ->
  528. @call (err, sub) =>
  529. expect(err).to.not.be.instanceof Error
  530. done()
  531. it 'should produce a subscription object', (done) ->
  532. @call (err, sub) =>
  533. expect(sub).to.not.equal null
  534. expect(sub).to.equal @subscription
  535. done()
  536. it 'should call each of the paypal stages', (done) ->
  537. @call (err, sub) =>
  538. @checkAccountExists.callCount.should.equal 1
  539. @createAccount.callCount.should.equal 1
  540. @createBillingInfo.callCount.should.equal 1
  541. @setAddress.callCount.should.equal 1
  542. @createSubscription.callCount.should.equal 1
  543. done()
  544. describe 'when one of the paypal stages produces an error', ->
  545. beforeEach ->
  546. @createAccount.callsArgWith(1, new Error('woops'))
  547. it 'should produce an error', (done) ->
  548. @call (err, sub) =>
  549. expect(err).to.be.instanceof Error
  550. done()
  551. it 'should stop calling the paypal stages after the error', (done) ->
  552. @call (err, sub) =>
  553. @checkAccountExists.callCount.should.equal 1
  554. @createAccount.callCount.should.equal 1
  555. @createBillingInfo.callCount.should.equal 0
  556. @setAddress.callCount.should.equal 0
  557. @createSubscription.callCount.should.equal 0
  558. done()
  559. describe 'paypal actions', ->
  560. beforeEach ->
  561. @apiRequest = sinon.stub(@RecurlyWrapper, 'apiRequest')
  562. @_parseAccountXml = sinon.spy(@RecurlyWrapper, '_parseAccountXml')
  563. @_parseBillingInfoXml = sinon.spy(@RecurlyWrapper, '_parseBillingInfoXml')
  564. @_parseSubscriptionXml = sinon.spy(@RecurlyWrapper, '_parseSubscriptionXml')
  565. @cache =
  566. user: @user = {_id: 'some_id'}
  567. recurly_token_id: @recurly_token_id = "some_token"
  568. subscriptionDetails: @subscriptionDetails =
  569. currencyCode: "EUR"
  570. plan_code: "some_plan_code"
  571. coupon_code: ""
  572. isPaypal: true
  573. address:
  574. address1: "addr_one"
  575. address2: "addr_two"
  576. country: "some_country"
  577. state: "some_state"
  578. zip: "some_zip"
  579. afterEach ->
  580. @apiRequest.restore()
  581. @_parseAccountXml.restore()
  582. @_parseBillingInfoXml.restore()
  583. @_parseSubscriptionXml.restore()
  584. describe '_paypal.checkAccountExists', ->
  585. beforeEach ->
  586. @call = (callback) =>
  587. @RecurlyWrapper._paypal.checkAccountExists @cache, callback
  588. describe 'when the account exists', ->
  589. beforeEach ->
  590. resultXml = '<account><account_code>abc</account_code></account>'
  591. @apiRequest.callsArgWith(1, null, {statusCode: 200}, resultXml)
  592. it 'should not produce an error', (done) ->
  593. @call (err, result) =>
  594. expect(err).to.not.be.instanceof Error
  595. done()
  596. it 'should call apiRequest', (done) ->
  597. @call (err, result) =>
  598. @apiRequest.callCount.should.equal 1
  599. done()
  600. it 'should call _parseAccountXml', (done) ->
  601. @call (err, result) =>
  602. @RecurlyWrapper._parseAccountXml.callCount.should.equal 1
  603. done()
  604. it 'should add the account to the cumulative result', (done) ->
  605. @call (err, result) =>
  606. expect(result.account).to.not.equal null
  607. expect(result.account).to.not.equal undefined
  608. expect(result.account).to.deep.equal {
  609. account_code: 'abc'
  610. }
  611. done()
  612. it 'should set userExists to true', (done) ->
  613. @call (err, result) =>
  614. expect(result.userExists).to.equal true
  615. done()
  616. describe 'when the account does not exist', ->
  617. beforeEach ->
  618. @apiRequest.callsArgWith(1, new Error('not found'), {statusCode: 404}, '')
  619. it 'should not produce an error', (done) ->
  620. @call (err, result) =>
  621. expect(err).to.not.be.instanceof Error
  622. done()
  623. it 'should call apiRequest', (done) ->
  624. @call (err, result) =>
  625. @apiRequest.callCount.should.equal 1
  626. @apiRequest.firstCall.args[0].method.should.equal 'GET'
  627. done()
  628. it 'should not call _parseAccountXml', (done) ->
  629. @call (err, result) =>
  630. @RecurlyWrapper._parseAccountXml.callCount.should.equal 0
  631. done()
  632. it 'should not add the account to result', (done) ->
  633. @call (err, result) =>
  634. expect(result.account).to.equal undefined
  635. done()
  636. it 'should set userExists to false', (done) ->
  637. @call (err, result) =>
  638. expect(result.userExists).to.equal false
  639. done()
  640. describe 'when apiRequest produces an error', ->
  641. beforeEach ->
  642. @apiRequest.callsArgWith(1, new Error('woops'), {statusCode: 500})
  643. it 'should produce an error', (done) ->
  644. @call (err, result) =>
  645. expect(err).to.be.instanceof Error
  646. done()
  647. describe '_paypal.createAccount', ->
  648. beforeEach ->
  649. @call = (callback) =>
  650. @RecurlyWrapper._paypal.createAccount @cache, callback
  651. describe 'when address is missing from subscriptionDetails', ->
  652. beforeEach ->
  653. @cache.subscriptionDetails.address = null
  654. it 'should produce an error', (done) ->
  655. @call (err, result) =>
  656. expect(err).to.be.instanceof Error
  657. done()
  658. describe 'when account already exists', ->
  659. beforeEach ->
  660. @cache.userExists = true
  661. @cache.account =
  662. account_code: 'abc'
  663. it 'should not produce an error', (done) ->
  664. @call (err, result) =>
  665. expect(err).to.not.be.instanceof Error
  666. done()
  667. it 'should produce cache object', (done) ->
  668. @call (err, result) =>
  669. expect(result).to.deep.equal @cache
  670. expect(result.account).to.deep.equal {
  671. account_code: 'abc'
  672. }
  673. done()
  674. it 'should not call apiRequest', (done) ->
  675. @call (err, result) =>
  676. @apiRequest.callCount.should.equal 0
  677. done()
  678. it 'should not call _parseAccountXml', (done) ->
  679. @call (err, result) =>
  680. @RecurlyWrapper._parseAccountXml.callCount.should.equal 0
  681. done()
  682. describe 'when account does not exist', ->
  683. beforeEach ->
  684. @cache.userExists = false
  685. resultXml = '<account><account_code>abc</account_code></account>'
  686. @apiRequest.callsArgWith(1, null, {statusCode: 200}, resultXml)
  687. it 'should not produce an error', (done) ->
  688. @call (err, result) =>
  689. expect(err).to.not.be.instanceof Error
  690. done()
  691. it 'should call apiRequest', (done) ->
  692. @call (err, result) =>
  693. @apiRequest.callCount.should.equal 1
  694. @apiRequest.firstCall.args[0].method.should.equal 'POST'
  695. done()
  696. it 'should call _parseAccountXml', (done) ->
  697. @call (err, result) =>
  698. @RecurlyWrapper._parseAccountXml.callCount.should.equal 1
  699. done()
  700. describe 'when apiRequest produces an error', ->
  701. beforeEach ->
  702. @apiRequest.callsArgWith(1, new Error('woops'), {statusCode: 500})
  703. it 'should produce an error', (done) ->
  704. @call (err, result) =>
  705. expect(err).to.be.instanceof Error
  706. done()
  707. describe '_paypal.createBillingInfo', ->
  708. beforeEach ->
  709. @cache.account =
  710. account_code: 'abc'
  711. @call = (callback) =>
  712. @RecurlyWrapper._paypal.createBillingInfo @cache, callback
  713. describe 'when account_code is missing from cache', ->
  714. beforeEach ->
  715. @cache.account.account_code = null
  716. it 'should produce an error', (done) ->
  717. @call (err, result) =>
  718. expect(err).to.be.instanceof Error
  719. done()
  720. describe 'when all goes well', ->
  721. beforeEach ->
  722. resultXml = '<billing_info><a>1</a></billing_info>'
  723. @apiRequest.callsArgWith(1, null, {statusCode: 200}, resultXml)
  724. it 'should not produce an error', (done) ->
  725. @call (err, result) =>
  726. expect(err).to.not.be.instanceof Error
  727. done()
  728. it 'should call apiRequest', (done) ->
  729. @call (err, result) =>
  730. @apiRequest.callCount.should.equal 1
  731. @apiRequest.firstCall.args[0].method.should.equal 'POST'
  732. done()
  733. it 'should call _parseBillingInfoXml', (done) ->
  734. @call (err, result) =>
  735. @RecurlyWrapper._parseBillingInfoXml.callCount.should.equal 1
  736. done()
  737. it 'should set billingInfo on cache', (done) ->
  738. @call (err, result) =>
  739. expect(result.billingInfo).to.deep.equal {
  740. a: "1"
  741. }
  742. done()
  743. describe 'when apiRequest produces an error', ->
  744. beforeEach ->
  745. @apiRequest.callsArgWith(1, new Error('woops'), {statusCode: 500})
  746. it 'should produce an error', (done) ->
  747. @call (err, result) =>
  748. expect(err).to.be.instanceof Error
  749. done()
  750. describe '_paypal.setAddress', ->
  751. beforeEach ->
  752. @cache.account =
  753. account_code: 'abc'
  754. @cache.billingInfo = {}
  755. @call = (callback) =>
  756. @RecurlyWrapper._paypal.setAddress @cache, callback
  757. describe 'when account_code is missing from cache', ->
  758. beforeEach ->
  759. @cache.account.account_code = null
  760. it 'should produce an error', (done) ->
  761. @call (err, result) =>
  762. expect(err).to.be.instanceof Error
  763. done()
  764. describe 'when address is missing from subscriptionDetails', ->
  765. beforeEach ->
  766. @cache.subscriptionDetails.address = null
  767. it 'should produce an error', (done) ->
  768. @call (err, result) =>
  769. expect(err).to.be.instanceof Error
  770. done()
  771. describe 'when all goes well', ->
  772. beforeEach ->
  773. resultXml = '<billing_info><city>London</city></billing_info>'
  774. @apiRequest.callsArgWith(1, null, {statusCode: 200}, resultXml)
  775. it 'should not produce an error', (done) ->
  776. @call (err, result) =>
  777. expect(err).to.not.be.instanceof Error
  778. done()
  779. it 'should call apiRequest', (done) ->
  780. @call (err, result) =>
  781. @apiRequest.callCount.should.equal 1
  782. @apiRequest.firstCall.args[0].method.should.equal 'PUT'
  783. done()
  784. it 'should call _parseBillingInfoXml', (done) ->
  785. @call (err, result) =>
  786. @RecurlyWrapper._parseBillingInfoXml.callCount.should.equal 1
  787. done()
  788. it 'should set billingInfo on cache', (done) ->
  789. @call (err, result) =>
  790. expect(result.billingInfo).to.deep.equal {
  791. city: 'London'
  792. }
  793. done()
  794. describe 'when apiRequest produces an error', ->
  795. beforeEach ->
  796. @apiRequest.callsArgWith(1, new Error('woops'), {statusCode: 500})
  797. it 'should produce an error', (done) ->
  798. @call (err, result) =>
  799. expect(err).to.be.instanceof Error
  800. done()
  801. describe '_paypal.createSubscription', ->
  802. beforeEach ->
  803. @cache.account =
  804. account_code: 'abc'
  805. @cache.billingInfo = {}
  806. @call = (callback) =>
  807. @RecurlyWrapper._paypal.createSubscription @cache, callback
  808. describe 'when all goes well', ->
  809. beforeEach ->
  810. resultXml = '<subscription><a>1</a></subscription>'
  811. @apiRequest.callsArgWith(1, null, {statusCode: 200}, resultXml)
  812. it 'should not produce an error', (done) ->
  813. @call (err, result) =>
  814. expect(err).to.not.be.instanceof Error
  815. done()
  816. it 'should call apiRequest', (done) ->
  817. @call (err, result) =>
  818. @apiRequest.callCount.should.equal 1
  819. @apiRequest.firstCall.args[0].method.should.equal 'POST'
  820. done()
  821. it 'should call _parseSubscriptionXml', (done) ->
  822. @call (err, result) =>
  823. @RecurlyWrapper._parseSubscriptionXml.callCount.should.equal 1
  824. done()
  825. it 'should set subscription on cache', (done) ->
  826. @call (err, result) =>
  827. expect(result.subscription).to.deep.equal {
  828. a: "1"
  829. }
  830. done()
  831. describe 'when apiRequest produces an error', ->
  832. beforeEach ->
  833. @apiRequest.callsArgWith(1, new Error('woops'), {statusCode: 500})
  834. it 'should produce an error', (done) ->
  835. @call (err, result) =>
  836. expect(err).to.be.instanceof Error
  837. done()