FeaturesUpdaterTests.js 7.1 KB

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