FeaturesUpdaterTests.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  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/AiHelper')
  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. '../../models/Subscription': {},
  123. },
  124. })
  125. })
  126. describe('computeFeatures', function () {
  127. beforeEach(function () {
  128. this.SubscriptionLocator.promises.getUsersSubscription
  129. .withArgs(this.user._id)
  130. .resolves(null)
  131. this.SubscriptionLocator.promises.getGroupSubscriptionsMemberOf
  132. .withArgs(this.user._id)
  133. .resolves([])
  134. this.ReferalFeatures.promises.getBonusFeatures.resolves({})
  135. this.V1SubscriptionManager.getGrandfatheredFeaturesForV1User
  136. .withArgs(this.v1UserId)
  137. .returns({})
  138. this.InstitutionsFeatures.promises.getInstitutionsFeatures.resolves({})
  139. })
  140. describe('individual subscriber', function () {
  141. beforeEach(function () {
  142. this.SubscriptionLocator.promises.getUsersSubscription
  143. .withArgs(this.user._id)
  144. .resolves(this.subscriptions.individual)
  145. })
  146. it('returns the individual features', async function () {
  147. const features = await this.FeaturesUpdater.promises.computeFeatures(
  148. this.user._id
  149. )
  150. expect(features).to.deep.equal({
  151. default: 'features',
  152. individual: 'features',
  153. })
  154. })
  155. })
  156. describe('group admin', function () {
  157. beforeEach(function () {
  158. this.SubscriptionLocator.promises.getUsersSubscription
  159. .withArgs(this.user._id)
  160. .resolves(this.subscriptions.group1)
  161. })
  162. it("doesn't return the group features", async function () {
  163. const features = await this.FeaturesUpdater.promises.computeFeatures(
  164. this.user._id
  165. )
  166. expect(features).to.deep.equal({
  167. default: 'features',
  168. })
  169. })
  170. })
  171. describe('group member', function () {
  172. beforeEach(function () {
  173. this.SubscriptionLocator.promises.getGroupSubscriptionsMemberOf
  174. .withArgs(this.user._id)
  175. .resolves([this.subscriptions.group1])
  176. })
  177. it('returns the group features', async function () {
  178. const features = await this.FeaturesUpdater.promises.computeFeatures(
  179. this.user._id
  180. )
  181. expect(features).to.deep.equal({
  182. default: 'features',
  183. group1: 'features',
  184. })
  185. })
  186. })
  187. describe('individual subscription + AI add-on', function () {
  188. beforeEach(function () {
  189. this.SubscriptionLocator.promises.getUsersSubscription
  190. .withArgs(this.user._id)
  191. .resolves(this.subscriptions.individualPlusAiAddOn)
  192. })
  193. it('returns the individual features and the AI error assistant', async function () {
  194. const features = await this.FeaturesUpdater.promises.computeFeatures(
  195. this.user._id
  196. )
  197. expect(features).to.deep.equal({
  198. default: 'features',
  199. individual: 'features',
  200. aiErrorAssistant: true,
  201. })
  202. })
  203. })
  204. describe('group admin + AI add-on', function () {
  205. beforeEach(function () {
  206. this.SubscriptionLocator.promises.getUsersSubscription
  207. .withArgs(this.user._id)
  208. .resolves(this.subscriptions.groupPlusAiAddOn)
  209. })
  210. it('returns the AI error assistant only', async function () {
  211. const features = await this.FeaturesUpdater.promises.computeFeatures(
  212. this.user._id
  213. )
  214. expect(features).to.deep.equal({
  215. default: 'features',
  216. aiErrorAssistant: true,
  217. })
  218. })
  219. })
  220. describe('group member + AI add-on', function () {
  221. beforeEach(function () {
  222. this.SubscriptionLocator.promises.getGroupSubscriptionsMemberOf
  223. .withArgs(this.user._id)
  224. .resolves([this.subscriptions.groupPlusAiAddOn])
  225. })
  226. it('returns the group features without the AI features', async function () {
  227. const features = await this.FeaturesUpdater.promises.computeFeatures(
  228. this.user._id
  229. )
  230. expect(features).to.deep.equal({
  231. default: 'features',
  232. group1: 'features',
  233. })
  234. })
  235. })
  236. })
  237. describe('refreshFeatures', function () {
  238. it('should return features and featuresChanged', async function () {
  239. const { features, featuresChanged } =
  240. await this.FeaturesUpdater.promises.refreshFeatures(
  241. this.user._id,
  242. 'test'
  243. )
  244. expect(features).to.exist
  245. expect(featuresChanged).to.exist
  246. })
  247. describe('normally', function () {
  248. beforeEach(async function () {
  249. await this.FeaturesUpdater.promises.refreshFeatures(
  250. this.user._id,
  251. 'test'
  252. )
  253. })
  254. it('should update the user with the merged features', function () {
  255. expect(
  256. this.UserFeaturesUpdater.promises.updateFeatures
  257. ).to.have.been.calledWith(this.user._id, this.Settings.features.all)
  258. })
  259. it('should send the corresponding feature set user property', function () {
  260. expect(
  261. this.AnalyticsManager.setUserPropertyForUserInBackground
  262. ).to.have.been.calledWith(this.user._id, 'feature-set', 'all')
  263. })
  264. })
  265. describe('with a non-standard feature set', async function () {
  266. beforeEach(async function () {
  267. this.SubscriptionLocator.promises.getGroupSubscriptionsMemberOf
  268. .withArgs(this.user._id)
  269. .resolves(null)
  270. await this.FeaturesUpdater.promises.refreshFeatures(
  271. this.user._id,
  272. 'test'
  273. )
  274. })
  275. it('should send mixed feature set user property', function () {
  276. sinon.assert.calledWith(
  277. this.AnalyticsManager.setUserPropertyForUserInBackground,
  278. this.user._id,
  279. 'feature-set',
  280. 'mixed'
  281. )
  282. })
  283. })
  284. describe('when losing dropbox feature', async function () {
  285. beforeEach(async function () {
  286. this.user.features = { dropbox: true }
  287. this.SubscriptionLocator.promises.getUsersSubscription
  288. .withArgs(this.user._id)
  289. .resolves(this.subscriptions.noDropbox)
  290. await this.FeaturesUpdater.promises.refreshFeatures(
  291. this.user._id,
  292. 'test'
  293. )
  294. })
  295. it('should fire module hook to unlink dropbox', function () {
  296. expect(this.Modules.promises.hooks.fire).to.have.been.calledWith(
  297. 'removeDropbox',
  298. this.user._id,
  299. 'test'
  300. )
  301. })
  302. })
  303. })
  304. describe('doSyncFromV1', function () {
  305. describe('when all goes well', function () {
  306. beforeEach(async function () {
  307. await this.FeaturesUpdater.promises.doSyncFromV1(this.v1UserId)
  308. })
  309. it('should update the user with the merged features', function () {
  310. expect(
  311. this.UserFeaturesUpdater.promises.updateFeatures
  312. ).to.have.been.calledWith(this.user._id, this.Settings.features.all)
  313. })
  314. })
  315. describe('when getUser produces an error', function () {
  316. beforeEach(function () {
  317. this.UserGetter.promises.getUser.rejects(new Error('woops'))
  318. })
  319. it('should propagate the error', async function () {
  320. const someId = 9090
  321. await expect(this.FeaturesUpdater.promises.doSyncFromV1(someId)).to.be
  322. .rejected
  323. expect(this.UserFeaturesUpdater.promises.updateFeatures).not.to.have
  324. .been.called
  325. })
  326. })
  327. describe('when getUser does not find a user', function () {
  328. beforeEach(async function () {
  329. const someOtherId = 987
  330. await this.FeaturesUpdater.promises.doSyncFromV1(someOtherId)
  331. })
  332. it('should not update the user', function () {
  333. expect(this.UserFeaturesUpdater.promises.updateFeatures).not.to.have
  334. .been.called
  335. })
  336. })
  337. })
  338. })