SubscriptionControllerTests.coffee 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  1. SandboxedModule = require('sandboxed-module')
  2. sinon = require 'sinon'
  3. should = require("chai").should()
  4. MockRequest = require "../helpers/MockRequest"
  5. MockResponse = require "../helpers/MockResponse"
  6. modulePath = '../../../../app/js/Features/Subscription/SubscriptionController'
  7. mockSubscriptions =
  8. "subscription-123-active":
  9. uuid: "subscription-123-active"
  10. plan:
  11. name: "Gold"
  12. plan_code: "gold"
  13. current_period_ends_at: new Date()
  14. state: "active"
  15. unit_amount_in_cents: 999
  16. account:
  17. account_code: "user-123"
  18. describe "SubscriptionController", ->
  19. beforeEach ->
  20. @user = {email:"tom@yahoo.com", _id: 'one', signUpDate: new Date('2000-10-01')}
  21. @activeRecurlySubscription = mockSubscriptions["subscription-123-active"]
  22. @AuthenticationController =
  23. getLoggedInUser: sinon.stub().callsArgWith(1, null, @user)
  24. getLoggedInUserId: sinon.stub().returns(@user._id)
  25. getSessionUser: sinon.stub().returns(@user)
  26. isUserLoggedIn: sinon.stub().returns(true)
  27. @SubscriptionHandler =
  28. createSubscription: sinon.stub().callsArgWith(3)
  29. updateSubscription: sinon.stub().callsArgWith(3)
  30. reactivateSubscription: sinon.stub().callsArgWith(1)
  31. cancelSubscription: sinon.stub().callsArgWith(1)
  32. recurlyCallback: sinon.stub().callsArgWith(1)
  33. startFreeTrial: sinon.stub()
  34. @PlansLocator =
  35. findLocalPlanInSettings: sinon.stub()
  36. @LimitationsManager =
  37. hasPaidSubscription: sinon.stub()
  38. userHasV1OrV2Subscription : sinon.stub()
  39. userHasV2Subscription: sinon.stub()
  40. @SubscriptionViewModelBuilder =
  41. buildUsersSubscriptionViewModel:sinon.stub().callsArgWith(1, null, @activeRecurlySubscription)
  42. buildViewModel: sinon.stub()
  43. @settings =
  44. coupon_codes:
  45. upgradeToAnnualPromo:
  46. student:"STUDENTCODEHERE"
  47. collaborator:"COLLABORATORCODEHERE"
  48. apis:
  49. recurly:
  50. subdomain:"sl"
  51. siteUrl: "http://de.sharelatex.dev:3000"
  52. gaExperiments:{}
  53. @GeoIpLookup =
  54. getCurrencyCode:sinon.stub()
  55. @SubscriptionDomainHandler =
  56. getDomainLicencePage:sinon.stub()
  57. @UserGetter =
  58. getUser: sinon.stub().callsArgWith(2, null, @user)
  59. @SubscriptionController = SandboxedModule.require modulePath, requires:
  60. '../Authentication/AuthenticationController': @AuthenticationController
  61. './SubscriptionHandler': @SubscriptionHandler
  62. "./PlansLocator": @PlansLocator
  63. './SubscriptionViewModelBuilder': @SubscriptionViewModelBuilder
  64. "./LimitationsManager": @LimitationsManager
  65. "../../infrastructure/GeoIpLookup":@GeoIpLookup
  66. "logger-sharelatex":
  67. log:->
  68. warn:->
  69. "settings-sharelatex": @settings
  70. "./SubscriptionDomainHandler":@SubscriptionDomainHandler
  71. "../User/UserGetter": @UserGetter
  72. "./RecurlyWrapper": @RecurlyWrapper = {}
  73. "./FeaturesUpdater": @FeaturesUpdater = {}
  74. "./GroupPlansData": @GroupPlansData = {}
  75. @res = new MockResponse()
  76. @req = new MockRequest()
  77. @req.body = {}
  78. @req.query =
  79. planCode:"123123"
  80. @stubbedCurrencyCode = "GBP"
  81. describe "plansPage", ->
  82. beforeEach ->
  83. @req.ip = "1234.3123.3131.333 313.133.445.666 653.5345.5345.534"
  84. @GeoIpLookup.getCurrencyCode.callsArgWith(1, null, @stubbedCurrencyCode)
  85. describe 'when user is logged in', (done) ->
  86. beforeEach (done) ->
  87. @res.callback = done
  88. @SubscriptionController.plansPage(@req, @res)
  89. it 'should fetch the current user', (done) ->
  90. @UserGetter.getUser.callCount.should.equal 1
  91. done()
  92. describe 'not dependant on logged in state', (done) ->
  93. # these could have been put in 'when user is not logged in' too
  94. it "should set the recommended currency from the geoiplookup", (done)->
  95. @res.renderedVariables.recomendedCurrency.should.equal(@stubbedCurrencyCode)
  96. @GeoIpLookup.getCurrencyCode.calledWith(@req.ip).should.equal true
  97. done()
  98. it 'should include data for features table', (done) ->
  99. @res.renderedVariables.planFeatures.length.should.not.equal 0
  100. done()
  101. describe 'when user is not logged in', (done) ->
  102. beforeEach (done) ->
  103. @UserGetter =
  104. getUser: sinon.stub().callsArgWith(2, null, null)
  105. @res.callback = done
  106. @AuthenticationController =
  107. getLoggedInUser: sinon.stub().callsArgWith(1, null, null)
  108. getLoggedInUserId: sinon.stub().returns(null)
  109. getSessionUser: sinon.stub().returns(null)
  110. isUserLoggedIn: sinon.stub().returns(false)
  111. @SubscriptionController = SandboxedModule.require modulePath, requires:
  112. '../Authentication/AuthenticationController': @AuthenticationController
  113. './SubscriptionHandler': @SubscriptionHandler
  114. "./PlansLocator": @PlansLocator
  115. './SubscriptionViewModelBuilder': @SubscriptionViewModelBuilder
  116. "./LimitationsManager": @LimitationsManager
  117. "../../infrastructure/GeoIpLookup":@GeoIpLookup
  118. "logger-sharelatex":
  119. log:->
  120. warn:->
  121. "settings-sharelatex": @settings
  122. "./SubscriptionDomainHandler":@SubscriptionDomainHandler
  123. "../User/UserGetter": @UserGetter
  124. "./RecurlyWrapper": @RecurlyWrapper = {}
  125. "./FeaturesUpdater": @FeaturesUpdater = {}
  126. "./GroupPlansData": @GroupPlansData
  127. @SubscriptionController.plansPage(@req, @res)
  128. it 'should not fetch the current user', (done) ->
  129. @UserGetter.getUser.callCount.should.equal 0
  130. done()
  131. describe "paymentPage", ->
  132. beforeEach ->
  133. @req.headers = {}
  134. @RecurlyWrapper.sign = sinon.stub().yields(null, @signature = "signature")
  135. @SubscriptionHandler.validateNoSubscriptionInRecurly = sinon.stub().yields(null, true)
  136. @GeoIpLookup.getCurrencyCode.callsArgWith(1, null, @stubbedCurrencyCode)
  137. describe "with a user without a subscription", ->
  138. beforeEach ->
  139. @LimitationsManager.userHasV1OrV2Subscription.callsArgWith(1, null, false)
  140. @PlansLocator.findLocalPlanInSettings.returns({})
  141. describe "with a valid plan code", ->
  142. it "should render the new subscription page", (done)->
  143. @res.render = (page, opts)=>
  144. page.should.equal "subscriptions/new"
  145. done()
  146. @SubscriptionController.paymentPage @req, @res
  147. describe "with a user with subscription", ->
  148. it "should redirect to the subscription dashboard", (done)->
  149. @LimitationsManager.userHasV1OrV2Subscription.callsArgWith(1, null, true)
  150. @res.redirect = (url)=>
  151. url.should.equal "/user/subscription"
  152. done()
  153. @SubscriptionController.paymentPage(@req, @res)
  154. describe "with an invalid plan code", ->
  155. it "should redirect to the subscription dashboard", (done)->
  156. @LimitationsManager.userHasV1OrV2Subscription.callsArgWith(1, null, false)
  157. @PlansLocator.findLocalPlanInSettings.returns(null)
  158. @res.redirect = (url)=>
  159. url.should.equal "/user/subscription"
  160. done()
  161. @SubscriptionController.paymentPage(@req, @res)
  162. describe "which currency to use", ->
  163. beforeEach ->
  164. @LimitationsManager.userHasV1OrV2Subscription.callsArgWith(1, null, false)
  165. @PlansLocator.findLocalPlanInSettings.returns({})
  166. it "should use the set currency from the query string", (done)->
  167. @req.query.currency = "EUR"
  168. @res.render = (page, opts)=>
  169. opts.currency.should.equal "EUR"
  170. opts.currency.should.not.equal @stubbedCurrencyCode
  171. done()
  172. @SubscriptionController.paymentPage @req, @res
  173. it "should upercase the currency code", (done)->
  174. @req.query.currency = "eur"
  175. @res.render = (page, opts)=>
  176. opts.currency.should.equal "EUR"
  177. done()
  178. @SubscriptionController.paymentPage @req, @res
  179. it "should use the geo ip currency if non is provided", (done)->
  180. @req.query.currency = null
  181. @res.render = (page, opts)=>
  182. opts.currency.should.equal @stubbedCurrencyCode
  183. done()
  184. @SubscriptionController.paymentPage @req, @res
  185. describe "with a recurly subscription already", ->
  186. it "should redirect to the subscription dashboard", (done)->
  187. @LimitationsManager.userHasV1OrV2Subscription.callsArgWith(1, null, false)
  188. @SubscriptionHandler.validateNoSubscriptionInRecurly = sinon.stub().yields(null, false)
  189. @res.redirect = (url)=>
  190. url.should.equal "/user/subscription"
  191. done()
  192. @SubscriptionController.paymentPage(@req, @res)
  193. describe "successful_subscription", ->
  194. beforeEach (done) ->
  195. @SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel.callsArgWith(1, null, {})
  196. @res.callback = done
  197. @SubscriptionController.successful_subscription @req, @res
  198. describe "userSubscriptionPage", ->
  199. describe "with a user without a subscription", ->
  200. beforeEach (done) ->
  201. @res.callback = done
  202. @LimitationsManager.hasPaidSubscription.callsArgWith(1, null, false)
  203. @SubscriptionController.userSubscriptionPage @req, @res
  204. it "should redirect to the plans page", ->
  205. @res.redirected.should.equal true
  206. @res.redirectedTo.should.equal "/user/subscription/plans"
  207. describe "with a potential domain licence", ->
  208. beforeEach () ->
  209. @groupUrl = "/go/over-here"
  210. @SubscriptionDomainHandler.getDomainLicencePage.returns(@groupUrl)
  211. describe "without an existing subscription", ->
  212. beforeEach (done)->
  213. @res.callback = done
  214. @LimitationsManager.hasPaidSubscription.callsArgWith(1, null, false)
  215. @SubscriptionController.userSubscriptionPage @req, @res
  216. it "should redirect to the group invite url", ->
  217. @res.redirected.should.equal true
  218. @res.redirectedTo.should.equal @groupUrl
  219. describe "with an existing subscription", ->
  220. beforeEach (done)->
  221. @res.callback = done
  222. @settings.apis.recurly.subdomain = 'test'
  223. @userSub = {account: {hosted_login_token: 'abcd'}}
  224. @LimitationsManager.hasPaidSubscription
  225. .callsArgWith(1, null, true, {})
  226. @SubscriptionController.userSubscriptionPage @req, @res
  227. it "should render the dashboard", ->
  228. @res.renderedTemplate.should.equal "subscriptions/dashboard"
  229. describe "with a user with a paid subscription", ->
  230. beforeEach (done) ->
  231. @res.callback = done
  232. @SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel.callsArgWith(1, null, @activeRecurlySubscription)
  233. @LimitationsManager.hasPaidSubscription.callsArgWith(1, null, true, {})
  234. @SubscriptionController.userSubscriptionPage @req, @res
  235. it "should render the dashboard", (done)->
  236. @res.rendered.should.equal true
  237. @res.renderedTemplate.should.equal "subscriptions/dashboard"
  238. done()
  239. it "should set the correct subscription details", ->
  240. @res.renderedVariables.subscription.should.deep.equal @activeRecurlySubscription
  241. describe "with a user with a free trial", ->
  242. beforeEach (done) ->
  243. @res.callback = done
  244. @SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel.callsArgWith(1, null, @activeRecurlySubscription)
  245. @LimitationsManager.hasPaidSubscription.callsArgWith(1, null, true, {})
  246. @SubscriptionController.userSubscriptionPage @req, @res
  247. it "should render the dashboard", ->
  248. @res.renderedTemplate.should.equal "subscriptions/dashboard"
  249. it "should set the correct subscription details", ->
  250. @res.renderedVariables.subscription.should.deep.equal @activeRecurlySubscription
  251. describe "when its a custom subscription which is non recurly", ->
  252. beforeEach ()->
  253. @LimitationsManager.hasPaidSubscription.callsArgWith(1, null, true, {customAccount:true})
  254. @SubscriptionController.userSubscriptionPage @req, @res
  255. it "should redirect to /user/subscription/custom_account", ->
  256. @res.redirectedTo.should.equal("/user/subscription/custom_account")
  257. describe "userCustomSubscriptionPage", ->
  258. beforeEach (done) ->
  259. @res.callback = done
  260. @LimitationsManager.hasPaidSubscription.callsArgWith(1, null, true, {})
  261. @SubscriptionController.userCustomSubscriptionPage @req, @res
  262. it "should render the page", (done)->
  263. @res.rendered.should.equal true
  264. @res.renderedTemplate.should.equal "subscriptions/custom_account"
  265. done()
  266. describe "createSubscription", ->
  267. beforeEach (done)->
  268. @res =
  269. sendStatus:->
  270. done()
  271. sinon.spy @res, "sendStatus"
  272. @subscriptionDetails =
  273. card:"1234"
  274. cvv:"123"
  275. @req.body.recurly_token_id = "1234"
  276. @req.body.subscriptionDetails = @subscriptionDetails
  277. @LimitationsManager.userHasV1OrV2Subscription.yields(null, false)
  278. @SubscriptionController.createSubscription @req, @res
  279. it "should send the user and subscriptionId to the handler", (done)->
  280. @SubscriptionHandler.createSubscription.calledWith(@user, @subscriptionDetails, @req.body.recurly_token_id).should.equal true
  281. done()
  282. it "should redurect to the subscription page", (done)->
  283. @res.sendStatus.calledWith(201).should.equal true
  284. done()
  285. describe "updateSubscription via post", ->
  286. beforeEach (done)->
  287. @res =
  288. redirect:->
  289. done()
  290. sinon.spy @res, "redirect"
  291. @plan_code = "1234"
  292. @req.body.plan_code = @plan_code
  293. @SubscriptionController.updateSubscription @req, @res
  294. it "should send the user and subscriptionId to the handler", (done)->
  295. @SubscriptionHandler.updateSubscription.calledWith(@user, @plan_code).should.equal true
  296. done()
  297. it "should redurect to the subscription page", (done)->
  298. @res.redirect.calledWith("/user/subscription").should.equal true
  299. done()
  300. describe "reactivateSubscription", ->
  301. beforeEach (done)->
  302. @res =
  303. redirect:->
  304. done()
  305. sinon.spy @res, "redirect"
  306. @SubscriptionController.reactivateSubscription @req, @res
  307. it "should tell the handler to reactivate this user", (done)->
  308. @SubscriptionHandler.reactivateSubscription.calledWith(@user).should.equal true
  309. done()
  310. it "should redurect to the subscription page", (done)->
  311. @res.redirect.calledWith("/user/subscription").should.equal true
  312. done()
  313. describe "cancelSubscription", ->
  314. beforeEach (done)->
  315. @res =
  316. redirect:->
  317. done()
  318. sinon.spy @res, "redirect"
  319. @SubscriptionController.cancelSubscription @req, @res
  320. it "should tell the handler to cancel this user", (done)->
  321. @SubscriptionHandler.cancelSubscription.calledWith(@user).should.equal true
  322. done()
  323. it "should redurect to the subscription page", (done)->
  324. @res.redirect.calledWith("/user/subscription").should.equal true
  325. done()
  326. describe "recurly callback", ->
  327. describe "with a actionable request", ->
  328. beforeEach (done)->
  329. @req =
  330. body:
  331. expired_subscription_notification:
  332. subscription:
  333. uuid: @activeRecurlySubscription.uuid
  334. @res = sendStatus:->
  335. done()
  336. sinon.spy @res, "sendStatus"
  337. @SubscriptionController.recurlyCallback @req, @res
  338. it "should tell the SubscriptionHandler to process the recurly callback", (done)->
  339. @SubscriptionHandler.recurlyCallback.called.should.equal true
  340. done()
  341. it "should send a 200", (done)->
  342. @res.sendStatus.calledWith(200)
  343. done()
  344. describe "with a non-actionable request", ->
  345. beforeEach (done) ->
  346. @user.id = @activeRecurlySubscription.account.account_code
  347. @req =
  348. body:
  349. new_subscription_notification:
  350. subscription:
  351. uuid: @activeRecurlySubscription.uuid
  352. @res = sendStatus:->
  353. done()
  354. sinon.spy @res, "sendStatus"
  355. @SubscriptionController.recurlyCallback @req, @res
  356. it "should not call the subscriptionshandler", ->
  357. @SubscriptionHandler.recurlyCallback.called.should.equal false
  358. it "should respond with a 200 status", ->
  359. @res.sendStatus.calledWith(200)
  360. describe "renderUpgradeToAnnualPlanPage", ->
  361. it "should redirect to the plans page if the user does not have a subscription", (done)->
  362. @LimitationsManager.userHasV2Subscription.callsArgWith(1, null, false)
  363. @res.redirect = (url)->
  364. url.should.equal "/user/subscription/plans"
  365. done()
  366. @SubscriptionController.renderUpgradeToAnnualPlanPage @req, @res
  367. it "should pass the plan code to the view - student", (done)->
  368. @LimitationsManager.userHasV2Subscription.callsArgWith(1, null, true, {planCode:"Student free trial 14 days"})
  369. @res.render = (view, opts)->
  370. view.should.equal "subscriptions/upgradeToAnnual"
  371. opts.planName.should.equal "student"
  372. done()
  373. @SubscriptionController.renderUpgradeToAnnualPlanPage @req, @res
  374. it "should pass the plan code to the view - collaborator", (done)->
  375. @LimitationsManager.userHasV2Subscription.callsArgWith(1, null, true, {planCode:"free trial for Collaborator free trial 14 days"})
  376. @res.render = (view, opts)->
  377. opts.planName.should.equal "collaborator"
  378. done()
  379. @SubscriptionController.renderUpgradeToAnnualPlanPage @req, @res
  380. it "should pass annual as the plan name if the user is already on an annual plan", (done)->
  381. @LimitationsManager.userHasV2Subscription.callsArgWith(1, null, true, {planCode:"student annual with free trial"})
  382. @res.render = (view, opts)->
  383. opts.planName.should.equal "annual"
  384. done()
  385. @SubscriptionController.renderUpgradeToAnnualPlanPage @req, @res
  386. describe "processUpgradeToAnnualPlan", ->
  387. beforeEach ->
  388. it "should tell the subscription handler to update the subscription with the annual plan and apply a coupon code", (done)->
  389. @req.body =
  390. planName:"student"
  391. @res.sendStatus = ()=>
  392. @SubscriptionHandler.updateSubscription.calledWith(@user, "student-annual", "STUDENTCODEHERE").should.equal true
  393. done()
  394. @SubscriptionController.processUpgradeToAnnualPlan @req, @res
  395. it "should get the collaborator coupon code", (done)->
  396. @req.body =
  397. planName:"collaborator"
  398. @res.sendStatus = (url)=>
  399. @SubscriptionHandler.updateSubscription.calledWith(@user, "collaborator-annual", "COLLABORATORCODEHERE").should.equal true
  400. done()
  401. @SubscriptionController.processUpgradeToAnnualPlan @req, @res