overleaf/services/web/test/unit/src/Subscription/FeaturesUpdaterTests.js

557 lines
15 KiB
JavaScript
Raw Normal View History

const SandboxedModule = require('sandboxed-module')
const { assert, expect } = require('chai')
const sinon = require('sinon')
const modulePath = '../../../../app/src/Features/Subscription/FeaturesUpdater'
const { ObjectId } = require('mongodb')
describe('FeaturesUpdater', function () {
beforeEach(function () {
this.user_id = ObjectId().toString()
this.FeaturesUpdater = SandboxedModule.require(modulePath, {
requires: {
'./UserFeaturesUpdater': (this.UserFeaturesUpdater = {}),
'./SubscriptionLocator': (this.SubscriptionLocator = {}),
'./PlansLocator': (this.PlansLocator = {}),
'@overleaf/settings': (this.Settings = {
features: {
personal: {
collaborators: 1,
dropbox: false,
compileTimeout: 60,
compileGroup: 'standard',
},
collaborator: {
collaborators: 10,
dropbox: true,
compileTimeout: 240,
compileGroup: 'priority',
},
professional: {
collaborators: -1,
dropbox: true,
compileTimeout: 240,
compileGroup: 'priority',
},
},
}),
'../Referal/ReferalFeatures': (this.ReferalFeatures = {}),
'./V1SubscriptionManager': (this.V1SubscriptionManager = {}),
'../Institutions/InstitutionsFeatures': (this.InstitutionsFeatures = {}),
'../User/UserGetter': (this.UserGetter = {}),
'../Analytics/AnalyticsManager': (this.AnalyticsManager = {
setUserProperty: sinon.stub(),
}),
'../../infrastructure/Modules': (this.Modules = {
hooks: { fire: sinon.stub() },
}),
},
})
})
describe('refreshFeatures', function () {
beforeEach(function () {
this.user = {
_id: this.user_id,
features: {},
}
this.UserFeaturesUpdater.updateFeatures = sinon
.stub()
.yields(null, { some: 'features' }, true)
this.FeaturesUpdater._getIndividualFeatures = sinon
.stub()
.yields(null, { individual: 'features' })
this.FeaturesUpdater._getGroupFeatureSets = sinon
.stub()
.yields(null, [{ group: 'features' }, { group: 'features2' }])
this.InstitutionsFeatures.getInstitutionsFeatures = sinon
.stub()
.yields(null, { institutions: 'features' })
this.FeaturesUpdater._getV1Features = sinon
.stub()
.yields(null, { v1: 'features' })
this.ReferalFeatures.getBonusFeatures = sinon
.stub()
.yields(null, { bonus: 'features' })
this.FeaturesUpdater._mergeFeatures = sinon
.stub()
.returns({ merged: 'features' })
this.UserGetter.getUser = sinon.stub().yields(null, this.user)
this.callback = sinon.stub()
})
it('should return features and featuresChanged', function () {
this.FeaturesUpdater.refreshFeatures(
this.user_id,
'test',
(err, features, featuresChanged) => {
expect(err).to.not.exist
expect(features).to.exist
expect(featuresChanged).to.exist
}
)
})
describe('normally', function () {
beforeEach(function () {
this.FeaturesUpdater.refreshFeatures(
this.user_id,
'test',
this.callback
)
})
it('should get the individual features', function () {
this.FeaturesUpdater._getIndividualFeatures
.calledWith(this.user_id)
.should.equal(true)
})
it('should get the group features', function () {
this.FeaturesUpdater._getGroupFeatureSets
.calledWith(this.user_id)
.should.equal(true)
})
it('should get the institution features', function () {
this.InstitutionsFeatures.getInstitutionsFeatures
.calledWith(this.user_id)
.should.equal(true)
})
it('should get the v1 features', function () {
this.FeaturesUpdater._getV1Features
.calledWith(this.user_id)
.should.equal(true)
})
it('should get the bonus features', function () {
this.ReferalFeatures.getBonusFeatures
.calledWith(this.user_id)
.should.equal(true)
})
it('should merge from the default features', function () {
this.FeaturesUpdater._mergeFeatures
.calledWith(this.Settings.defaultFeatures)
.should.equal(true)
})
it('should merge the individual features', function () {
this.FeaturesUpdater._mergeFeatures
.calledWith(sinon.match.any, { individual: 'features' })
.should.equal(true)
})
it('should merge the group features', function () {
this.FeaturesUpdater._mergeFeatures
.calledWith(sinon.match.any, { group: 'features' })
.should.equal(true)
this.FeaturesUpdater._mergeFeatures
.calledWith(sinon.match.any, { group: 'features2' })
.should.equal(true)
})
it('should merge the institutions features', function () {
this.FeaturesUpdater._mergeFeatures
.calledWith(sinon.match.any, { institutions: 'features' })
.should.equal(true)
})
it('should merge the v1 features', function () {
this.FeaturesUpdater._mergeFeatures
.calledWith(sinon.match.any, { v1: 'features' })
.should.equal(true)
})
it('should merge the bonus features', function () {
this.FeaturesUpdater._mergeFeatures
.calledWith(sinon.match.any, { bonus: 'features' })
.should.equal(true)
})
it('should update the user with the merged features', function () {
this.UserFeaturesUpdater.updateFeatures
.calledWith(this.user_id, { merged: 'features' })
.should.equal(true)
})
})
describe('analytics user properties', function () {
it('should send the corresponding feature set user property', function () {
this.FeaturesUpdater._mergeFeatures = sinon
.stub()
.returns(this.Settings.features.personal)
this.FeaturesUpdater.refreshFeatures(
this.user_id,
'test',
this.callback
)
sinon.assert.calledWith(
this.AnalyticsManager.setUserProperty,
this.user_id,
'feature-set',
'personal'
)
})
it('should send mixed feature set user property', function () {
this.FeaturesUpdater._mergeFeatures = sinon
.stub()
.returns({ dropbox: true, feature: 'some' })
this.FeaturesUpdater.refreshFeatures(
this.user_id,
'test',
this.callback
)
sinon.assert.calledWith(
this.AnalyticsManager.setUserProperty,
this.user_id,
'feature-set',
'mixed'
)
})
})
describe('when losing dropbox feature', function () {
beforeEach(function () {
this.user = {
_id: this.user_id,
features: { dropbox: true },
}
this.UserGetter.getUser = sinon.stub().yields(null, this.user)
this.FeaturesUpdater._mergeFeatures = sinon
.stub()
.returns({ dropbox: false })
this.FeaturesUpdater.refreshFeatures(
this.user_id,
'test',
this.callback
)
})
it('should fire module hook to unlink dropbox', function () {
this.Modules.hooks.fire
.calledWith('removeDropbox', this.user._id, 'test')
.should.equal(true)
})
})
})
describe('_mergeFeatures', function () {
it('should prefer priority over standard for compileGroup', function () {
expect(
this.FeaturesUpdater._mergeFeatures(
{
compileGroup: 'priority',
},
{
compileGroup: 'standard',
}
)
).to.deep.equal({
compileGroup: 'priority',
})
expect(
this.FeaturesUpdater._mergeFeatures(
{
compileGroup: 'standard',
},
{
compileGroup: 'priority',
}
)
).to.deep.equal({
compileGroup: 'priority',
})
expect(
this.FeaturesUpdater._mergeFeatures(
{
compileGroup: 'priority',
},
{
compileGroup: 'priority',
}
)
).to.deep.equal({
compileGroup: 'priority',
})
expect(
this.FeaturesUpdater._mergeFeatures(
{
compileGroup: 'standard',
},
{
compileGroup: 'standard',
}
)
).to.deep.equal({
compileGroup: 'standard',
})
})
it('should prefer -1 over any other for collaborators', function () {
expect(
this.FeaturesUpdater._mergeFeatures(
{
collaborators: -1,
},
{
collaborators: 10,
}
)
).to.deep.equal({
collaborators: -1,
})
expect(
this.FeaturesUpdater._mergeFeatures(
{
collaborators: 10,
},
{
collaborators: -1,
}
)
).to.deep.equal({
collaborators: -1,
})
expect(
this.FeaturesUpdater._mergeFeatures(
{
collaborators: 4,
},
{
collaborators: 10,
}
)
).to.deep.equal({
collaborators: 10,
})
})
it('should prefer the higher of compileTimeout', function () {
expect(
this.FeaturesUpdater._mergeFeatures(
{
compileTimeout: 20,
},
{
compileTimeout: 10,
}
)
).to.deep.equal({
compileTimeout: 20,
})
expect(
this.FeaturesUpdater._mergeFeatures(
{
compileTimeout: 10,
},
{
compileTimeout: 20,
}
)
).to.deep.equal({
compileTimeout: 20,
})
})
it('should prefer the true over false for other keys', function () {
expect(
this.FeaturesUpdater._mergeFeatures(
{
github: true,
},
{
github: false,
}
)
).to.deep.equal({
github: true,
})
expect(
this.FeaturesUpdater._mergeFeatures(
{
github: false,
},
{
github: true,
}
)
).to.deep.equal({
github: true,
})
expect(
this.FeaturesUpdater._mergeFeatures(
{
github: true,
},
{
github: true,
}
)
).to.deep.equal({
github: true,
})
expect(
this.FeaturesUpdater._mergeFeatures(
{
github: false,
},
{
github: false,
}
)
).to.deep.equal({
github: false,
})
})
})
describe('doSyncFromV1', function () {
beforeEach(function () {
this.v1UserId = 1
this.user = {
_id: this.user_id,
email: 'user@example.com',
overleaf: {
id: this.v1UserId,
},
}
this.UserGetter.getUser = sinon.stub().callsArgWith(2, null, this.user)
this.FeaturesUpdater.refreshFeatures = sinon.stub().yields(null)
this.call = cb => {
this.FeaturesUpdater.doSyncFromV1(this.v1UserId, cb)
}
})
describe('when all goes well', function () {
it('should call getUser', function (done) {
this.call(() => {
expect(this.UserGetter.getUser.callCount).to.equal(1)
expect(
this.UserGetter.getUser.calledWith({ 'overleaf.id': this.v1UserId })
).to.equal(true)
done()
})
})
it('should call refreshFeatures', function (done) {
this.call(() => {
expect(this.FeaturesUpdater.refreshFeatures.callCount).to.equal(1)
expect(
this.FeaturesUpdater.refreshFeatures.calledWith(this.user_id)
).to.equal(true)
done()
})
})
it('should not produce an error', function (done) {
this.call(err => {
expect(err).to.not.exist
done()
})
})
})
describe('when getUser produces an error', function () {
beforeEach(function () {
this.UserGetter.getUser = sinon
.stub()
.callsArgWith(2, new Error('woops'))
})
it('should not call refreshFeatures', function () {
expect(this.FeaturesUpdater.refreshFeatures.callCount).to.equal(0)
})
it('should produce an error', function (done) {
this.call(err => {
expect(err).to.exist
done()
})
})
})
describe('when getUser does not find a user', function () {
beforeEach(function () {
this.UserGetter.getUser = sinon.stub().callsArgWith(2, null, null)
})
it('should not call refreshFeatures', function (done) {
this.call(() => {
expect(this.FeaturesUpdater.refreshFeatures.callCount).to.equal(0)
done()
})
})
it('should not produce an error', function (done) {
this.call(err => {
expect(err).to.not.exist
done()
})
})
})
})
describe('isFeatureSetBetter', function () {
it('simple comparisons', function () {
const result1 = this.FeaturesUpdater.isFeatureSetBetter(
{
dropbox: true,
},
{
dropbox: false,
}
)
assert.isTrue(result1)
const result2 = this.FeaturesUpdater.isFeatureSetBetter(
{
dropbox: false,
},
{
dropbox: true,
}
)
assert.isFalse(result2)
})
it('compound comparisons with same features', function () {
const result1 = this.FeaturesUpdater.isFeatureSetBetter(
{
collaborators: 9,
dropbox: true,
},
{
collaborators: 10,
dropbox: true,
}
)
assert.isFalse(result1)
const result2 = this.FeaturesUpdater.isFeatureSetBetter(
{
collaborators: -1,
dropbox: true,
},
{
collaborators: 10,
dropbox: true,
}
)
assert.isTrue(result2)
const result3 = this.FeaturesUpdater.isFeatureSetBetter(
{
collaborators: -1,
compileTimeout: 60,
dropbox: true,
},
{
collaborators: 10,
compileTimeout: 60,
dropbox: true,
}
)
assert.isTrue(result3)
})
})
})