FeaturesUpdaterTests.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. const SandboxedModule = require('sandboxed-module')
  2. const { expect } = require('chai')
  3. const sinon = require('sinon')
  4. const { ObjectId } = require('mongodb')
  5. const MODULE_PATH = '../../../../app/src/Features/Subscription/FeaturesUpdater'
  6. describe('FeaturesUpdater', function () {
  7. beforeEach(function () {
  8. this.user = {
  9. _id: new ObjectId(),
  10. features: {},
  11. }
  12. this.v1UserId = 12345
  13. this.subscriptions = {
  14. individual: { planCode: 'individual-plan' },
  15. group1: { planCode: 'group-plan-1' },
  16. group2: { planCode: 'group-plan-2' },
  17. noDropbox: { planCode: 'no-dropbox' },
  18. }
  19. this.UserFeaturesUpdater = {
  20. promises: {
  21. updateFeatures: sinon
  22. .stub()
  23. .resolves({ features: { some: 'features' }, featuresChanged: true }),
  24. },
  25. }
  26. this.SubscriptionLocator = {
  27. promises: {
  28. getUserIndividualSubscription: sinon.stub(),
  29. getGroupSubscriptionsMemberOf: sinon.stub(),
  30. },
  31. }
  32. this.SubscriptionLocator.promises.getUserIndividualSubscription
  33. .withArgs(this.user._id)
  34. .resolves(this.subscriptions.individual)
  35. this.SubscriptionLocator.promises.getGroupSubscriptionsMemberOf
  36. .withArgs(this.user._id)
  37. .resolves([this.subscriptions.group1, this.subscriptions.group2])
  38. this.Settings = {
  39. defaultFeatures: { default: 'features' },
  40. plans: [
  41. { planCode: 'individual-plan', features: { individual: 'features' } },
  42. { planCode: 'group-plan-1', features: { group1: 'features' } },
  43. { planCode: 'group-plan-2', features: { group2: 'features' } },
  44. { planCode: 'v1-plan', features: { v1: 'features' } },
  45. { planCode: 'no-dropbox', features: { dropbox: false } },
  46. ],
  47. features: {
  48. all: {
  49. default: 'features',
  50. individual: 'features',
  51. group1: 'features',
  52. group2: 'features',
  53. institutions: 'features',
  54. v1: 'features',
  55. grandfathered: 'features',
  56. bonus: 'features',
  57. },
  58. },
  59. }
  60. this.ReferalFeatures = {
  61. promises: {
  62. getBonusFeatures: sinon.stub().resolves({ bonus: 'features' }),
  63. },
  64. }
  65. this.V1SubscriptionManager = {
  66. getGrandfatheredFeaturesForV1User: sinon.stub(),
  67. promises: {
  68. getPlanCodeFromV1: sinon.stub(),
  69. },
  70. }
  71. this.V1SubscriptionManager.promises.getPlanCodeFromV1
  72. .withArgs(this.user._id)
  73. .resolves({ planCode: 'v1-plan', v1Id: this.v1UserId })
  74. this.V1SubscriptionManager.getGrandfatheredFeaturesForV1User
  75. .withArgs(this.v1UserId)
  76. .returns({ grandfathered: 'features' })
  77. this.InstitutionsFeatures = {
  78. promises: {
  79. getInstitutionsFeatures: sinon
  80. .stub()
  81. .resolves({ institutions: 'features' }),
  82. },
  83. }
  84. this.UserGetter = {
  85. promises: {
  86. getUser: sinon.stub().resolves(null),
  87. },
  88. }
  89. this.UserGetter.promises.getUser.withArgs(this.user._id).resolves(this.user)
  90. this.UserGetter.promises.getUser
  91. .withArgs({ 'overleaf.id': this.v1UserId })
  92. .resolves(this.user)
  93. this.AnalyticsManager = {
  94. setUserPropertyForUser: sinon.stub(),
  95. }
  96. this.Modules = {
  97. promises: { hooks: { fire: sinon.stub().resolves() } },
  98. }
  99. this.FeaturesUpdater = SandboxedModule.require(MODULE_PATH, {
  100. requires: {
  101. './UserFeaturesUpdater': this.UserFeaturesUpdater,
  102. './SubscriptionLocator': this.SubscriptionLocator,
  103. '@overleaf/settings': this.Settings,
  104. '../Referal/ReferalFeatures': this.ReferalFeatures,
  105. './V1SubscriptionManager': this.V1SubscriptionManager,
  106. '../Institutions/InstitutionsFeatures': this.InstitutionsFeatures,
  107. '../User/UserGetter': this.UserGetter,
  108. '../Analytics/AnalyticsManager': this.AnalyticsManager,
  109. '../../infrastructure/Modules': this.Modules,
  110. },
  111. })
  112. })
  113. describe('refreshFeatures', function () {
  114. it('should return features and featuresChanged', async function () {
  115. const { features, featuresChanged } =
  116. await this.FeaturesUpdater.promises.refreshFeatures(
  117. this.user._id,
  118. 'test'
  119. )
  120. expect(features).to.exist
  121. expect(featuresChanged).to.exist
  122. })
  123. describe('normally', function () {
  124. beforeEach(async function () {
  125. await this.FeaturesUpdater.promises.refreshFeatures(
  126. this.user._id,
  127. 'test'
  128. )
  129. })
  130. it('should update the user with the merged features', function () {
  131. expect(
  132. this.UserFeaturesUpdater.promises.updateFeatures
  133. ).to.have.been.calledWith(this.user._id, this.Settings.features.all)
  134. })
  135. it('should send the corresponding feature set user property', function () {
  136. expect(
  137. this.AnalyticsManager.setUserPropertyForUser
  138. ).to.have.been.calledWith(this.user._id, 'feature-set', 'all')
  139. })
  140. })
  141. describe('with a non-standard feature set', async function () {
  142. beforeEach(async function () {
  143. this.SubscriptionLocator.promises.getGroupSubscriptionsMemberOf
  144. .withArgs(this.user._id)
  145. .resolves(null)
  146. await this.FeaturesUpdater.promises.refreshFeatures(
  147. this.user._id,
  148. 'test'
  149. )
  150. })
  151. it('should send mixed feature set user property', function () {
  152. sinon.assert.calledWith(
  153. this.AnalyticsManager.setUserPropertyForUser,
  154. this.user._id,
  155. 'feature-set',
  156. 'mixed'
  157. )
  158. })
  159. })
  160. describe('when losing dropbox feature', async function () {
  161. beforeEach(async function () {
  162. this.user.features = { dropbox: true }
  163. this.SubscriptionLocator.promises.getUserIndividualSubscription
  164. .withArgs(this.user._id)
  165. .resolves(this.subscriptions.noDropbox)
  166. await this.FeaturesUpdater.promises.refreshFeatures(
  167. this.user._id,
  168. 'test'
  169. )
  170. })
  171. it('should fire module hook to unlink dropbox', function () {
  172. expect(this.Modules.promises.hooks.fire).to.have.been.calledWith(
  173. 'removeDropbox',
  174. this.user._id,
  175. 'test'
  176. )
  177. })
  178. })
  179. })
  180. describe('doSyncFromV1', function () {
  181. describe('when all goes well', function () {
  182. beforeEach(async function () {
  183. await this.FeaturesUpdater.promises.doSyncFromV1(this.v1UserId)
  184. })
  185. it('should update the user with the merged features', function () {
  186. expect(
  187. this.UserFeaturesUpdater.promises.updateFeatures
  188. ).to.have.been.calledWith(this.user._id, this.Settings.features.all)
  189. })
  190. })
  191. describe('when getUser produces an error', function () {
  192. beforeEach(function () {
  193. this.UserGetter.promises.getUser.rejects(new Error('woops'))
  194. })
  195. it('should propagate the error', async function () {
  196. const someId = 9090
  197. await expect(this.FeaturesUpdater.promises.doSyncFromV1(someId)).to.be
  198. .rejected
  199. expect(this.UserFeaturesUpdater.promises.updateFeatures).not.to.have
  200. .been.called
  201. })
  202. })
  203. describe('when getUser does not find a user', function () {
  204. beforeEach(async function () {
  205. const someOtherId = 987
  206. await this.FeaturesUpdater.promises.doSyncFromV1(someOtherId)
  207. })
  208. it('should not update the user', function () {
  209. expect(this.UserFeaturesUpdater.promises.updateFeatures).not.to.have
  210. .been.called
  211. })
  212. })
  213. })
  214. })