RecurlyWrapperTests.coffee 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. should = require('chai').should()
  2. sinon = require 'sinon'
  3. crypto = require 'crypto'
  4. querystring = require 'querystring'
  5. RecurlyWrapper = require "../../../../app/js/Features/Subscription/RecurlyWrapper"
  6. Settings = require "settings-sharelatex"
  7. tk = require("timekeeper")
  8. fixtures =
  9. "subscriptions/44f83d7cba354d5b84812419f923ea96":
  10. "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
  11. "<subscription href=\"https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96\">" +
  12. " <account href=\"https://api.recurly.com/v2/accounts/104\"/>" +
  13. " <plan href=\"https://api.recurly.com/v2/plans/gold\">" +
  14. " <plan_code>gold</plan_code>" +
  15. " <name>Gold plan</name>" +
  16. " </plan>" +
  17. " <uuid>44f83d7cba354d5b84812419f923ea96</uuid>" +
  18. " <state>active</state>" +
  19. " <unit_amount_in_cents type=\"integer\">800</unit_amount_in_cents>" +
  20. " <currency>EUR</currency>" +
  21. " <quantity type=\"integer\">1</quantity>" +
  22. " <activated_at type=\"datetime\">2011-05-27T07:00:00Z</activated_at>" +
  23. " <canceled_at nil=\"nil\"></canceled_at>" +
  24. " <expires_at nil=\"nil\"></expires_at>" +
  25. " <current_period_started_at type=\"datetime\">2011-06-27T07:00:00Z</current_period_started_at>" +
  26. " <current_period_ends_at type=\"datetime\">2011-07-27T07:00:00Z</current_period_ends_at>" +
  27. " <trial_started_at nil=\"nil\"></trial_started_at>" +
  28. " <trial_ends_at nil=\"nil\"></trial_ends_at>" +
  29. " <subscription_add_ons type=\"array\">" +
  30. " <subscription_add_on>" +
  31. " <add_on_code>ipaddresses</add_on_code>" +
  32. " <quantity>10</quantity>" +
  33. " <unit_amount_in_cents>150</unit_amount_in_cents>" +
  34. " </subscription_add_on>" +
  35. " </subscription_add_ons>" +
  36. " <a name=\"cancel\" href=\"https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/cancel\" method=\"put\"/>" +
  37. " <a name=\"terminate\" href=\"https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/terminate\" method=\"put\"/>" +
  38. " <a name=\"postpone\" href=\"https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/postpone\" method=\"put\"/>" +
  39. "</subscription>"
  40. "recurly_js/result/70db44b10f5f4b238669480c9903f6f5":
  41. "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
  42. "<subscription href=\"https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96\">" +
  43. " <account href=\"https://api.recurly.com/v2/accounts/104\"/>" +
  44. " <plan href=\"https://api.recurly.com/v2/plans/gold\">" +
  45. " <plan_code>gold</plan_code>" +
  46. " <name>Gold plan</name>" +
  47. " </plan>" +
  48. " <uuid>44f83d7cba354d5b84812419f923ea96</uuid>" +
  49. " <state>active</state>" +
  50. " <unit_amount_in_cents type=\"integer\">800</unit_amount_in_cents>" +
  51. " <currency>EUR</currency>" +
  52. " <quantity type=\"integer\">1</quantity>" +
  53. " <activated_at type=\"datetime\">2011-05-27T07:00:00Z</activated_at>" +
  54. " <canceled_at nil=\"nil\"></canceled_at>" +
  55. " <expires_at nil=\"nil\"></expires_at>" +
  56. " <current_period_started_at type=\"datetime\">2011-06-27T07:00:00Z</current_period_started_at>" +
  57. " <current_period_ends_at type=\"datetime\">2011-07-27T07:00:00Z</current_period_ends_at>" +
  58. " <trial_started_at nil=\"nil\"></trial_started_at>" +
  59. " <trial_ends_at nil=\"nil\"></trial_ends_at>" +
  60. " <subscription_add_ons type=\"array\">" +
  61. " <subscription_add_on>" +
  62. " <add_on_code>ipaddresses</add_on_code>" +
  63. " <quantity>10</quantity>" +
  64. " <unit_amount_in_cents>150</unit_amount_in_cents>" +
  65. " </subscription_add_on>" +
  66. " </subscription_add_ons>" +
  67. " <a name=\"cancel\" href=\"https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/cancel\" method=\"put\"/>" +
  68. " <a name=\"terminate\" href=\"https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/terminate\" method=\"put\"/>" +
  69. " <a name=\"postpone\" href=\"https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/postpone\" method=\"put\"/>" +
  70. "</subscription>"
  71. "accounts/104":
  72. "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
  73. "<account href=\"https://api.recurly.com/v2/accounts/104\">" +
  74. " <adjustments href=\"https://api.recurly.com/v2/accounts/1/adjustments\"/>" +
  75. " <billing_info href=\"https://api.recurly.com/v2/accounts/1/billing_info\"/>" +
  76. " <invoices href=\"https://api.recurly.com/v2/accounts/1/invoices\"/>" +
  77. " <redemption href=\"https://api.recurly.com/v2/accounts/1/redemption\"/>" +
  78. " <subscriptions href=\"https://api.recurly.com/v2/accounts/1/subscriptions\"/>" +
  79. " <transactions href=\"https://api.recurly.com/v2/accounts/1/transactions\"/>" +
  80. " <account_code>104</account_code>" +
  81. " <state>active</state>" +
  82. " <username nil=\"nil\"></username>" +
  83. " <email>verena@example.com</email>" +
  84. " <first_name>Verena</first_name>" +
  85. " <last_name>Example</last_name>" +
  86. " <accept_language nil=\"nil\"></accept_language>" +
  87. " <hosted_login_token>a92468579e9c4231a6c0031c4716c01d</hosted_login_token>" +
  88. " <created_at type=\"datetime\">2011-10-25T12:00:00</created_at>" +
  89. "</account>"
  90. mockApiRequest = (options, callback) ->
  91. if fixtures[options.url]
  92. callback(null, {statusCode : 200}, fixtures[options.url])
  93. else
  94. callback("Not found", {statusCode : 404})
  95. describe "RecurlyWrapper", ->
  96. beforeEach ->
  97. Settings.plans = [{
  98. planCode: "collaborator"
  99. name: "Collaborator"
  100. features:
  101. collaborators: -1
  102. versioning: true
  103. }]
  104. Settings.defaultPlanCode =
  105. collaborators: 0
  106. versioning: false
  107. tk.freeze(Date.now())
  108. afterEach ->
  109. tk.reset()
  110. describe "sign", ->
  111. before (done) ->
  112. RecurlyWrapper.sign({
  113. subscription :
  114. plan_code : "gold"
  115. name : "$$$"
  116. }, (error, signature) =>
  117. @signature = signature
  118. done()
  119. )
  120. it "should be signed correctly", ->
  121. signed = @signature.split("|")[0]
  122. query = @signature.split("|")[1]
  123. crypto.createHmac("sha1", Settings.apis.recurly.privateKey).update(query).digest("hex").should.equal signed
  124. it "should be url escaped", ->
  125. query = @signature.split("|")[1]
  126. should.equal query.match(/\[/), null
  127. query.match(/\%5B/).should.not.equal null
  128. it "should contain the passed data", ->
  129. query = querystring.parse @signature.split("|")[1]
  130. query["subscription[plan_code]"].should.equal "gold"
  131. query["subscription[name]"].should.equal "$$$"
  132. it "should contain a nonce", ->
  133. query = querystring.parse @signature.split("|")[1]
  134. should.exist query["nonce"]
  135. it "should contain a timestamp", ->
  136. query = querystring.parse @signature.split("|")[1]
  137. query["timestamp"].should.equal Math.round(Date.now() / 1000) + ""
  138. describe "_parseXml", ->
  139. it "should convert different data types into correct representations", (done) ->
  140. xml =
  141. "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
  142. "<subscription href=\"https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96\">" +
  143. " <account href=\"https://api.recurly.com/v2/accounts/1\"/>" +
  144. " <plan href=\"https://api.recurly.com/v2/plans/gold\">" +
  145. " <plan_code>gold</plan_code>" +
  146. " <name>Gold plan</name>" +
  147. " </plan>" +
  148. " <uuid>44f83d7cba354d5b84812419f923ea96</uuid>" +
  149. " <state>active</state>" +
  150. " <unit_amount_in_cents type=\"integer\">800</unit_amount_in_cents>" +
  151. " <currency>EUR</currency>" +
  152. " <quantity type=\"integer\">1</quantity>" +
  153. " <activated_at type=\"datetime\">2011-05-27T07:00:00Z</activated_at>" +
  154. " <canceled_at nil=\"nil\"></canceled_at>" +
  155. " <expires_at nil=\"nil\"></expires_at>" +
  156. " <current_period_started_at type=\"datetime\">2011-06-27T07:00:00Z</current_period_started_at>" +
  157. " <current_period_ends_at type=\"datetime\">2011-07-27T07:00:00Z</current_period_ends_at>" +
  158. " <trial_started_at nil=\"nil\"></trial_started_at>" +
  159. " <trial_ends_at nil=\"nil\"></trial_ends_at>" +
  160. " <subscription_add_ons type=\"array\">" +
  161. " <subscription_add_on>" +
  162. " <add_on_code>ipaddresses</add_on_code>" +
  163. " <quantity>10</quantity>" +
  164. " <unit_amount_in_cents>150</unit_amount_in_cents>" +
  165. " </subscription_add_on>" +
  166. " </subscription_add_ons>" +
  167. " <a name=\"cancel\" href=\"https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/cancel\" method=\"put\"/>" +
  168. " <a name=\"terminate\" href=\"https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/terminate\" method=\"put\"/>" +
  169. " <a name=\"postpone\" href=\"https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96/postpone\" method=\"put\"/>" +
  170. "</subscription>"
  171. RecurlyWrapper._parseXml xml, (error, data) ->
  172. data.subscription.plan.plan_code.should.equal "gold"
  173. data.subscription.plan.name.should.equal "Gold plan"
  174. data.subscription.uuid.should.equal "44f83d7cba354d5b84812419f923ea96"
  175. data.subscription.state.should.equal "active"
  176. data.subscription.unit_amount_in_cents.should.equal 800
  177. data.subscription.currency.should.equal "EUR"
  178. data.subscription.quantity.should.equal 1
  179. data.subscription.activated_at.should.deep.equal new Date("2011-05-27T07:00:00Z")
  180. should.equal data.subscription.canceled_at, null
  181. should.equal data.subscription.expires_at, null
  182. data.subscription.current_period_started_at.should.deep.equal new Date("2011-06-27T07:00:00Z")
  183. data.subscription.current_period_ends_at.should.deep.equal new Date("2011-07-27T07:00:00Z")
  184. should.equal data.subscription.trial_started_at, null
  185. should.equal data.subscription.trial_ends_at, null
  186. data.subscription.subscription_add_ons.should.deep.equal [{
  187. add_on_code: "ipaddresses"
  188. quantity: "10"
  189. unit_amount_in_cents: "150"
  190. }]
  191. data.subscription.account.url.should.equal "https://api.recurly.com/v2/accounts/1"
  192. data.subscription.url.should.equal "https://api.recurly.com/v2/subscriptions/44f83d7cba354d5b84812419f923ea96"
  193. data.subscription.plan.url.should.equal "https://api.recurly.com/v2/plans/gold"
  194. done()
  195. describe "getSubscription", ->
  196. describe "with proper subscription id", ->
  197. before ->
  198. @apiRequest = sinon.stub(RecurlyWrapper, "apiRequest", mockApiRequest)
  199. RecurlyWrapper.getSubscription "44f83d7cba354d5b84812419f923ea96", (error, recurlySubscription) =>
  200. @recurlySubscription = recurlySubscription
  201. after ->
  202. RecurlyWrapper.apiRequest.restore()
  203. it "should look up the subscription at the normal API end point", ->
  204. @apiRequest.args[0][0].url.should.equal "subscriptions/44f83d7cba354d5b84812419f923ea96"
  205. it "should return the subscription", ->
  206. @recurlySubscription.uuid.should.equal "44f83d7cba354d5b84812419f923ea96"
  207. describe "with ReculyJS token", ->
  208. before ->
  209. @apiRequest = sinon.stub(RecurlyWrapper, "apiRequest", mockApiRequest)
  210. RecurlyWrapper.getSubscription "70db44b10f5f4b238669480c9903f6f5", {recurlyJsResult: true}, (error, recurlySubscription) =>
  211. @recurlySubscription = recurlySubscription
  212. after ->
  213. RecurlyWrapper.apiRequest.restore()
  214. it "should return the subscription", ->
  215. @recurlySubscription.uuid.should.equal "44f83d7cba354d5b84812419f923ea96"
  216. it "should look up the subscription at the RecurlyJS API end point", ->
  217. @apiRequest.args[0][0].url.should.equal "recurly_js/result/70db44b10f5f4b238669480c9903f6f5"
  218. describe "with includeAccount", ->
  219. beforeEach ->
  220. @apiRequest = sinon.stub(RecurlyWrapper, "apiRequest", mockApiRequest)
  221. RecurlyWrapper.getSubscription "44f83d7cba354d5b84812419f923ea96", {includeAccount: true}, (error, recurlySubscription) =>
  222. @recurlySubscription = recurlySubscription
  223. afterEach ->
  224. RecurlyWrapper.apiRequest.restore()
  225. it "should request the account from the API", ->
  226. @apiRequest.args[1][0].url.should.equal "accounts/104"
  227. it "should populate the account attribute", ->
  228. @recurlySubscription.account.account_code.should.equal "104"
  229. describe "updateSubscription", ->
  230. beforeEach (done) ->
  231. @recurlySubscriptionId = "subscription-id-123"
  232. @apiRequest = sinon.stub RecurlyWrapper, "apiRequest", (options, callback) =>
  233. @requestOptions = options
  234. callback null, {}, fixtures["subscriptions/44f83d7cba354d5b84812419f923ea96"]
  235. RecurlyWrapper.updateSubscription @recurlySubscriptionId, { plan_code : "silver", timeframe: "now" }, (error, recurlySubscription) =>
  236. @recurlySubscription = recurlySubscription
  237. done()
  238. afterEach ->
  239. RecurlyWrapper.apiRequest.restore()
  240. it "should send an update request to the API", ->
  241. @apiRequest.called.should.equal true
  242. @requestOptions.body.should.equal """
  243. <subscription>
  244. <plan_code>silver</plan_code>
  245. <timeframe>now</timeframe>
  246. </subscription>
  247. """
  248. @requestOptions.url.should.equal "subscriptions/#{@recurlySubscriptionId}"
  249. @requestOptions.method.should.equal "put"
  250. it "should return the updated subscription", ->
  251. should.exist @recurlySubscription
  252. @recurlySubscription.plan.plan_code.should.equal "gold"
  253. describe "cancelSubscription", ->
  254. beforeEach (done) ->
  255. @recurlySubscriptionId = "subscription-id-123"
  256. @apiRequest = sinon.stub RecurlyWrapper, "apiRequest", (options, callback) =>
  257. options.url.should.equal "subscriptions/#{@recurlySubscriptionId}/cancel"
  258. options.method.should.equal "put"
  259. callback()
  260. RecurlyWrapper.cancelSubscription(@recurlySubscriptionId, done)
  261. afterEach ->
  262. RecurlyWrapper.apiRequest.restore()
  263. it "should send a cancel request to the API", ->
  264. @apiRequest.called.should.equal true
  265. describe "reactivateSubscription", ->
  266. beforeEach (done) ->
  267. @recurlySubscriptionId = "subscription-id-123"
  268. @apiRequest = sinon.stub RecurlyWrapper, "apiRequest", (options, callback) =>
  269. options.url.should.equal "subscriptions/#{@recurlySubscriptionId}/reactivate"
  270. options.method.should.equal "put"
  271. callback()
  272. RecurlyWrapper.reactivateSubscription(@recurlySubscriptionId, done)
  273. afterEach ->
  274. RecurlyWrapper.apiRequest.restore()
  275. it "should send a cancel request to the API", ->
  276. @apiRequest.called.should.equal true
  277. describe "redeemCoupon", ->
  278. beforeEach (done) ->
  279. @recurlyAccountId = "account-id-123"
  280. @coupon_code = "312321312"
  281. @apiRequest = sinon.stub RecurlyWrapper, "apiRequest", (options, callback) =>
  282. options.url.should.equal "coupons/#{@coupon_code}/redeem"
  283. options.body.indexOf("<account_code>#{@recurlyAccountId}</account_code>").should.not.equal -1
  284. options.body.indexOf("<currency>USD</currency>").should.not.equal -1
  285. options.method.should.equal "post"
  286. callback()
  287. RecurlyWrapper.redeemCoupon(@recurlyAccountId, @coupon_code, done)
  288. afterEach ->
  289. RecurlyWrapper.apiRequest.restore()
  290. it "should send the request to redem the coupon", ->
  291. @apiRequest.called.should.equal true