FeaturesUpdaterTests.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. const SandboxedModule = require('sandboxed-module')
  2. const { expect } = require('chai')
  3. const sinon = require('sinon')
  4. const { ObjectId } = require('mongodb-legacy')
  5. const {
  6. AI_ADD_ON_CODE,
  7. } = require('../../../../app/src/Features/Subscription/RecurlyEntities')
  8. const MODULE_PATH = '../../../../app/src/Features/Subscription/FeaturesUpdater'
  9. describe('FeaturesUpdater', function () {
  10. beforeEach(function () {
  11. this.v1UserId = 12345
  12. this.user = {
  13. _id: new ObjectId(),
  14. features: {},
  15. overleaf: { id: this.v1UserId },
  16. }
  17. this.aiAddOn = { addOnCode: AI_ADD_ON_CODE, quantity: 1 }
  18. this.subscriptions = {
  19. individual: { planCode: 'individual-plan' },
  20. group1: { planCode: 'group-plan-1', groupPlan: true },
  21. group2: { planCode: 'group-plan-2', groupPlan: true },
  22. noDropbox: { planCode: 'no-dropbox' },
  23. individualPlusAiAddOn: {
  24. planCode: 'individual-plan',
  25. addOns: [this.aiAddOn],
  26. },
  27. groupPlusAiAddOn: {
  28. planCode: 'group-plan-1',
  29. groupPlan: true,
  30. addOns: [this.aiAddOn],
  31. },
  32. }
  33. this.UserFeaturesUpdater = {
  34. promises: {
  35. updateFeatures: sinon
  36. .stub()
  37. .resolves({ features: { some: 'features' }, featuresChanged: true }),
  38. },
  39. }
  40. this.SubscriptionLocator = {
  41. promises: {
  42. getUsersSubscription: sinon.stub(),
  43. getGroupSubscriptionsMemberOf: sinon.stub(),
  44. },
  45. }
  46. this.SubscriptionLocator.promises.getUsersSubscription
  47. .withArgs(this.user._id)
  48. .resolves(this.subscriptions.individual)
  49. this.SubscriptionLocator.promises.getGroupSubscriptionsMemberOf
  50. .withArgs(this.user._id)
  51. .resolves([this.subscriptions.group1, this.subscriptions.group2])
  52. this.Settings = {
  53. defaultFeatures: { default: 'features' },
  54. plans: [
  55. { planCode: 'individual-plan', features: { individual: 'features' } },
  56. { planCode: 'group-plan-1', features: { group1: 'features' } },
  57. { planCode: 'group-plan-2', features: { group2: 'features' } },
  58. { planCode: 'no-dropbox', features: { dropbox: false } },
  59. ],
  60. features: {
  61. all: {
  62. default: 'features',
  63. individual: 'features',
  64. group1: 'features',
  65. group2: 'features',
  66. institutions: 'features',
  67. grandfathered: 'features',
  68. bonus: 'features',
  69. },
  70. },
  71. }
  72. this.ReferalFeatures = {
  73. promises: {
  74. getBonusFeatures: sinon.stub().resolves({ bonus: 'features' }),
  75. },
  76. }
  77. this.V1SubscriptionManager = {
  78. getGrandfatheredFeaturesForV1User: sinon.stub(),
  79. }
  80. this.V1SubscriptionManager.getGrandfatheredFeaturesForV1User
  81. .withArgs(this.v1UserId)
  82. .returns({ grandfathered: 'features' })
  83. this.InstitutionsFeatures = {
  84. promises: {
  85. getInstitutionsFeatures: sinon
  86. .stub()
  87. .resolves({ institutions: 'features' }),
  88. },
  89. }
  90. this.UserGetter = {
  91. promises: {
  92. getUser: sinon.stub().resolves(null),
  93. },
  94. }
  95. this.UserGetter.promises.getUser.withArgs(this.user._id).resolves(this.user)
  96. this.UserGetter.promises.getUser
  97. .withArgs({ 'overleaf.id': this.v1UserId })
  98. .resolves(this.user)
  99. this.AnalyticsManager = {
  100. setUserPropertyForUserInBackground: sinon.stub(),
  101. }
  102. this.Modules = {
  103. promises: { hooks: { fire: sinon.stub().resolves() } },
  104. }
  105. this.Queues = {
  106. getQueue: sinon.stub().returns({
  107. add: sinon.stub().resolves(),
  108. }),
  109. }
  110. this.FeaturesUpdater = SandboxedModule.require(MODULE_PATH, {
  111. requires: {
  112. './UserFeaturesUpdater': this.UserFeaturesUpdater,
  113. './SubscriptionLocator': this.SubscriptionLocator,
  114. '@overleaf/settings': this.Settings,
  115. '../Referal/ReferalFeatures': this.ReferalFeatures,
  116. './V1SubscriptionManager': this.V1SubscriptionManager,
  117. '../Institutions/InstitutionsFeatures': this.InstitutionsFeatures,
  118. '../User/UserGetter': this.UserGetter,
  119. '../Analytics/AnalyticsManager': this.AnalyticsManager,
  120. '../../infrastructure/Modules': this.Modules,
  121. '../../infrastructure/Queues': this.Queues,
  122. },
  123. })
  124. })
  125. describe('computeFeatures', function () {
  126. beforeEach(function () {
  127. this.SubscriptionLocator.promises.getUsersSubscription
  128. .withArgs(this.user._id)
  129. .resolves(null)
  130. this.SubscriptionLocator.promises.getGroupSubscriptionsMemberOf
  131. .withArgs(this.user._id)
  132. .resolves([])
  133. this.ReferalFeatures.promises.getBonusFeatures.resolves({})
  134. this.V1SubscriptionManager.getGrandfatheredFeaturesForV1User
  135. .withArgs(this.v1UserId)
  136. .returns({})
  137. this.InstitutionsFeatures.promises.getInstitutionsFeatures.resolves({})
  138. })
  139. describe('individual subscriber', function () {
  140. beforeEach(function () {
  141. this.SubscriptionLocator.promises.getUsersSubscription
  142. .withArgs(this.user._id)
  143. .resolves(this.subscriptions.individual)
  144. })
  145. it('returns the individual features', async function () {
  146. const features = await this.FeaturesUpdater.promises.computeFeatures(
  147. this.user._id
  148. )
  149. expect(features).to.deep.equal({
  150. default: 'features',
  151. individual: 'features',
  152. })
  153. })
  154. })
  155. describe('group admin', function () {
  156. beforeEach(function () {
  157. this.SubscriptionLocator.promises.getUsersSubscription
  158. .withArgs(this.user._id)
  159. .resolves(this.subscriptions.group1)
  160. })
  161. it("doesn't return the group features", async function () {
  162. const features = await this.FeaturesUpdater.promises.computeFeatures(
  163. this.user._id
  164. )
  165. expect(features).to.deep.equal({
  166. default: 'features',
  167. })
  168. })
  169. })
  170. describe('group member', function () {
  171. beforeEach(function () {
  172. this.SubscriptionLocator.promises.getGroupSubscriptionsMemberOf
  173. .withArgs(this.user._id)
  174. .resolves([this.subscriptions.group1])
  175. })
  176. it('returns the group features', async function () {
  177. const features = await this.FeaturesUpdater.promises.computeFeatures(
  178. this.user._id
  179. )
  180. expect(features).to.deep.equal({
  181. default: 'features',
  182. group1: 'features',
  183. })
  184. })
  185. })
  186. describe('individual subscription + AI add-on', function () {
  187. beforeEach(function () {
  188. this.SubscriptionLocator.promises.getUsersSubscription
  189. .withArgs(this.user._id)
  190. .resolves(this.subscriptions.individualPlusAiAddOn)
  191. })
  192. it('returns the individual features and the AI error assistant', async function () {
  193. const features = await this.FeaturesUpdater.promises.computeFeatures(
  194. this.user._id
  195. )
  196. expect(features).to.deep.equal({
  197. default: 'features',
  198. individual: 'features',
  199. aiErrorAssistant: true,
  200. })
  201. })
  202. })
  203. describe('group admin + AI add-on', function () {
  204. beforeEach(function () {
  205. this.SubscriptionLocator.promises.getUsersSubscription
  206. .withArgs(this.user._id)
  207. .resolves(this.subscriptions.groupPlusAiAddOn)
  208. })
  209. it('returns the AI error assistant only', async function () {
  210. const features = await this.FeaturesUpdater.promises.computeFeatures(
  211. this.user._id
  212. )
  213. expect(features).to.deep.equal({
  214. default: 'features',
  215. aiErrorAssistant: true,
  216. })
  217. })
  218. })
  219. describe('group member + AI add-on', function () {
  220. beforeEach(function () {
  221. this.SubscriptionLocator.promises.getGroupSubscriptionsMemberOf
  222. .withArgs(this.user._id)
  223. .resolves([this.subscriptions.groupPlusAiAddOn])
  224. })
  225. it('returns the group features without the AI features', async function () {
  226. const features = await this.FeaturesUpdater.promises.computeFeatures(
  227. this.user._id
  228. )
  229. expect(features).to.deep.equal({
  230. default: 'features',
  231. group1: 'features',
  232. })
  233. })
  234. })
  235. })
  236. describe('refreshFeatures', function () {
  237. it('should return features and featuresChanged', async function () {
  238. const { features, featuresChanged } =
  239. await this.FeaturesUpdater.promises.refreshFeatures(
  240. this.user._id,
  241. 'test'
  242. )
  243. expect(features).to.exist
  244. expect(featuresChanged).to.exist
  245. })
  246. describe('normally', function () {
  247. beforeEach(async function () {
  248. await this.FeaturesUpdater.promises.refreshFeatures(
  249. this.user._id,
  250. 'test'
  251. )
  252. })
  253. it('should update the user with the merged features', function () {
  254. expect(
  255. this.UserFeaturesUpdater.promises.updateFeatures
  256. ).to.have.been.calledWith(this.user._id, this.Settings.features.all)
  257. })
  258. it('should send the corresponding feature set user property', function () {
  259. expect(
  260. this.AnalyticsManager.setUserPropertyForUserInBackground
  261. ).to.have.been.calledWith(this.user._id, 'feature-set', 'all')
  262. })
  263. })
  264. describe('with a non-standard feature set', async function () {
  265. beforeEach(async function () {
  266. this.SubscriptionLocator.promises.getGroupSubscriptionsMemberOf
  267. .withArgs(this.user._id)
  268. .resolves(null)
  269. await this.FeaturesUpdater.promises.refreshFeatures(
  270. this.user._id,
  271. 'test'
  272. )
  273. })
  274. it('should send mixed feature set user property', function () {
  275. sinon.assert.calledWith(
  276. this.AnalyticsManager.setUserPropertyForUserInBackground,
  277. this.user._id,
  278. 'feature-set',
  279. 'mixed'
  280. )
  281. })
  282. })
  283. describe('when losing dropbox feature', async function () {
  284. beforeEach(async function () {
  285. this.user.features = { dropbox: true }
  286. this.SubscriptionLocator.promises.getUsersSubscription
  287. .withArgs(this.user._id)
  288. .resolves(this.subscriptions.noDropbox)
  289. await this.FeaturesUpdater.promises.refreshFeatures(
  290. this.user._id,
  291. 'test'
  292. )
  293. })
  294. it('should fire module hook to unlink dropbox', function () {
  295. expect(this.Modules.promises.hooks.fire).to.have.been.calledWith(
  296. 'removeDropbox',
  297. this.user._id,
  298. 'test'
  299. )
  300. })
  301. })
  302. })
  303. describe('doSyncFromV1', function () {
  304. describe('when all goes well', function () {
  305. beforeEach(async function () {
  306. await this.FeaturesUpdater.promises.doSyncFromV1(this.v1UserId)
  307. })
  308. it('should update the user with the merged features', function () {
  309. expect(
  310. this.UserFeaturesUpdater.promises.updateFeatures
  311. ).to.have.been.calledWith(this.user._id, this.Settings.features.all)
  312. })
  313. })
  314. describe('when getUser produces an error', function () {
  315. beforeEach(function () {
  316. this.UserGetter.promises.getUser.rejects(new Error('woops'))
  317. })
  318. it('should propagate the error', async function () {
  319. const someId = 9090
  320. await expect(this.FeaturesUpdater.promises.doSyncFromV1(someId)).to.be
  321. .rejected
  322. expect(this.UserFeaturesUpdater.promises.updateFeatures).not.to.have
  323. .been.called
  324. })
  325. })
  326. describe('when getUser does not find a user', function () {
  327. beforeEach(async function () {
  328. const someOtherId = 987
  329. await this.FeaturesUpdater.promises.doSyncFromV1(someOtherId)
  330. })
  331. it('should not update the user', function () {
  332. expect(this.UserFeaturesUpdater.promises.updateFeatures).not.to.have
  333. .been.called
  334. })
  335. })
  336. })
  337. })