RecurlyWrapperTests.coffee 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966
  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. tk.freeze Date.now() # freeze the time for these tests
  114. @RecurlyWrapper = RecurlyWrapper = SandboxedModule.require modulePath, requires:
  115. "settings-sharelatex": @settings
  116. "logger-sharelatex":
  117. err: sinon.stub()
  118. error: sinon.stub()
  119. log: sinon.stub()
  120. "request": sinon.stub()
  121. after ->
  122. tk.reset()
  123. describe "getSubscription", ->
  124. describe "with proper subscription id", ->
  125. before ->
  126. @apiRequest = sinon.stub(@RecurlyWrapper, "apiRequest", mockApiRequest)
  127. @RecurlyWrapper.getSubscription "44f83d7cba354d5b84812419f923ea96", (error, recurlySubscription) =>
  128. @recurlySubscription = recurlySubscription
  129. after ->
  130. @RecurlyWrapper.apiRequest.restore()
  131. it "should look up the subscription at the normal API end point", ->
  132. @apiRequest.args[0][0].url.should.equal "subscriptions/44f83d7cba354d5b84812419f923ea96"
  133. it "should return the subscription", ->
  134. @recurlySubscription.uuid.should.equal "44f83d7cba354d5b84812419f923ea96"
  135. describe "with ReculyJS token", ->
  136. before ->
  137. @apiRequest = sinon.stub(@RecurlyWrapper, "apiRequest", mockApiRequest)
  138. @RecurlyWrapper.getSubscription "70db44b10f5f4b238669480c9903f6f5", {recurlyJsResult: true}, (error, recurlySubscription) =>
  139. @recurlySubscription = recurlySubscription
  140. after ->
  141. @RecurlyWrapper.apiRequest.restore()
  142. it "should return the subscription", ->
  143. @recurlySubscription.uuid.should.equal "44f83d7cba354d5b84812419f923ea96"
  144. it "should look up the subscription at the RecurlyJS API end point", ->
  145. @apiRequest.args[0][0].url.should.equal "recurly_js/result/70db44b10f5f4b238669480c9903f6f5"
  146. describe "with includeAccount", ->
  147. beforeEach ->
  148. @apiRequest = sinon.stub(@RecurlyWrapper, "apiRequest", mockApiRequest)
  149. @RecurlyWrapper.getSubscription "44f83d7cba354d5b84812419f923ea96", {includeAccount: true}, (error, recurlySubscription) =>
  150. @recurlySubscription = recurlySubscription
  151. afterEach ->
  152. @RecurlyWrapper.apiRequest.restore()
  153. it "should request the account from the API", ->
  154. @apiRequest.args[1][0].url.should.equal "accounts/104"
  155. it "should populate the account attribute", ->
  156. @recurlySubscription.account.account_code.should.equal "104"
  157. describe "updateSubscription", ->
  158. beforeEach (done) ->
  159. @recurlySubscriptionId = "subscription-id-123"
  160. @apiRequest = sinon.stub @RecurlyWrapper, "apiRequest", (options, callback) =>
  161. @requestOptions = options
  162. callback null, {}, fixtures["subscriptions/44f83d7cba354d5b84812419f923ea96"]
  163. @RecurlyWrapper.updateSubscription @recurlySubscriptionId, { plan_code : "silver", timeframe: "now" }, (error, recurlySubscription) =>
  164. @recurlySubscription = recurlySubscription
  165. done()
  166. afterEach ->
  167. @RecurlyWrapper.apiRequest.restore()
  168. it "should send an update request to the API", ->
  169. @apiRequest.called.should.equal true
  170. @requestOptions.body.should.equal """
  171. <subscription>
  172. <plan_code>silver</plan_code>
  173. <timeframe>now</timeframe>
  174. </subscription>
  175. """
  176. @requestOptions.url.should.equal "subscriptions/#{@recurlySubscriptionId}"
  177. @requestOptions.method.should.equal "put"
  178. it "should return the updated subscription", ->
  179. should.exist @recurlySubscription
  180. @recurlySubscription.plan.plan_code.should.equal "gold"
  181. describe "cancelSubscription", ->
  182. beforeEach (done) ->
  183. @recurlySubscriptionId = "subscription-id-123"
  184. @apiRequest = sinon.stub @RecurlyWrapper, "apiRequest", (options, callback) =>
  185. options.url.should.equal "subscriptions/#{@recurlySubscriptionId}/cancel"
  186. options.method.should.equal "put"
  187. callback()
  188. @RecurlyWrapper.cancelSubscription(@recurlySubscriptionId, done)
  189. afterEach ->
  190. @RecurlyWrapper.apiRequest.restore()
  191. it "should send a cancel request to the API", ->
  192. @apiRequest.called.should.equal true
  193. describe 'when the subscription is already cancelled', ->
  194. beforeEach ->
  195. @RecurlyWrapper.apiRequest.restore()
  196. @recurlySubscriptionId = "subscription-id-123"
  197. @apiRequest = sinon.stub @RecurlyWrapper, "apiRequest", (options, callback) =>
  198. callback(new Error('woops'), {}, "<error><description>A canceled subscription can't transition to canceled</description></error>")
  199. it 'should not produce an error', (done) ->
  200. @RecurlyWrapper.cancelSubscription @recurlySubscriptionId, (err) =>
  201. expect(err).to.equal null
  202. done()
  203. describe "reactivateSubscription", ->
  204. beforeEach (done) ->
  205. @recurlySubscriptionId = "subscription-id-123"
  206. @apiRequest = sinon.stub @RecurlyWrapper, "apiRequest", (options, callback) =>
  207. options.url.should.equal "subscriptions/#{@recurlySubscriptionId}/reactivate"
  208. options.method.should.equal "put"
  209. callback()
  210. @RecurlyWrapper.reactivateSubscription(@recurlySubscriptionId, done)
  211. afterEach ->
  212. @RecurlyWrapper.apiRequest.restore()
  213. it "should send a cancel request to the API", ->
  214. @apiRequest.called.should.equal true
  215. describe "redeemCoupon", ->
  216. beforeEach (done) ->
  217. @recurlyAccountId = "account-id-123"
  218. @coupon_code = "312321312"
  219. @apiRequest = sinon.stub @RecurlyWrapper, "apiRequest", (options, callback) =>
  220. options.url.should.equal "coupons/#{@coupon_code}/redeem"
  221. options.body.indexOf("<account_code>#{@recurlyAccountId}</account_code>").should.not.equal -1
  222. options.body.indexOf("<currency>USD</currency>").should.not.equal -1
  223. options.method.should.equal "post"
  224. callback()
  225. @RecurlyWrapper.redeemCoupon(@recurlyAccountId, @coupon_code, done)
  226. afterEach ->
  227. @RecurlyWrapper.apiRequest.restore()
  228. it "should send the request to redem the coupon", ->
  229. @apiRequest.called.should.equal true
  230. describe "_addressToXml", ->
  231. beforeEach ->
  232. @address =
  233. address1: "addr_one"
  234. address2: "addr_two"
  235. country: "some_country"
  236. state: "some_state"
  237. postal_code: "some_zip"
  238. nonsenseKey: "rubbish"
  239. it 'should generate the correct xml', () ->
  240. result = @RecurlyWrapper._addressToXml @address
  241. should.equal(
  242. result,
  243. """
  244. <billing_info>
  245. <address1>addr_one</address1>
  246. <address2 nil="nil">addr_two</address2>
  247. <country>some_country</country>
  248. <state>some_state</state>
  249. <zip>some_zip</zip>
  250. </billing_info>\n
  251. """
  252. )
  253. describe 'createSubscription', ->
  254. beforeEach ->
  255. @user =
  256. _id: 'some_id'
  257. email: 'user@example.com'
  258. @subscriptionDetails =
  259. currencyCode: "EUR"
  260. plan_code: "some_plan_code"
  261. coupon_code: ""
  262. isPaypal: true
  263. address:
  264. address1: "addr_one"
  265. address2: "addr_two"
  266. country: "some_country"
  267. state: "some_state"
  268. zip: "some_zip"
  269. @subscription = {}
  270. @recurly_token_id = "a-token-id"
  271. @call = (callback) =>
  272. @RecurlyWrapper.createSubscription(@user, @subscriptionDetails, @recurly_token_id, callback)
  273. describe 'when paypal', ->
  274. beforeEach ->
  275. @subscriptionDetails.isPaypal = true
  276. @_createPaypalSubscription = sinon.stub(@RecurlyWrapper, '_createPaypalSubscription')
  277. @_createPaypalSubscription.callsArgWith(3, null, @subscription)
  278. afterEach ->
  279. @_createPaypalSubscription.restore()
  280. it 'should not produce an error', (done) ->
  281. @call (err, sub) =>
  282. expect(err).to.equal null
  283. expect(err).to.not.be.instanceof Error
  284. done()
  285. it 'should produce a subscription object', (done) ->
  286. @call (err, sub) =>
  287. expect(sub).to.deep.equal @subscription
  288. done()
  289. it 'should call _createPaypalSubscription', (done) ->
  290. @call (err, sub) =>
  291. @_createPaypalSubscription.callCount.should.equal 1
  292. done()
  293. describe "when _createPaypalSubscription produces an error", ->
  294. beforeEach ->
  295. @_createPaypalSubscription.callsArgWith(3, new Error('woops'))
  296. it 'should produce an error', (done) ->
  297. @call (err, sub) =>
  298. expect(err).to.be.instanceof Error
  299. done()
  300. describe 'when not paypal', ->
  301. beforeEach ->
  302. @subscriptionDetails.isPaypal = false
  303. @_createCreditCardSubscription = sinon.stub(@RecurlyWrapper, '_createCreditCardSubscription')
  304. @_createCreditCardSubscription.callsArgWith(3, null, @subscription)
  305. afterEach ->
  306. @_createCreditCardSubscription.restore()
  307. it 'should not produce an error', (done) ->
  308. @call (err, sub) =>
  309. expect(err).to.equal null
  310. expect(err).to.not.be.instanceof Error
  311. done()
  312. it 'should produce a subscription object', (done) ->
  313. @call (err, sub) =>
  314. expect(sub).to.deep.equal @subscription
  315. done()
  316. it 'should call _createCreditCardSubscription', (done) ->
  317. @call (err, sub) =>
  318. @_createCreditCardSubscription.callCount.should.equal 1
  319. done()
  320. describe "when _createCreditCardSubscription produces an error", ->
  321. beforeEach ->
  322. @_createCreditCardSubscription.callsArgWith(3, new Error('woops'))
  323. it 'should produce an error', (done) ->
  324. @call (err, sub) =>
  325. expect(err).to.be.instanceof Error
  326. done()
  327. describe '_createCreditCardSubscription', ->
  328. beforeEach ->
  329. @user =
  330. _id: 'some_id'
  331. email: 'user@example.com'
  332. @subscriptionDetails =
  333. currencyCode: "EUR"
  334. plan_code: "some_plan_code"
  335. coupon_code: ""
  336. isPaypal: true
  337. address:
  338. address1: "addr_one"
  339. address2: "addr_two"
  340. country: "some_country"
  341. state: "some_state"
  342. zip: "some_zip"
  343. @subscription = {}
  344. @recurly_token_id = "a-token-id"
  345. @apiRequest = sinon.stub(@RecurlyWrapper, 'apiRequest')
  346. @response =
  347. statusCode: 200
  348. @body = "<xml>is_bad</xml>"
  349. @apiRequest.callsArgWith(1, null, @response, @body)
  350. @_parseSubscriptionXml = sinon.stub(@RecurlyWrapper, '_parseSubscriptionXml')
  351. @_parseSubscriptionXml.callsArgWith(1, null, @subscription)
  352. @call = (callback) =>
  353. @RecurlyWrapper._createCreditCardSubscription(@user, @subscriptionDetails, @recurly_token_id, callback)
  354. afterEach ->
  355. @apiRequest.restore()
  356. @_parseSubscriptionXml.restore()
  357. it 'should not produce an error', (done) ->
  358. @call (err, sub) =>
  359. expect(err).to.not.be.instanceof Error
  360. expect(err).to.equal null
  361. done()
  362. it 'should produce a subscription', (done) ->
  363. @call (err, sub) =>
  364. expect(sub).to.equal @subscription
  365. done()
  366. it 'should call apiRequest', (done) ->
  367. @call (err, sub) =>
  368. @apiRequest.callCount.should.equal 1
  369. done()
  370. it 'should call _parseSubscriptionXml', (done) ->
  371. @call (err, sub) =>
  372. @_parseSubscriptionXml.callCount.should.equal 1
  373. done()
  374. describe 'when api request produces an error', ->
  375. beforeEach ->
  376. @apiRequest.callsArgWith(1, new Error('woops'))
  377. it 'should produce an error', (done) ->
  378. @call (err, sub) =>
  379. expect(err).to.be.instanceof Error
  380. done()
  381. it 'should call apiRequest', (done) ->
  382. @call (err, sub) =>
  383. @apiRequest.callCount.should.equal 1
  384. done()
  385. it 'should not _parseSubscriptionXml', (done) ->
  386. @call (err, sub) =>
  387. @_parseSubscriptionXml.callCount.should.equal 0
  388. done()
  389. describe 'when parse xml produces an error', ->
  390. beforeEach ->
  391. @_parseSubscriptionXml.callsArgWith(1, new Error('woops'))
  392. it 'should produce an error', (done) ->
  393. @call (err, sub) =>
  394. expect(err).to.be.instanceof Error
  395. done()
  396. describe '_createPaypalSubscription', ->
  397. beforeEach ->
  398. @checkAccountExists = sinon.stub(@RecurlyWrapper._paypal, 'checkAccountExists')
  399. @createAccount = sinon.stub(@RecurlyWrapper._paypal, 'createAccount')
  400. @createBillingInfo = sinon.stub(@RecurlyWrapper._paypal, 'createBillingInfo')
  401. @setAddress = sinon.stub(@RecurlyWrapper._paypal, 'setAddress')
  402. @createSubscription = sinon.stub(@RecurlyWrapper._paypal, 'createSubscription')
  403. @user =
  404. _id: 'some_id'
  405. email: 'user@example.com'
  406. @subscriptionDetails =
  407. currencyCode: "EUR"
  408. plan_code: "some_plan_code"
  409. coupon_code: ""
  410. isPaypal: true
  411. address:
  412. address1: "addr_one"
  413. address2: "addr_two"
  414. country: "some_country"
  415. state: "some_state"
  416. zip: "some_zip"
  417. @subscription = {}
  418. @recurly_token_id = "a-token-id"
  419. # set up data callbacks
  420. user = @user
  421. subscriptionDetails = @subscriptionDetails
  422. recurly_token_id = @recurly_token_id
  423. @checkAccountExists.callsArgWith(1, null,
  424. {user, subscriptionDetails, recurly_token_id,
  425. userExists: false, account: {accountCode: 'xx'}}
  426. )
  427. @createAccount.callsArgWith(1, null,
  428. {user, subscriptionDetails, recurly_token_id,
  429. userExists: false, account: {accountCode: 'xx'}}
  430. )
  431. @createBillingInfo.callsArgWith(1, null,
  432. {user, subscriptionDetails, recurly_token_id,
  433. userExists: false, account: {accountCode: 'xx'}, billingInfo: {token_id: 'abc'}}
  434. )
  435. @setAddress.callsArgWith(1, null,
  436. {user, subscriptionDetails, recurly_token_id,
  437. userExists: false, account: {accountCode: 'xx'}, billingInfo: {token_id: 'abc'}}
  438. )
  439. @createSubscription.callsArgWith(1, null,
  440. {user, subscriptionDetails, recurly_token_id,
  441. userExists: false, account: {accountCode: 'xx'}, billingInfo: {token_id: 'abc'}, subscription: @subscription}
  442. )
  443. @call = (callback) =>
  444. @RecurlyWrapper._createPaypalSubscription @user, @subscriptionDetails, @recurly_token_id, callback
  445. afterEach ->
  446. @checkAccountExists.restore()
  447. @createAccount.restore()
  448. @createBillingInfo.restore()
  449. @setAddress.restore()
  450. @createSubscription.restore()
  451. it 'should not produce an error', (done) ->
  452. @call (err, sub) =>
  453. expect(err).to.not.be.instanceof Error
  454. done()
  455. it 'should produce a subscription object', (done) ->
  456. @call (err, sub) =>
  457. expect(sub).to.not.equal null
  458. expect(sub).to.equal @subscription
  459. done()
  460. it 'should call each of the paypal stages', (done) ->
  461. @call (err, sub) =>
  462. @checkAccountExists.callCount.should.equal 1
  463. @createAccount.callCount.should.equal 1
  464. @createBillingInfo.callCount.should.equal 1
  465. @setAddress.callCount.should.equal 1
  466. @createSubscription.callCount.should.equal 1
  467. done()
  468. describe 'when one of the paypal stages produces an error', ->
  469. beforeEach ->
  470. @createAccount.callsArgWith(1, new Error('woops'))
  471. it 'should produce an error', (done) ->
  472. @call (err, sub) =>
  473. expect(err).to.be.instanceof Error
  474. done()
  475. it 'should stop calling the paypal stages after the error', (done) ->
  476. @call (err, sub) =>
  477. @checkAccountExists.callCount.should.equal 1
  478. @createAccount.callCount.should.equal 1
  479. @createBillingInfo.callCount.should.equal 0
  480. @setAddress.callCount.should.equal 0
  481. @createSubscription.callCount.should.equal 0
  482. done()
  483. describe 'paypal actions', ->
  484. beforeEach ->
  485. @apiRequest = sinon.stub(@RecurlyWrapper, 'apiRequest')
  486. @_parseAccountXml = sinon.spy(@RecurlyWrapper, '_parseAccountXml')
  487. @_parseBillingInfoXml = sinon.spy(@RecurlyWrapper, '_parseBillingInfoXml')
  488. @_parseSubscriptionXml = sinon.spy(@RecurlyWrapper, '_parseSubscriptionXml')
  489. @cache =
  490. user: @user = {_id: 'some_id'}
  491. recurly_token_id: @recurly_token_id = "some_token"
  492. subscriptionDetails: @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. afterEach ->
  504. @apiRequest.restore()
  505. @_parseAccountXml.restore()
  506. @_parseBillingInfoXml.restore()
  507. @_parseSubscriptionXml.restore()
  508. describe '_paypal.checkAccountExists', ->
  509. beforeEach ->
  510. @call = (callback) =>
  511. @RecurlyWrapper._paypal.checkAccountExists @cache, callback
  512. describe 'when the account exists', ->
  513. beforeEach ->
  514. resultXml = '<account><account_code>abc</account_code></account>'
  515. @apiRequest.callsArgWith(1, null, {statusCode: 200}, resultXml)
  516. it 'should not produce an error', (done) ->
  517. @call (err, result) =>
  518. expect(err).to.not.be.instanceof Error
  519. done()
  520. it 'should call apiRequest', (done) ->
  521. @call (err, result) =>
  522. @apiRequest.callCount.should.equal 1
  523. done()
  524. it 'should call _parseAccountXml', (done) ->
  525. @call (err, result) =>
  526. @RecurlyWrapper._parseAccountXml.callCount.should.equal 1
  527. done()
  528. it 'should add the account to the cumulative result', (done) ->
  529. @call (err, result) =>
  530. expect(result.account).to.not.equal null
  531. expect(result.account).to.not.equal undefined
  532. expect(result.account).to.deep.equal {
  533. account_code: 'abc'
  534. }
  535. done()
  536. it 'should set userExists to true', (done) ->
  537. @call (err, result) =>
  538. expect(result.userExists).to.equal true
  539. done()
  540. describe 'when the account does not exist', ->
  541. beforeEach ->
  542. @apiRequest.callsArgWith(1, null, {statusCode: 404}, '')
  543. it 'should not produce an error', (done) ->
  544. @call (err, result) =>
  545. expect(err).to.not.be.instanceof Error
  546. done()
  547. it 'should call apiRequest', (done) ->
  548. @call (err, result) =>
  549. @apiRequest.callCount.should.equal 1
  550. @apiRequest.firstCall.args[0].method.should.equal 'GET'
  551. done()
  552. it 'should not call _parseAccountXml', (done) ->
  553. @call (err, result) =>
  554. @RecurlyWrapper._parseAccountXml.callCount.should.equal 0
  555. done()
  556. it 'should not add the account to result', (done) ->
  557. @call (err, result) =>
  558. expect(result.account).to.equal undefined
  559. done()
  560. it 'should set userExists to false', (done) ->
  561. @call (err, result) =>
  562. expect(result.userExists).to.equal false
  563. done()
  564. describe 'when apiRequest produces an error', ->
  565. beforeEach ->
  566. @apiRequest.callsArgWith(1, new Error('woops'), {statusCode: 500})
  567. it 'should produce an error', (done) ->
  568. @call (err, result) =>
  569. expect(err).to.be.instanceof Error
  570. done()
  571. describe '_paypal.createAccount', ->
  572. beforeEach ->
  573. @call = (callback) =>
  574. @RecurlyWrapper._paypal.createAccount @cache, callback
  575. describe 'when address is missing from subscriptionDetails', ->
  576. beforeEach ->
  577. @cache.subscriptionDetails.address = null
  578. it 'should produce an error', (done) ->
  579. @call (err, result) =>
  580. expect(err).to.be.instanceof Error
  581. done()
  582. describe 'when account already exists', ->
  583. beforeEach ->
  584. @cache.userExists = true
  585. @cache.account =
  586. account_code: 'abc'
  587. it 'should not produce an error', (done) ->
  588. @call (err, result) =>
  589. expect(err).to.not.be.instanceof Error
  590. done()
  591. it 'should produce cache object', (done) ->
  592. @call (err, result) =>
  593. expect(result).to.deep.equal @cache
  594. expect(result.account).to.deep.equal {
  595. account_code: 'abc'
  596. }
  597. done()
  598. it 'should not call apiRequest', (done) ->
  599. @call (err, result) =>
  600. @apiRequest.callCount.should.equal 0
  601. done()
  602. it 'should not call _parseAccountXml', (done) ->
  603. @call (err, result) =>
  604. @RecurlyWrapper._parseAccountXml.callCount.should.equal 0
  605. done()
  606. describe 'when account does not exist', ->
  607. beforeEach ->
  608. @cache.userExists = false
  609. resultXml = '<account><account_code>abc</account_code></account>'
  610. @apiRequest.callsArgWith(1, null, {statusCode: 200}, resultXml)
  611. it 'should not produce an error', (done) ->
  612. @call (err, result) =>
  613. expect(err).to.not.be.instanceof Error
  614. done()
  615. it 'should call apiRequest', (done) ->
  616. @call (err, result) =>
  617. @apiRequest.callCount.should.equal 1
  618. @apiRequest.firstCall.args[0].method.should.equal 'POST'
  619. done()
  620. it 'should call _parseAccountXml', (done) ->
  621. @call (err, result) =>
  622. @RecurlyWrapper._parseAccountXml.callCount.should.equal 1
  623. done()
  624. describe 'when apiRequest produces an error', ->
  625. beforeEach ->
  626. @apiRequest.callsArgWith(1, new Error('woops'), {statusCode: 500})
  627. it 'should produce an error', (done) ->
  628. @call (err, result) =>
  629. expect(err).to.be.instanceof Error
  630. done()
  631. describe '_paypal.createBillingInfo', ->
  632. beforeEach ->
  633. @cache.account =
  634. account_code: 'abc'
  635. @call = (callback) =>
  636. @RecurlyWrapper._paypal.createBillingInfo @cache, callback
  637. describe 'when account_code is missing from cache', ->
  638. beforeEach ->
  639. @cache.account.account_code = null
  640. it 'should produce an error', (done) ->
  641. @call (err, result) =>
  642. expect(err).to.be.instanceof Error
  643. done()
  644. describe 'when all goes well', ->
  645. beforeEach ->
  646. resultXml = '<billing_info><a>1</a></billing_info>'
  647. @apiRequest.callsArgWith(1, null, {statusCode: 200}, resultXml)
  648. it 'should not produce an error', (done) ->
  649. @call (err, result) =>
  650. expect(err).to.not.be.instanceof Error
  651. done()
  652. it 'should call apiRequest', (done) ->
  653. @call (err, result) =>
  654. @apiRequest.callCount.should.equal 1
  655. @apiRequest.firstCall.args[0].method.should.equal 'POST'
  656. done()
  657. it 'should call _parseBillingInfoXml', (done) ->
  658. @call (err, result) =>
  659. @RecurlyWrapper._parseBillingInfoXml.callCount.should.equal 1
  660. done()
  661. it 'should set billingInfo on cache', (done) ->
  662. @call (err, result) =>
  663. expect(result.billingInfo).to.deep.equal {
  664. a: "1"
  665. }
  666. done()
  667. describe 'when apiRequest produces an error', ->
  668. beforeEach ->
  669. @apiRequest.callsArgWith(1, new Error('woops'), {statusCode: 500})
  670. it 'should produce an error', (done) ->
  671. @call (err, result) =>
  672. expect(err).to.be.instanceof Error
  673. done()
  674. describe '_paypal.setAddress', ->
  675. beforeEach ->
  676. @cache.account =
  677. account_code: 'abc'
  678. @cache.billingInfo = {}
  679. @call = (callback) =>
  680. @RecurlyWrapper._paypal.setAddress @cache, callback
  681. describe 'when account_code is missing from cache', ->
  682. beforeEach ->
  683. @cache.account.account_code = null
  684. it 'should produce an error', (done) ->
  685. @call (err, result) =>
  686. expect(err).to.be.instanceof Error
  687. done()
  688. describe 'when address is missing from subscriptionDetails', ->
  689. beforeEach ->
  690. @cache.subscriptionDetails.address = null
  691. it 'should produce an error', (done) ->
  692. @call (err, result) =>
  693. expect(err).to.be.instanceof Error
  694. done()
  695. describe 'when all goes well', ->
  696. beforeEach ->
  697. resultXml = '<billing_info><city>London</city></billing_info>'
  698. @apiRequest.callsArgWith(1, null, {statusCode: 200}, resultXml)
  699. it 'should not produce an error', (done) ->
  700. @call (err, result) =>
  701. expect(err).to.not.be.instanceof Error
  702. done()
  703. it 'should call apiRequest', (done) ->
  704. @call (err, result) =>
  705. @apiRequest.callCount.should.equal 1
  706. @apiRequest.firstCall.args[0].method.should.equal 'PUT'
  707. done()
  708. it 'should call _parseBillingInfoXml', (done) ->
  709. @call (err, result) =>
  710. @RecurlyWrapper._parseBillingInfoXml.callCount.should.equal 1
  711. done()
  712. it 'should set billingInfo on cache', (done) ->
  713. @call (err, result) =>
  714. expect(result.billingInfo).to.deep.equal {
  715. city: 'London'
  716. }
  717. done()
  718. describe 'when apiRequest produces an error', ->
  719. beforeEach ->
  720. @apiRequest.callsArgWith(1, new Error('woops'), {statusCode: 500})
  721. it 'should produce an error', (done) ->
  722. @call (err, result) =>
  723. expect(err).to.be.instanceof Error
  724. done()
  725. describe '_paypal.createSubscription', ->
  726. beforeEach ->
  727. @cache.account =
  728. account_code: 'abc'
  729. @cache.billingInfo = {}
  730. @call = (callback) =>
  731. @RecurlyWrapper._paypal.createSubscription @cache, callback
  732. describe 'when all goes well', ->
  733. beforeEach ->
  734. resultXml = '<subscription><a>1</a></subscription>'
  735. @apiRequest.callsArgWith(1, null, {statusCode: 200}, resultXml)
  736. it 'should not produce an error', (done) ->
  737. @call (err, result) =>
  738. expect(err).to.not.be.instanceof Error
  739. done()
  740. it 'should call apiRequest', (done) ->
  741. @call (err, result) =>
  742. @apiRequest.callCount.should.equal 1
  743. @apiRequest.firstCall.args[0].method.should.equal 'POST'
  744. done()
  745. it 'should call _parseSubscriptionXml', (done) ->
  746. @call (err, result) =>
  747. @RecurlyWrapper._parseSubscriptionXml.callCount.should.equal 1
  748. done()
  749. it 'should set subscription on cache', (done) ->
  750. @call (err, result) =>
  751. expect(result.subscription).to.deep.equal {
  752. a: "1"
  753. }
  754. done()
  755. describe 'when apiRequest produces an error', ->
  756. beforeEach ->
  757. @apiRequest.callsArgWith(1, new Error('woops'), {statusCode: 500})
  758. it 'should produce an error', (done) ->
  759. @call (err, result) =>
  760. expect(err).to.be.instanceof Error
  761. done()
  762. describe "listAccountActiveSubscriptions", ->
  763. beforeEach ->
  764. @user_id = "mock-user-id"
  765. @callback = sinon.stub()
  766. @RecurlyWrapper.apiRequest = sinon.stub().yields(null, @response = {"mock": "response"}, @body = "<mock body/>")
  767. @RecurlyWrapper._parseSubscriptionsXml = sinon.stub().yields(null, @subscriptions = ["mock", "subscriptions"])
  768. describe "with an account", ->
  769. beforeEach ->
  770. @RecurlyWrapper.listAccountActiveSubscriptions @user_id, @callback
  771. it "should send a request to Recurly", ->
  772. @RecurlyWrapper.apiRequest
  773. .calledWith({
  774. url: "accounts/#{@user_id}/subscriptions"
  775. qs:
  776. state: "active"
  777. expect404: true
  778. })
  779. .should.equal true
  780. it "should return the subscriptions", ->
  781. @callback.calledWith(null, @subscriptions).should.equal true
  782. describe "without an account", ->
  783. beforeEach ->
  784. @response.statusCode = 404
  785. @RecurlyWrapper.listAccountActiveSubscriptions @user_id, @callback
  786. it "should return an empty array of subscriptions", ->
  787. @callback.calledWith(null, []).should.equal true