FeaturesUpdaterTests.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  1. const SandboxedModule = require('sandboxed-module')
  2. const { assert, expect } = require('chai')
  3. const sinon = require('sinon')
  4. const modulePath = '../../../../app/src/Features/Subscription/FeaturesUpdater'
  5. const { ObjectId } = require('mongodb')
  6. describe('FeaturesUpdater', function () {
  7. beforeEach(function () {
  8. this.user_id = ObjectId().toString()
  9. this.FeaturesUpdater = SandboxedModule.require(modulePath, {
  10. requires: {
  11. './UserFeaturesUpdater': (this.UserFeaturesUpdater = {}),
  12. './SubscriptionLocator': (this.SubscriptionLocator = {}),
  13. './PlansLocator': (this.PlansLocator = {}),
  14. '@overleaf/settings': (this.Settings = {
  15. features: {
  16. personal: {
  17. collaborators: 1,
  18. dropbox: false,
  19. compileTimeout: 60,
  20. compileGroup: 'standard',
  21. },
  22. collaborator: {
  23. collaborators: 10,
  24. dropbox: true,
  25. compileTimeout: 240,
  26. compileGroup: 'priority',
  27. },
  28. professional: {
  29. collaborators: -1,
  30. dropbox: true,
  31. compileTimeout: 240,
  32. compileGroup: 'priority',
  33. },
  34. },
  35. }),
  36. '../Referal/ReferalFeatures': (this.ReferalFeatures = {}),
  37. './V1SubscriptionManager': (this.V1SubscriptionManager = {}),
  38. '../Institutions/InstitutionsFeatures': (this.InstitutionsFeatures = {}),
  39. '../User/UserGetter': (this.UserGetter = {}),
  40. '../Analytics/AnalyticsManager': (this.AnalyticsManager = {
  41. setUserProperty: sinon.stub(),
  42. }),
  43. '../../infrastructure/Modules': (this.Modules = {
  44. hooks: { fire: sinon.stub() },
  45. }),
  46. },
  47. })
  48. })
  49. describe('refreshFeatures', function () {
  50. beforeEach(function () {
  51. this.user = {
  52. _id: this.user_id,
  53. features: {},
  54. }
  55. this.UserFeaturesUpdater.updateFeatures = sinon
  56. .stub()
  57. .yields(null, { some: 'features' }, true)
  58. this.FeaturesUpdater._getIndividualFeatures = sinon
  59. .stub()
  60. .yields(null, { individual: 'features' })
  61. this.FeaturesUpdater._getGroupFeatureSets = sinon
  62. .stub()
  63. .yields(null, [{ group: 'features' }, { group: 'features2' }])
  64. this.InstitutionsFeatures.getInstitutionsFeatures = sinon
  65. .stub()
  66. .yields(null, { institutions: 'features' })
  67. this.FeaturesUpdater._getV1Features = sinon
  68. .stub()
  69. .yields(null, { v1: 'features' })
  70. this.ReferalFeatures.getBonusFeatures = sinon
  71. .stub()
  72. .yields(null, { bonus: 'features' })
  73. this.FeaturesUpdater._mergeFeatures = sinon
  74. .stub()
  75. .returns({ merged: 'features' })
  76. this.UserGetter.getUser = sinon.stub().yields(null, this.user)
  77. this.callback = sinon.stub()
  78. })
  79. it('should return features and featuresChanged', function () {
  80. this.FeaturesUpdater.refreshFeatures(
  81. this.user_id,
  82. 'test',
  83. (err, features, featuresChanged) => {
  84. expect(err).to.not.exist
  85. expect(features).to.exist
  86. expect(featuresChanged).to.exist
  87. }
  88. )
  89. })
  90. describe('normally', function () {
  91. beforeEach(function () {
  92. this.FeaturesUpdater.refreshFeatures(
  93. this.user_id,
  94. 'test',
  95. this.callback
  96. )
  97. })
  98. it('should get the individual features', function () {
  99. this.FeaturesUpdater._getIndividualFeatures
  100. .calledWith(this.user_id)
  101. .should.equal(true)
  102. })
  103. it('should get the group features', function () {
  104. this.FeaturesUpdater._getGroupFeatureSets
  105. .calledWith(this.user_id)
  106. .should.equal(true)
  107. })
  108. it('should get the institution features', function () {
  109. this.InstitutionsFeatures.getInstitutionsFeatures
  110. .calledWith(this.user_id)
  111. .should.equal(true)
  112. })
  113. it('should get the v1 features', function () {
  114. this.FeaturesUpdater._getV1Features
  115. .calledWith(this.user_id)
  116. .should.equal(true)
  117. })
  118. it('should get the bonus features', function () {
  119. this.ReferalFeatures.getBonusFeatures
  120. .calledWith(this.user_id)
  121. .should.equal(true)
  122. })
  123. it('should merge from the default features', function () {
  124. this.FeaturesUpdater._mergeFeatures
  125. .calledWith(this.Settings.defaultFeatures)
  126. .should.equal(true)
  127. })
  128. it('should merge the individual features', function () {
  129. this.FeaturesUpdater._mergeFeatures
  130. .calledWith(sinon.match.any, { individual: 'features' })
  131. .should.equal(true)
  132. })
  133. it('should merge the group features', function () {
  134. this.FeaturesUpdater._mergeFeatures
  135. .calledWith(sinon.match.any, { group: 'features' })
  136. .should.equal(true)
  137. this.FeaturesUpdater._mergeFeatures
  138. .calledWith(sinon.match.any, { group: 'features2' })
  139. .should.equal(true)
  140. })
  141. it('should merge the institutions features', function () {
  142. this.FeaturesUpdater._mergeFeatures
  143. .calledWith(sinon.match.any, { institutions: 'features' })
  144. .should.equal(true)
  145. })
  146. it('should merge the v1 features', function () {
  147. this.FeaturesUpdater._mergeFeatures
  148. .calledWith(sinon.match.any, { v1: 'features' })
  149. .should.equal(true)
  150. })
  151. it('should merge the bonus features', function () {
  152. this.FeaturesUpdater._mergeFeatures
  153. .calledWith(sinon.match.any, { bonus: 'features' })
  154. .should.equal(true)
  155. })
  156. it('should update the user with the merged features', function () {
  157. this.UserFeaturesUpdater.updateFeatures
  158. .calledWith(this.user_id, { merged: 'features' })
  159. .should.equal(true)
  160. })
  161. })
  162. describe('analytics user properties', function () {
  163. it('should send the corresponding feature set user property', function () {
  164. this.FeaturesUpdater._mergeFeatures = sinon
  165. .stub()
  166. .returns(this.Settings.features.personal)
  167. this.FeaturesUpdater.refreshFeatures(
  168. this.user_id,
  169. 'test',
  170. this.callback
  171. )
  172. sinon.assert.calledWith(
  173. this.AnalyticsManager.setUserProperty,
  174. this.user_id,
  175. 'feature-set',
  176. 'personal'
  177. )
  178. })
  179. it('should send mixed feature set user property', function () {
  180. this.FeaturesUpdater._mergeFeatures = sinon
  181. .stub()
  182. .returns({ dropbox: true, feature: 'some' })
  183. this.FeaturesUpdater.refreshFeatures(
  184. this.user_id,
  185. 'test',
  186. this.callback
  187. )
  188. sinon.assert.calledWith(
  189. this.AnalyticsManager.setUserProperty,
  190. this.user_id,
  191. 'feature-set',
  192. 'mixed'
  193. )
  194. })
  195. })
  196. describe('when losing dropbox feature', function () {
  197. beforeEach(function () {
  198. this.user = {
  199. _id: this.user_id,
  200. features: { dropbox: true },
  201. }
  202. this.UserGetter.getUser = sinon.stub().yields(null, this.user)
  203. this.FeaturesUpdater._mergeFeatures = sinon
  204. .stub()
  205. .returns({ dropbox: false })
  206. this.FeaturesUpdater.refreshFeatures(
  207. this.user_id,
  208. 'test',
  209. this.callback
  210. )
  211. })
  212. it('should fire module hook to unlink dropbox', function () {
  213. this.Modules.hooks.fire
  214. .calledWith('removeDropbox', this.user._id, 'test')
  215. .should.equal(true)
  216. })
  217. })
  218. })
  219. describe('_mergeFeatures', function () {
  220. it('should prefer priority over standard for compileGroup', function () {
  221. expect(
  222. this.FeaturesUpdater._mergeFeatures(
  223. {
  224. compileGroup: 'priority',
  225. },
  226. {
  227. compileGroup: 'standard',
  228. }
  229. )
  230. ).to.deep.equal({
  231. compileGroup: 'priority',
  232. })
  233. expect(
  234. this.FeaturesUpdater._mergeFeatures(
  235. {
  236. compileGroup: 'standard',
  237. },
  238. {
  239. compileGroup: 'priority',
  240. }
  241. )
  242. ).to.deep.equal({
  243. compileGroup: 'priority',
  244. })
  245. expect(
  246. this.FeaturesUpdater._mergeFeatures(
  247. {
  248. compileGroup: 'priority',
  249. },
  250. {
  251. compileGroup: 'priority',
  252. }
  253. )
  254. ).to.deep.equal({
  255. compileGroup: 'priority',
  256. })
  257. expect(
  258. this.FeaturesUpdater._mergeFeatures(
  259. {
  260. compileGroup: 'standard',
  261. },
  262. {
  263. compileGroup: 'standard',
  264. }
  265. )
  266. ).to.deep.equal({
  267. compileGroup: 'standard',
  268. })
  269. })
  270. it('should prefer -1 over any other for collaborators', function () {
  271. expect(
  272. this.FeaturesUpdater._mergeFeatures(
  273. {
  274. collaborators: -1,
  275. },
  276. {
  277. collaborators: 10,
  278. }
  279. )
  280. ).to.deep.equal({
  281. collaborators: -1,
  282. })
  283. expect(
  284. this.FeaturesUpdater._mergeFeatures(
  285. {
  286. collaborators: 10,
  287. },
  288. {
  289. collaborators: -1,
  290. }
  291. )
  292. ).to.deep.equal({
  293. collaborators: -1,
  294. })
  295. expect(
  296. this.FeaturesUpdater._mergeFeatures(
  297. {
  298. collaborators: 4,
  299. },
  300. {
  301. collaborators: 10,
  302. }
  303. )
  304. ).to.deep.equal({
  305. collaborators: 10,
  306. })
  307. })
  308. it('should prefer the higher of compileTimeout', function () {
  309. expect(
  310. this.FeaturesUpdater._mergeFeatures(
  311. {
  312. compileTimeout: 20,
  313. },
  314. {
  315. compileTimeout: 10,
  316. }
  317. )
  318. ).to.deep.equal({
  319. compileTimeout: 20,
  320. })
  321. expect(
  322. this.FeaturesUpdater._mergeFeatures(
  323. {
  324. compileTimeout: 10,
  325. },
  326. {
  327. compileTimeout: 20,
  328. }
  329. )
  330. ).to.deep.equal({
  331. compileTimeout: 20,
  332. })
  333. })
  334. it('should prefer the true over false for other keys', function () {
  335. expect(
  336. this.FeaturesUpdater._mergeFeatures(
  337. {
  338. github: true,
  339. },
  340. {
  341. github: false,
  342. }
  343. )
  344. ).to.deep.equal({
  345. github: true,
  346. })
  347. expect(
  348. this.FeaturesUpdater._mergeFeatures(
  349. {
  350. github: false,
  351. },
  352. {
  353. github: true,
  354. }
  355. )
  356. ).to.deep.equal({
  357. github: true,
  358. })
  359. expect(
  360. this.FeaturesUpdater._mergeFeatures(
  361. {
  362. github: true,
  363. },
  364. {
  365. github: true,
  366. }
  367. )
  368. ).to.deep.equal({
  369. github: true,
  370. })
  371. expect(
  372. this.FeaturesUpdater._mergeFeatures(
  373. {
  374. github: false,
  375. },
  376. {
  377. github: false,
  378. }
  379. )
  380. ).to.deep.equal({
  381. github: false,
  382. })
  383. })
  384. })
  385. describe('doSyncFromV1', function () {
  386. beforeEach(function () {
  387. this.v1UserId = 1
  388. this.user = {
  389. _id: this.user_id,
  390. email: 'user@example.com',
  391. overleaf: {
  392. id: this.v1UserId,
  393. },
  394. }
  395. this.UserGetter.getUser = sinon.stub().callsArgWith(2, null, this.user)
  396. this.FeaturesUpdater.refreshFeatures = sinon.stub().yields(null)
  397. this.call = cb => {
  398. this.FeaturesUpdater.doSyncFromV1(this.v1UserId, cb)
  399. }
  400. })
  401. describe('when all goes well', function () {
  402. it('should call getUser', function (done) {
  403. this.call(() => {
  404. expect(this.UserGetter.getUser.callCount).to.equal(1)
  405. expect(
  406. this.UserGetter.getUser.calledWith({ 'overleaf.id': this.v1UserId })
  407. ).to.equal(true)
  408. done()
  409. })
  410. })
  411. it('should call refreshFeatures', function (done) {
  412. this.call(() => {
  413. expect(this.FeaturesUpdater.refreshFeatures.callCount).to.equal(1)
  414. expect(
  415. this.FeaturesUpdater.refreshFeatures.calledWith(this.user_id)
  416. ).to.equal(true)
  417. done()
  418. })
  419. })
  420. it('should not produce an error', function (done) {
  421. this.call(err => {
  422. expect(err).to.not.exist
  423. done()
  424. })
  425. })
  426. })
  427. describe('when getUser produces an error', function () {
  428. beforeEach(function () {
  429. this.UserGetter.getUser = sinon
  430. .stub()
  431. .callsArgWith(2, new Error('woops'))
  432. })
  433. it('should not call refreshFeatures', function () {
  434. expect(this.FeaturesUpdater.refreshFeatures.callCount).to.equal(0)
  435. })
  436. it('should produce an error', function (done) {
  437. this.call(err => {
  438. expect(err).to.exist
  439. done()
  440. })
  441. })
  442. })
  443. describe('when getUser does not find a user', function () {
  444. beforeEach(function () {
  445. this.UserGetter.getUser = sinon.stub().callsArgWith(2, null, null)
  446. })
  447. it('should not call refreshFeatures', function (done) {
  448. this.call(() => {
  449. expect(this.FeaturesUpdater.refreshFeatures.callCount).to.equal(0)
  450. done()
  451. })
  452. })
  453. it('should not produce an error', function (done) {
  454. this.call(err => {
  455. expect(err).to.not.exist
  456. done()
  457. })
  458. })
  459. })
  460. })
  461. describe('isFeatureSetBetter', function () {
  462. it('simple comparisons', function () {
  463. const result1 = this.FeaturesUpdater.isFeatureSetBetter(
  464. {
  465. dropbox: true,
  466. },
  467. {
  468. dropbox: false,
  469. }
  470. )
  471. assert.isTrue(result1)
  472. const result2 = this.FeaturesUpdater.isFeatureSetBetter(
  473. {
  474. dropbox: false,
  475. },
  476. {
  477. dropbox: true,
  478. }
  479. )
  480. assert.isFalse(result2)
  481. })
  482. it('compound comparisons with same features', function () {
  483. const result1 = this.FeaturesUpdater.isFeatureSetBetter(
  484. {
  485. collaborators: 9,
  486. dropbox: true,
  487. },
  488. {
  489. collaborators: 10,
  490. dropbox: true,
  491. }
  492. )
  493. assert.isFalse(result1)
  494. const result2 = this.FeaturesUpdater.isFeatureSetBetter(
  495. {
  496. collaborators: -1,
  497. dropbox: true,
  498. },
  499. {
  500. collaborators: 10,
  501. dropbox: true,
  502. }
  503. )
  504. assert.isTrue(result2)
  505. const result3 = this.FeaturesUpdater.isFeatureSetBetter(
  506. {
  507. collaborators: -1,
  508. compileTimeout: 60,
  509. dropbox: true,
  510. },
  511. {
  512. collaborators: 10,
  513. compileTimeout: 60,
  514. dropbox: true,
  515. }
  516. )
  517. assert.isTrue(result3)
  518. })
  519. })
  520. })