SubscriptionHandlerTests.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  1. /* eslint-disable
  2. max-len,
  3. no-return-assign,
  4. no-unused-vars,
  5. */
  6. // TODO: This file was created by bulk-decaffeinate.
  7. // Fix any style issues and re-enable lint.
  8. /*
  9. * decaffeinate suggestions:
  10. * DS102: Remove unnecessary code created because of implicit returns
  11. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  12. */
  13. const SandboxedModule = require('sandboxed-module')
  14. const should = require('chai').should()
  15. const sinon = require('sinon')
  16. const querystring = require('querystring')
  17. const modulePath =
  18. '../../../../app/src/Features/Subscription/SubscriptionHandler'
  19. const mockRecurlySubscriptions = {
  20. 'subscription-123-active': {
  21. uuid: 'subscription-123-active',
  22. plan: {
  23. name: 'Gold',
  24. plan_code: 'gold'
  25. },
  26. current_period_ends_at: new Date(),
  27. state: 'active',
  28. unit_amount_in_cents: 999,
  29. account: {
  30. account_code: 'user-123'
  31. }
  32. }
  33. }
  34. describe('SubscriptionHandler', function() {
  35. beforeEach(function() {
  36. this.Settings = {
  37. plans: [
  38. {
  39. planCode: 'collaborator',
  40. name: 'Collaborator',
  41. features: {
  42. collaborators: -1,
  43. versioning: true
  44. }
  45. }
  46. ],
  47. defaultPlanCode: {
  48. collaborators: 0,
  49. versioning: false
  50. }
  51. }
  52. this.activeRecurlySubscription =
  53. mockRecurlySubscriptions['subscription-123-active']
  54. this.User = {}
  55. this.user = { _id: (this.user_id = 'user_id_here_') }
  56. this.subscription = {
  57. recurlySubscription_id: this.activeRecurlySubscription.uuid
  58. }
  59. this.RecurlyWrapper = {
  60. getSubscription: sinon
  61. .stub()
  62. .callsArgWith(2, null, this.activeRecurlySubscription),
  63. updateSubscription: sinon
  64. .stub()
  65. .callsArgWith(2, null, this.activeRecurlySubscription),
  66. cancelSubscription: sinon.stub().callsArgWith(1),
  67. reactivateSubscription: sinon.stub().callsArgWith(1),
  68. redeemCoupon: sinon.stub().callsArgWith(2),
  69. createSubscription: sinon
  70. .stub()
  71. .callsArgWith(3, null, this.activeRecurlySubscription)
  72. }
  73. this.DropboxHandler = { unlinkAccount: sinon.stub().callsArgWith(1) }
  74. this.SubscriptionUpdater = {
  75. syncSubscription: sinon.stub().yields(),
  76. startFreeTrial: sinon.stub().callsArgWith(1)
  77. }
  78. this.LimitationsManager = { userHasV2Subscription: sinon.stub() }
  79. this.EmailHandler = { sendEmail: sinon.stub() }
  80. this.AnalyticsManager = { recordEvent: sinon.stub() }
  81. this.SubscriptionHandler = SandboxedModule.require(modulePath, {
  82. globals: {
  83. console: console
  84. },
  85. requires: {
  86. './RecurlyWrapper': this.RecurlyWrapper,
  87. 'settings-sharelatex': this.Settings,
  88. '../../models/User': {
  89. User: this.User
  90. },
  91. './SubscriptionUpdater': this.SubscriptionUpdater,
  92. 'logger-sharelatex': { log() {} },
  93. './LimitationsManager': this.LimitationsManager,
  94. '../Email/EmailHandler': this.EmailHandler,
  95. '../Dropbox/DropboxHandler': this.DropboxHandler,
  96. '../../infrastructure/Events': (this.Events = { emit: sinon.stub() }),
  97. '../Analytics/AnalyticsManager': this.AnalyticsManager
  98. }
  99. })
  100. return (this.SubscriptionHandler.syncSubscriptionToUser = sinon
  101. .stub()
  102. .callsArgWith(2))
  103. })
  104. describe('createSubscription', function() {
  105. beforeEach(function() {
  106. this.callback = sinon.stub()
  107. this.subscriptionDetails = {
  108. cvv: '123',
  109. number: '12345'
  110. }
  111. this.recurlyTokenIds = { billing: '45555666' }
  112. return (this.SubscriptionHandler.validateNoSubscriptionInRecurly = sinon
  113. .stub()
  114. .yields(null, true))
  115. })
  116. describe('successfully', function() {
  117. beforeEach(function() {
  118. return this.SubscriptionHandler.createSubscription(
  119. this.user,
  120. this.subscriptionDetails,
  121. this.recurlyTokenIds,
  122. this.callback
  123. )
  124. })
  125. it('should create the subscription with the wrapper', function() {
  126. return this.RecurlyWrapper.createSubscription
  127. .calledWith(this.user, this.subscriptionDetails, this.recurlyTokenIds)
  128. .should.equal(true)
  129. })
  130. it('should sync the subscription to the user', function() {
  131. this.SubscriptionUpdater.syncSubscription.calledOnce.should.equal(true)
  132. this.SubscriptionUpdater.syncSubscription.args[0][0].should.deep.equal(
  133. this.activeRecurlySubscription
  134. )
  135. return this.SubscriptionUpdater.syncSubscription.args[0][1].should.deep.equal(
  136. this.user._id
  137. )
  138. })
  139. })
  140. describe('when there is already a subscription in Recurly', function() {
  141. beforeEach(function() {
  142. this.SubscriptionHandler.validateNoSubscriptionInRecurly = sinon
  143. .stub()
  144. .yields(null, false)
  145. return this.SubscriptionHandler.createSubscription(
  146. this.user,
  147. this.subscriptionDetails,
  148. this.recurlyTokenIds,
  149. this.callback
  150. )
  151. })
  152. it('should return an error', function() {
  153. return this.callback.calledWith(
  154. new Error('user already has subscription in recurly')
  155. )
  156. })
  157. })
  158. })
  159. describe('updateSubscription', function() {
  160. describe('with a user with a subscription', function() {
  161. describe('with a valid plan code', function() {
  162. beforeEach(function(done) {
  163. this.plan_code = 'collaborator'
  164. this.LimitationsManager.userHasV2Subscription.callsArgWith(
  165. 1,
  166. null,
  167. true,
  168. this.subscription
  169. )
  170. return this.SubscriptionHandler.updateSubscription(
  171. this.user,
  172. this.plan_code,
  173. null,
  174. done
  175. )
  176. })
  177. it('should update the subscription', function() {
  178. this.RecurlyWrapper.updateSubscription
  179. .calledWith(this.subscription.recurlySubscription_id)
  180. .should.equal(true)
  181. const updateOptions = this.RecurlyWrapper.updateSubscription
  182. .args[0][1]
  183. return updateOptions.plan_code.should.equal(this.plan_code)
  184. })
  185. it('should update immediately', function() {
  186. const updateOptions = this.RecurlyWrapper.updateSubscription
  187. .args[0][1]
  188. return updateOptions.timeframe.should.equal('now')
  189. })
  190. it('should sync the new subscription to the user', function() {
  191. this.SubscriptionUpdater.syncSubscription.calledOnce.should.equal(
  192. true
  193. )
  194. this.SubscriptionUpdater.syncSubscription.args[0][0].should.deep.equal(
  195. this.activeRecurlySubscription
  196. )
  197. return this.SubscriptionUpdater.syncSubscription.args[0][1].should.deep.equal(
  198. this.user._id
  199. )
  200. })
  201. })
  202. })
  203. describe('with a user without a subscription', function() {
  204. beforeEach(function(done) {
  205. this.LimitationsManager.userHasV2Subscription.callsArgWith(
  206. 1,
  207. null,
  208. false
  209. )
  210. return this.SubscriptionHandler.updateSubscription(
  211. this.user,
  212. this.plan_code,
  213. null,
  214. done
  215. )
  216. })
  217. it('should redirect to the subscription dashboard', function() {
  218. this.RecurlyWrapper.updateSubscription.called.should.equal(false)
  219. return this.SubscriptionHandler.syncSubscriptionToUser.called.should.equal(
  220. false
  221. )
  222. })
  223. })
  224. describe('with a coupon code', function() {
  225. beforeEach(function(done) {
  226. this.plan_code = 'collaborator'
  227. this.coupon_code = '1231312'
  228. this.LimitationsManager.userHasV2Subscription.callsArgWith(
  229. 1,
  230. null,
  231. true,
  232. this.subscription
  233. )
  234. return this.SubscriptionHandler.updateSubscription(
  235. this.user,
  236. this.plan_code,
  237. this.coupon_code,
  238. done
  239. )
  240. })
  241. it('should get the users account', function() {
  242. return this.RecurlyWrapper.getSubscription
  243. .calledWith(this.activeRecurlySubscription.uuid)
  244. .should.equal(true)
  245. })
  246. it('should redeme the coupon', function(done) {
  247. this.RecurlyWrapper.redeemCoupon
  248. .calledWith(
  249. this.activeRecurlySubscription.account.account_code,
  250. this.coupon_code
  251. )
  252. .should.equal(true)
  253. return done()
  254. })
  255. it('should update the subscription', function() {
  256. this.RecurlyWrapper.updateSubscription
  257. .calledWith(this.subscription.recurlySubscription_id)
  258. .should.equal(true)
  259. const updateOptions = this.RecurlyWrapper.updateSubscription.args[0][1]
  260. return updateOptions.plan_code.should.equal(this.plan_code)
  261. })
  262. })
  263. })
  264. describe('cancelSubscription', function() {
  265. describe('with a user without a subscription', function() {
  266. beforeEach(function(done) {
  267. this.LimitationsManager.userHasV2Subscription.callsArgWith(
  268. 1,
  269. null,
  270. false,
  271. this.subscription
  272. )
  273. return this.SubscriptionHandler.cancelSubscription(this.user, done)
  274. })
  275. it('should redirect to the subscription dashboard', function() {
  276. return this.RecurlyWrapper.cancelSubscription.called.should.equal(false)
  277. })
  278. })
  279. describe('with a user with a subscription', function() {
  280. beforeEach(function(done) {
  281. this.LimitationsManager.userHasV2Subscription.callsArgWith(
  282. 1,
  283. null,
  284. true,
  285. this.subscription
  286. )
  287. return this.SubscriptionHandler.cancelSubscription(this.user, done)
  288. })
  289. it('should cancel the subscription', function() {
  290. this.RecurlyWrapper.cancelSubscription.called.should.equal(true)
  291. return this.RecurlyWrapper.cancelSubscription
  292. .calledWith(this.subscription.recurlySubscription_id)
  293. .should.equal(true)
  294. })
  295. it('should trigger the cancel subscription event', function() {
  296. return this.Events.emit
  297. .calledWith('cancelSubscription', this.user._id)
  298. .should.equal(true)
  299. })
  300. })
  301. })
  302. describe('reactiveRecurlySubscription', function() {
  303. describe('with a user without a subscription', function() {
  304. beforeEach(function(done) {
  305. this.LimitationsManager.userHasV2Subscription.callsArgWith(
  306. 1,
  307. null,
  308. false,
  309. this.subscription
  310. )
  311. return this.SubscriptionHandler.reactivateSubscription(this.user, done)
  312. })
  313. it('should redirect to the subscription dashboard', function() {
  314. return this.RecurlyWrapper.reactivateSubscription.called.should.equal(
  315. false
  316. )
  317. })
  318. it('should not send a notification email', function() {
  319. return sinon.assert.notCalled(this.EmailHandler.sendEmail)
  320. })
  321. })
  322. describe('with a user with a subscription', function() {
  323. beforeEach(function(done) {
  324. this.LimitationsManager.userHasV2Subscription.callsArgWith(
  325. 1,
  326. null,
  327. true,
  328. this.subscription
  329. )
  330. return this.SubscriptionHandler.reactivateSubscription(this.user, done)
  331. })
  332. it('should reactivate the subscription', function() {
  333. this.RecurlyWrapper.reactivateSubscription.called.should.equal(true)
  334. return this.RecurlyWrapper.reactivateSubscription
  335. .calledWith(this.subscription.recurlySubscription_id)
  336. .should.equal(true)
  337. })
  338. it('should send a notification email', function() {
  339. return sinon.assert.calledWith(
  340. this.EmailHandler.sendEmail,
  341. 'reactivatedSubscription'
  342. )
  343. })
  344. })
  345. })
  346. describe('recurlyCallback', function() {
  347. describe('with an actionable request', function() {
  348. beforeEach(function(done) {
  349. this.user.id = this.activeRecurlySubscription.account.account_code
  350. this.User.findById = (userId, callback) => {
  351. userId.should.equal(this.user.id)
  352. return callback(null, this.user)
  353. }
  354. return this.SubscriptionHandler.recurlyCallback(
  355. this.activeRecurlySubscription,
  356. {},
  357. done
  358. )
  359. })
  360. it('should request the affected subscription from the API', function() {
  361. return this.RecurlyWrapper.getSubscription
  362. .calledWith(this.activeRecurlySubscription.uuid)
  363. .should.equal(true)
  364. })
  365. it('should request the account details of the subscription', function() {
  366. const options = this.RecurlyWrapper.getSubscription.args[0][1]
  367. return options.includeAccount.should.equal(true)
  368. })
  369. it('should sync the subscription to the user', function() {
  370. this.SubscriptionUpdater.syncSubscription.calledOnce.should.equal(true)
  371. this.SubscriptionUpdater.syncSubscription.args[0][0].should.deep.equal(
  372. this.activeRecurlySubscription
  373. )
  374. return this.SubscriptionUpdater.syncSubscription.args[0][1].should.deep.equal(
  375. this.user._id
  376. )
  377. })
  378. })
  379. })
  380. describe('validateNoSubscriptionInRecurly', function() {
  381. beforeEach(function() {
  382. this.subscriptions = []
  383. this.RecurlyWrapper.listAccountActiveSubscriptions = sinon
  384. .stub()
  385. .yields(null, this.subscriptions)
  386. this.SubscriptionUpdater.syncSubscription = sinon.stub().yields()
  387. return (this.callback = sinon.stub())
  388. })
  389. describe('with no subscription in recurly', function() {
  390. beforeEach(function() {
  391. this.subscriptions.push((this.subscription = { mock: 'subscription' }))
  392. return this.SubscriptionHandler.validateNoSubscriptionInRecurly(
  393. this.user_id,
  394. this.callback
  395. )
  396. })
  397. it('should call RecurlyWrapper.listAccountActiveSubscriptions with the user id', function() {
  398. return this.RecurlyWrapper.listAccountActiveSubscriptions
  399. .calledWith(this.user_id)
  400. .should.equal(true)
  401. })
  402. it('should sync the subscription', function() {
  403. return this.SubscriptionUpdater.syncSubscription
  404. .calledWith(this.subscription, this.user_id)
  405. .should.equal(true)
  406. })
  407. it('should call the callback with valid == false', function() {
  408. return this.callback.calledWith(null, false).should.equal(true)
  409. })
  410. })
  411. describe('with a subscription in recurly', function() {
  412. beforeEach(function() {
  413. return this.SubscriptionHandler.validateNoSubscriptionInRecurly(
  414. this.user_id,
  415. this.callback
  416. )
  417. })
  418. it('should not sync the subscription', function() {
  419. return this.SubscriptionUpdater.syncSubscription.called.should.equal(
  420. false
  421. )
  422. })
  423. it('should call the callback with valid == true', function() {
  424. return this.callback.calledWith(null, true).should.equal(true)
  425. })
  426. })
  427. })
  428. })