RecurlyWrapperTests.coffee 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032
  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 'when the subscription is already cancelled', ->
  280. beforeEach ->
  281. @RecurlyWrapper.apiRequest.restore()
  282. @recurlySubscriptionId = "subscription-id-123"
  283. @apiRequest = sinon.stub @RecurlyWrapper, "apiRequest", (options, callback) =>
  284. callback(new Error('woops'), {}, "<error><description>A canceled subscription can't transition to canceled</description></error>")
  285. it 'should not produce an error', (done) ->
  286. @RecurlyWrapper.cancelSubscription @recurlySubscriptionId, (err) =>
  287. expect(err).to.equal null
  288. done()
  289. describe "reactivateSubscription", ->
  290. beforeEach (done) ->
  291. @recurlySubscriptionId = "subscription-id-123"
  292. @apiRequest = sinon.stub @RecurlyWrapper, "apiRequest", (options, callback) =>
  293. options.url.should.equal "subscriptions/#{@recurlySubscriptionId}/reactivate"
  294. options.method.should.equal "put"
  295. callback()
  296. @RecurlyWrapper.reactivateSubscription(@recurlySubscriptionId, done)
  297. afterEach ->
  298. @RecurlyWrapper.apiRequest.restore()
  299. it "should send a cancel request to the API", ->
  300. @apiRequest.called.should.equal true
  301. describe "redeemCoupon", ->
  302. beforeEach (done) ->
  303. @recurlyAccountId = "account-id-123"
  304. @coupon_code = "312321312"
  305. @apiRequest = sinon.stub @RecurlyWrapper, "apiRequest", (options, callback) =>
  306. options.url.should.equal "coupons/#{@coupon_code}/redeem"
  307. options.body.indexOf("<account_code>#{@recurlyAccountId}</account_code>").should.not.equal -1
  308. options.body.indexOf("<currency>USD</currency>").should.not.equal -1
  309. options.method.should.equal "post"
  310. callback()
  311. @RecurlyWrapper.redeemCoupon(@recurlyAccountId, @coupon_code, done)
  312. afterEach ->
  313. @RecurlyWrapper.apiRequest.restore()
  314. it "should send the request to redem the coupon", ->
  315. @apiRequest.called.should.equal true
  316. describe "_addressToXml", ->
  317. beforeEach ->
  318. @address =
  319. address1: "addr_one"
  320. address2: "addr_two"
  321. country: "some_country"
  322. state: "some_state"
  323. postal_code: "some_zip"
  324. nonsenseKey: "rubbish"
  325. it 'should generate the correct xml', () ->
  326. result = @RecurlyWrapper._addressToXml @address
  327. should.equal(
  328. result,
  329. """
  330. <billing_info>
  331. <address1>addr_one</address1>
  332. <address2 nil="nil">addr_two</address2>
  333. <country>some_country</country>
  334. <state>some_state</state>
  335. <zip>some_zip</zip>
  336. </billing_info>\n
  337. """
  338. )
  339. describe 'createSubscription', ->
  340. beforeEach ->
  341. @user =
  342. _id: 'some_id'
  343. email: 'user@example.com'
  344. @subscriptionDetails =
  345. currencyCode: "EUR"
  346. plan_code: "some_plan_code"
  347. coupon_code: ""
  348. isPaypal: true
  349. address:
  350. address1: "addr_one"
  351. address2: "addr_two"
  352. country: "some_country"
  353. state: "some_state"
  354. zip: "some_zip"
  355. @subscription = {}
  356. @recurly_token_id = "a-token-id"
  357. @call = (callback) =>
  358. @RecurlyWrapper.createSubscription(@user, @subscriptionDetails, @recurly_token_id, callback)
  359. describe 'when paypal', ->
  360. beforeEach ->
  361. @subscriptionDetails.isPaypal = true
  362. @_createPaypalSubscription = sinon.stub(@RecurlyWrapper, '_createPaypalSubscription')
  363. @_createPaypalSubscription.callsArgWith(3, null, @subscription)
  364. afterEach ->
  365. @_createPaypalSubscription.restore()
  366. it 'should not produce an error', (done) ->
  367. @call (err, sub) =>
  368. expect(err).to.equal null
  369. expect(err).to.not.be.instanceof Error
  370. done()
  371. it 'should produce a subscription object', (done) ->
  372. @call (err, sub) =>
  373. expect(sub).to.deep.equal @subscription
  374. done()
  375. it 'should call _createPaypalSubscription', (done) ->
  376. @call (err, sub) =>
  377. @_createPaypalSubscription.callCount.should.equal 1
  378. done()
  379. describe "when _createPaypalSubscription produces an error", ->
  380. beforeEach ->
  381. @_createPaypalSubscription.callsArgWith(3, new Error('woops'))
  382. it 'should produce an error', (done) ->
  383. @call (err, sub) =>
  384. expect(err).to.be.instanceof Error
  385. done()
  386. describe 'when not paypal', ->
  387. beforeEach ->
  388. @subscriptionDetails.isPaypal = false
  389. @_createCreditCardSubscription = sinon.stub(@RecurlyWrapper, '_createCreditCardSubscription')
  390. @_createCreditCardSubscription.callsArgWith(3, null, @subscription)
  391. afterEach ->
  392. @_createCreditCardSubscription.restore()
  393. it 'should not produce an error', (done) ->
  394. @call (err, sub) =>
  395. expect(err).to.equal null
  396. expect(err).to.not.be.instanceof Error
  397. done()
  398. it 'should produce a subscription object', (done) ->
  399. @call (err, sub) =>
  400. expect(sub).to.deep.equal @subscription
  401. done()
  402. it 'should call _createCreditCardSubscription', (done) ->
  403. @call (err, sub) =>
  404. @_createCreditCardSubscription.callCount.should.equal 1
  405. done()
  406. describe "when _createCreditCardSubscription produces an error", ->
  407. beforeEach ->
  408. @_createCreditCardSubscription.callsArgWith(3, new Error('woops'))
  409. it 'should produce an error', (done) ->
  410. @call (err, sub) =>
  411. expect(err).to.be.instanceof Error
  412. done()
  413. describe '_createCreditCardSubscription', ->
  414. beforeEach ->
  415. @user =
  416. _id: 'some_id'
  417. email: 'user@example.com'
  418. @subscriptionDetails =
  419. currencyCode: "EUR"
  420. plan_code: "some_plan_code"
  421. coupon_code: ""
  422. isPaypal: true
  423. address:
  424. address1: "addr_one"
  425. address2: "addr_two"
  426. country: "some_country"
  427. state: "some_state"
  428. zip: "some_zip"
  429. @subscription = {}
  430. @recurly_token_id = "a-token-id"
  431. @apiRequest = sinon.stub(@RecurlyWrapper, 'apiRequest')
  432. @response =
  433. statusCode: 200
  434. @body = "<xml>is_bad</xml>"
  435. @apiRequest.callsArgWith(1, null, @response, @body)
  436. @_parseSubscriptionXml = sinon.stub(@RecurlyWrapper, '_parseSubscriptionXml')
  437. @_parseSubscriptionXml.callsArgWith(1, null, @subscription)
  438. @call = (callback) =>
  439. @RecurlyWrapper._createCreditCardSubscription(@user, @subscriptionDetails, @recurly_token_id, callback)
  440. afterEach ->
  441. @apiRequest.restore()
  442. @_parseSubscriptionXml.restore()
  443. it 'should not produce an error', (done) ->
  444. @call (err, sub) =>
  445. expect(err).to.not.be.instanceof Error
  446. expect(err).to.equal null
  447. done()
  448. it 'should produce a subscription', (done) ->
  449. @call (err, sub) =>
  450. expect(sub).to.equal @subscription
  451. done()
  452. it 'should call apiRequest', (done) ->
  453. @call (err, sub) =>
  454. @apiRequest.callCount.should.equal 1
  455. done()
  456. it 'should call _parseSubscriptionXml', (done) ->
  457. @call (err, sub) =>
  458. @_parseSubscriptionXml.callCount.should.equal 1
  459. done()
  460. describe 'when api request produces an error', ->
  461. beforeEach ->
  462. @apiRequest.callsArgWith(1, new Error('woops'))
  463. it 'should produce an error', (done) ->
  464. @call (err, sub) =>
  465. expect(err).to.be.instanceof Error
  466. done()
  467. it 'should call apiRequest', (done) ->
  468. @call (err, sub) =>
  469. @apiRequest.callCount.should.equal 1
  470. done()
  471. it 'should not _parseSubscriptionXml', (done) ->
  472. @call (err, sub) =>
  473. @_parseSubscriptionXml.callCount.should.equal 0
  474. done()
  475. describe 'when parse xml produces an error', ->
  476. beforeEach ->
  477. @_parseSubscriptionXml.callsArgWith(1, new Error('woops'))
  478. it 'should produce an error', (done) ->
  479. @call (err, sub) =>
  480. expect(err).to.be.instanceof Error
  481. done()
  482. describe '_createPaypalSubscription', ->
  483. beforeEach ->
  484. @checkAccountExists = sinon.stub(@RecurlyWrapper._paypal, 'checkAccountExists')
  485. @createAccount = sinon.stub(@RecurlyWrapper._paypal, 'createAccount')
  486. @createBillingInfo = sinon.stub(@RecurlyWrapper._paypal, 'createBillingInfo')
  487. @setAddress = sinon.stub(@RecurlyWrapper._paypal, 'setAddress')
  488. @createSubscription = sinon.stub(@RecurlyWrapper._paypal, 'createSubscription')
  489. @user =
  490. _id: 'some_id'
  491. email: 'user@example.com'
  492. @subscriptionDetails =
  493. currencyCode: "EUR"
  494. plan_code: "some_plan_code"
  495. coupon_code: ""
  496. isPaypal: true
  497. address:
  498. address1: "addr_one"
  499. address2: "addr_two"
  500. country: "some_country"
  501. state: "some_state"
  502. zip: "some_zip"
  503. @subscription = {}
  504. @recurly_token_id = "a-token-id"
  505. # set up data callbacks
  506. user = @user
  507. subscriptionDetails = @subscriptionDetails
  508. recurly_token_id = @recurly_token_id
  509. @checkAccountExists.callsArgWith(1, null,
  510. {user, subscriptionDetails, recurly_token_id,
  511. userExists: false, account: {accountCode: 'xx'}}
  512. )
  513. @createAccount.callsArgWith(1, null,
  514. {user, subscriptionDetails, recurly_token_id,
  515. userExists: false, account: {accountCode: 'xx'}}
  516. )
  517. @createBillingInfo.callsArgWith(1, null,
  518. {user, subscriptionDetails, recurly_token_id,
  519. userExists: false, account: {accountCode: 'xx'}, billingInfo: {token_id: 'abc'}}
  520. )
  521. @setAddress.callsArgWith(1, null,
  522. {user, subscriptionDetails, recurly_token_id,
  523. userExists: false, account: {accountCode: 'xx'}, billingInfo: {token_id: 'abc'}}
  524. )
  525. @createSubscription.callsArgWith(1, null,
  526. {user, subscriptionDetails, recurly_token_id,
  527. userExists: false, account: {accountCode: 'xx'}, billingInfo: {token_id: 'abc'}, subscription: @subscription}
  528. )
  529. @call = (callback) =>
  530. @RecurlyWrapper._createPaypalSubscription @user, @subscriptionDetails, @recurly_token_id, callback
  531. afterEach ->
  532. @checkAccountExists.restore()
  533. @createAccount.restore()
  534. @createBillingInfo.restore()
  535. @setAddress.restore()
  536. @createSubscription.restore()
  537. it 'should not produce an error', (done) ->
  538. @call (err, sub) =>
  539. expect(err).to.not.be.instanceof Error
  540. done()
  541. it 'should produce a subscription object', (done) ->
  542. @call (err, sub) =>
  543. expect(sub).to.not.equal null
  544. expect(sub).to.equal @subscription
  545. done()
  546. it 'should call each of the paypal stages', (done) ->
  547. @call (err, sub) =>
  548. @checkAccountExists.callCount.should.equal 1
  549. @createAccount.callCount.should.equal 1
  550. @createBillingInfo.callCount.should.equal 1
  551. @setAddress.callCount.should.equal 1
  552. @createSubscription.callCount.should.equal 1
  553. done()
  554. describe 'when one of the paypal stages produces an error', ->
  555. beforeEach ->
  556. @createAccount.callsArgWith(1, new Error('woops'))
  557. it 'should produce an error', (done) ->
  558. @call (err, sub) =>
  559. expect(err).to.be.instanceof Error
  560. done()
  561. it 'should stop calling the paypal stages after the error', (done) ->
  562. @call (err, sub) =>
  563. @checkAccountExists.callCount.should.equal 1
  564. @createAccount.callCount.should.equal 1
  565. @createBillingInfo.callCount.should.equal 0
  566. @setAddress.callCount.should.equal 0
  567. @createSubscription.callCount.should.equal 0
  568. done()
  569. describe 'paypal actions', ->
  570. beforeEach ->
  571. @apiRequest = sinon.stub(@RecurlyWrapper, 'apiRequest')
  572. @_parseAccountXml = sinon.spy(@RecurlyWrapper, '_parseAccountXml')
  573. @_parseBillingInfoXml = sinon.spy(@RecurlyWrapper, '_parseBillingInfoXml')
  574. @_parseSubscriptionXml = sinon.spy(@RecurlyWrapper, '_parseSubscriptionXml')
  575. @cache =
  576. user: @user = {_id: 'some_id'}
  577. recurly_token_id: @recurly_token_id = "some_token"
  578. subscriptionDetails: @subscriptionDetails =
  579. currencyCode: "EUR"
  580. plan_code: "some_plan_code"
  581. coupon_code: ""
  582. isPaypal: true
  583. address:
  584. address1: "addr_one"
  585. address2: "addr_two"
  586. country: "some_country"
  587. state: "some_state"
  588. zip: "some_zip"
  589. afterEach ->
  590. @apiRequest.restore()
  591. @_parseAccountXml.restore()
  592. @_parseBillingInfoXml.restore()
  593. @_parseSubscriptionXml.restore()
  594. describe '_paypal.checkAccountExists', ->
  595. beforeEach ->
  596. @call = (callback) =>
  597. @RecurlyWrapper._paypal.checkAccountExists @cache, callback
  598. describe 'when the account exists', ->
  599. beforeEach ->
  600. resultXml = '<account><account_code>abc</account_code></account>'
  601. @apiRequest.callsArgWith(1, null, {statusCode: 200}, resultXml)
  602. it 'should not produce an error', (done) ->
  603. @call (err, result) =>
  604. expect(err).to.not.be.instanceof Error
  605. done()
  606. it 'should call apiRequest', (done) ->
  607. @call (err, result) =>
  608. @apiRequest.callCount.should.equal 1
  609. done()
  610. it 'should call _parseAccountXml', (done) ->
  611. @call (err, result) =>
  612. @RecurlyWrapper._parseAccountXml.callCount.should.equal 1
  613. done()
  614. it 'should add the account to the cumulative result', (done) ->
  615. @call (err, result) =>
  616. expect(result.account).to.not.equal null
  617. expect(result.account).to.not.equal undefined
  618. expect(result.account).to.deep.equal {
  619. account_code: 'abc'
  620. }
  621. done()
  622. it 'should set userExists to true', (done) ->
  623. @call (err, result) =>
  624. expect(result.userExists).to.equal true
  625. done()
  626. describe 'when the account does not exist', ->
  627. beforeEach ->
  628. @apiRequest.callsArgWith(1, null, {statusCode: 404}, '')
  629. it 'should not produce an error', (done) ->
  630. @call (err, result) =>
  631. expect(err).to.not.be.instanceof Error
  632. done()
  633. it 'should call apiRequest', (done) ->
  634. @call (err, result) =>
  635. @apiRequest.callCount.should.equal 1
  636. @apiRequest.firstCall.args[0].method.should.equal 'GET'
  637. done()
  638. it 'should not call _parseAccountXml', (done) ->
  639. @call (err, result) =>
  640. @RecurlyWrapper._parseAccountXml.callCount.should.equal 0
  641. done()
  642. it 'should not add the account to result', (done) ->
  643. @call (err, result) =>
  644. expect(result.account).to.equal undefined
  645. done()
  646. it 'should set userExists to false', (done) ->
  647. @call (err, result) =>
  648. expect(result.userExists).to.equal false
  649. done()
  650. describe 'when apiRequest produces an error', ->
  651. beforeEach ->
  652. @apiRequest.callsArgWith(1, new Error('woops'), {statusCode: 500})
  653. it 'should produce an error', (done) ->
  654. @call (err, result) =>
  655. expect(err).to.be.instanceof Error
  656. done()
  657. describe '_paypal.createAccount', ->
  658. beforeEach ->
  659. @call = (callback) =>
  660. @RecurlyWrapper._paypal.createAccount @cache, callback
  661. describe 'when address is missing from subscriptionDetails', ->
  662. beforeEach ->
  663. @cache.subscriptionDetails.address = null
  664. it 'should produce an error', (done) ->
  665. @call (err, result) =>
  666. expect(err).to.be.instanceof Error
  667. done()
  668. describe 'when account already exists', ->
  669. beforeEach ->
  670. @cache.userExists = true
  671. @cache.account =
  672. account_code: 'abc'
  673. it 'should not produce an error', (done) ->
  674. @call (err, result) =>
  675. expect(err).to.not.be.instanceof Error
  676. done()
  677. it 'should produce cache object', (done) ->
  678. @call (err, result) =>
  679. expect(result).to.deep.equal @cache
  680. expect(result.account).to.deep.equal {
  681. account_code: 'abc'
  682. }
  683. done()
  684. it 'should not call apiRequest', (done) ->
  685. @call (err, result) =>
  686. @apiRequest.callCount.should.equal 0
  687. done()
  688. it 'should not call _parseAccountXml', (done) ->
  689. @call (err, result) =>
  690. @RecurlyWrapper._parseAccountXml.callCount.should.equal 0
  691. done()
  692. describe 'when account does not exist', ->
  693. beforeEach ->
  694. @cache.userExists = false
  695. resultXml = '<account><account_code>abc</account_code></account>'
  696. @apiRequest.callsArgWith(1, null, {statusCode: 200}, resultXml)
  697. it 'should not produce an error', (done) ->
  698. @call (err, result) =>
  699. expect(err).to.not.be.instanceof Error
  700. done()
  701. it 'should call apiRequest', (done) ->
  702. @call (err, result) =>
  703. @apiRequest.callCount.should.equal 1
  704. @apiRequest.firstCall.args[0].method.should.equal 'POST'
  705. done()
  706. it 'should call _parseAccountXml', (done) ->
  707. @call (err, result) =>
  708. @RecurlyWrapper._parseAccountXml.callCount.should.equal 1
  709. done()
  710. describe 'when apiRequest produces an error', ->
  711. beforeEach ->
  712. @apiRequest.callsArgWith(1, new Error('woops'), {statusCode: 500})
  713. it 'should produce an error', (done) ->
  714. @call (err, result) =>
  715. expect(err).to.be.instanceof Error
  716. done()
  717. describe '_paypal.createBillingInfo', ->
  718. beforeEach ->
  719. @cache.account =
  720. account_code: 'abc'
  721. @call = (callback) =>
  722. @RecurlyWrapper._paypal.createBillingInfo @cache, callback
  723. describe 'when account_code is missing from cache', ->
  724. beforeEach ->
  725. @cache.account.account_code = null
  726. it 'should produce an error', (done) ->
  727. @call (err, result) =>
  728. expect(err).to.be.instanceof Error
  729. done()
  730. describe 'when all goes well', ->
  731. beforeEach ->
  732. resultXml = '<billing_info><a>1</a></billing_info>'
  733. @apiRequest.callsArgWith(1, null, {statusCode: 200}, resultXml)
  734. it 'should not produce an error', (done) ->
  735. @call (err, result) =>
  736. expect(err).to.not.be.instanceof Error
  737. done()
  738. it 'should call apiRequest', (done) ->
  739. @call (err, result) =>
  740. @apiRequest.callCount.should.equal 1
  741. @apiRequest.firstCall.args[0].method.should.equal 'POST'
  742. done()
  743. it 'should call _parseBillingInfoXml', (done) ->
  744. @call (err, result) =>
  745. @RecurlyWrapper._parseBillingInfoXml.callCount.should.equal 1
  746. done()
  747. it 'should set billingInfo on cache', (done) ->
  748. @call (err, result) =>
  749. expect(result.billingInfo).to.deep.equal {
  750. a: "1"
  751. }
  752. done()
  753. describe 'when apiRequest produces an error', ->
  754. beforeEach ->
  755. @apiRequest.callsArgWith(1, new Error('woops'), {statusCode: 500})
  756. it 'should produce an error', (done) ->
  757. @call (err, result) =>
  758. expect(err).to.be.instanceof Error
  759. done()
  760. describe '_paypal.setAddress', ->
  761. beforeEach ->
  762. @cache.account =
  763. account_code: 'abc'
  764. @cache.billingInfo = {}
  765. @call = (callback) =>
  766. @RecurlyWrapper._paypal.setAddress @cache, callback
  767. describe 'when account_code is missing from cache', ->
  768. beforeEach ->
  769. @cache.account.account_code = null
  770. it 'should produce an error', (done) ->
  771. @call (err, result) =>
  772. expect(err).to.be.instanceof Error
  773. done()
  774. describe 'when address is missing from subscriptionDetails', ->
  775. beforeEach ->
  776. @cache.subscriptionDetails.address = null
  777. it 'should produce an error', (done) ->
  778. @call (err, result) =>
  779. expect(err).to.be.instanceof Error
  780. done()
  781. describe 'when all goes well', ->
  782. beforeEach ->
  783. resultXml = '<billing_info><city>London</city></billing_info>'
  784. @apiRequest.callsArgWith(1, null, {statusCode: 200}, resultXml)
  785. it 'should not produce an error', (done) ->
  786. @call (err, result) =>
  787. expect(err).to.not.be.instanceof Error
  788. done()
  789. it 'should call apiRequest', (done) ->
  790. @call (err, result) =>
  791. @apiRequest.callCount.should.equal 1
  792. @apiRequest.firstCall.args[0].method.should.equal 'PUT'
  793. done()
  794. it 'should call _parseBillingInfoXml', (done) ->
  795. @call (err, result) =>
  796. @RecurlyWrapper._parseBillingInfoXml.callCount.should.equal 1
  797. done()
  798. it 'should set billingInfo on cache', (done) ->
  799. @call (err, result) =>
  800. expect(result.billingInfo).to.deep.equal {
  801. city: 'London'
  802. }
  803. done()
  804. describe 'when apiRequest produces an error', ->
  805. beforeEach ->
  806. @apiRequest.callsArgWith(1, new Error('woops'), {statusCode: 500})
  807. it 'should produce an error', (done) ->
  808. @call (err, result) =>
  809. expect(err).to.be.instanceof Error
  810. done()
  811. describe '_paypal.createSubscription', ->
  812. beforeEach ->
  813. @cache.account =
  814. account_code: 'abc'
  815. @cache.billingInfo = {}
  816. @call = (callback) =>
  817. @RecurlyWrapper._paypal.createSubscription @cache, callback
  818. describe 'when all goes well', ->
  819. beforeEach ->
  820. resultXml = '<subscription><a>1</a></subscription>'
  821. @apiRequest.callsArgWith(1, null, {statusCode: 200}, resultXml)
  822. it 'should not produce an error', (done) ->
  823. @call (err, result) =>
  824. expect(err).to.not.be.instanceof Error
  825. done()
  826. it 'should call apiRequest', (done) ->
  827. @call (err, result) =>
  828. @apiRequest.callCount.should.equal 1
  829. @apiRequest.firstCall.args[0].method.should.equal 'POST'
  830. done()
  831. it 'should call _parseSubscriptionXml', (done) ->
  832. @call (err, result) =>
  833. @RecurlyWrapper._parseSubscriptionXml.callCount.should.equal 1
  834. done()
  835. it 'should set subscription on cache', (done) ->
  836. @call (err, result) =>
  837. expect(result.subscription).to.deep.equal {
  838. a: "1"
  839. }
  840. done()
  841. describe 'when apiRequest produces an error', ->
  842. beforeEach ->
  843. @apiRequest.callsArgWith(1, new Error('woops'), {statusCode: 500})
  844. it 'should produce an error', (done) ->
  845. @call (err, result) =>
  846. expect(err).to.be.instanceof Error
  847. done()