@frontegg/entitlements-javascript-commons 1.0.1 → 1.1.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/plans/index.d.ts +2 -0
- package/dist/plans/index.js +18 -0
- package/dist/plans/index.js.map +1 -0
- package/dist/plans/plan.evaluator.d.ts +2 -0
- package/dist/plans/plan.evaluator.js +21 -0
- package/dist/plans/plan.evaluator.js.map +1 -0
- package/dist/plans/types.d.ts +8 -0
- package/dist/plans/types.js +3 -0
- package/dist/plans/types.js.map +1 -0
- package/dist/user-entitlements/index.d.ts +3 -3
- package/dist/user-entitlements/index.js +3 -3
- package/dist/user-entitlements/index.js.map +1 -1
- package/dist/user-entitlements/is-entitled-to-feature/evaluators/direct-entitlement.evaluator.d.ts +2 -0
- package/dist/user-entitlements/is-entitled-to-feature/evaluators/direct-entitlement.evaluator.js +20 -0
- package/dist/user-entitlements/is-entitled-to-feature/evaluators/direct-entitlement.evaluator.js.map +1 -0
- package/dist/user-entitlements/is-entitled-to-feature/evaluators/feature-flag.evaluator.d.ts +2 -0
- package/dist/user-entitlements/is-entitled-to-feature/evaluators/feature-flag.evaluator.js +23 -0
- package/dist/user-entitlements/is-entitled-to-feature/evaluators/feature-flag.evaluator.js.map +1 -0
- package/dist/user-entitlements/is-entitled-to-feature/evaluators/index.d.ts +2 -0
- package/dist/user-entitlements/is-entitled-to-feature/evaluators/index.js +11 -0
- package/dist/user-entitlements/is-entitled-to-feature/evaluators/index.js.map +1 -0
- package/dist/user-entitlements/is-entitled-to-feature/evaluators/plan-targeting-rules.evaluator.d.ts +2 -0
- package/dist/user-entitlements/is-entitled-to-feature/evaluators/plan-targeting-rules.evaluator.js +29 -0
- package/dist/user-entitlements/is-entitled-to-feature/evaluators/plan-targeting-rules.evaluator.js.map +1 -0
- package/dist/user-entitlements/is-entitled-to-feature/evaluators/types.d.ts +2 -0
- package/dist/user-entitlements/is-entitled-to-feature/evaluators/types.js +3 -0
- package/dist/user-entitlements/is-entitled-to-feature/evaluators/types.js.map +1 -0
- package/dist/user-entitlements/is-entitled-to-feature/index.d.ts +1 -0
- package/dist/user-entitlements/is-entitled-to-feature/index.js +18 -0
- package/dist/user-entitlements/is-entitled-to-feature/index.js.map +1 -0
- package/dist/user-entitlements/{is-entitled.evaluator.d.ts → is-entitled-to-feature/is-entitled-to-feature.evaluator.d.ts} +1 -2
- package/dist/user-entitlements/is-entitled-to-feature/is-entitled-to-feature.evaluator.js +17 -0
- package/dist/user-entitlements/is-entitled-to-feature/is-entitled-to-feature.evaluator.js.map +1 -0
- package/dist/user-entitlements/is-entitled-to-permission/index.d.ts +1 -0
- package/dist/user-entitlements/is-entitled-to-permission/index.js +18 -0
- package/dist/user-entitlements/is-entitled-to-permission/index.js.map +1 -0
- package/dist/user-entitlements/is-entitled-to-permission/is-entitled-to-permission.evaluator.d.ts +2 -0
- package/dist/user-entitlements/is-entitled-to-permission/is-entitled-to-permission.evaluator.js +30 -0
- package/dist/user-entitlements/is-entitled-to-permission/is-entitled-to-permission.evaluator.js.map +1 -0
- package/dist/user-entitlements/types.d.ts +7 -2
- package/dist/user-entitlements/types.js.map +1 -1
- package/dist/user-entitlements/{attributes.utils.d.ts → utils/attributes.utils.d.ts} +1 -1
- package/dist/user-entitlements/utils/attributes.utils.js.map +1 -0
- package/dist/user-entitlements/utils/entitlement-results.utils.d.ts +3 -0
- package/dist/user-entitlements/utils/entitlement-results.utils.js +25 -0
- package/dist/user-entitlements/utils/entitlement-results.utils.js.map +1 -0
- package/dist/user-entitlements/utils/flatten.utils.js.map +1 -0
- package/dist/user-entitlements/utils/index.d.ts +3 -0
- package/dist/user-entitlements/utils/index.js +20 -0
- package/dist/user-entitlements/utils/index.js.map +1 -0
- package/dist/user-entitlements/{permissions.utils.d.ts → utils/permissions.utils.d.ts} +1 -1
- package/dist/user-entitlements/utils/permissions.utils.js.map +1 -0
- package/docs/CHANGELOG.md +14 -0
- package/package.json +1 -1
- package/src/plans/index.ts +2 -0
- package/src/plans/plan.evaluator.ts +20 -0
- package/src/plans/tests/plans.evaluator.spec.ts +121 -0
- package/src/plans/types.ts +11 -0
- package/src/user-entitlements/index.ts +3 -3
- package/src/user-entitlements/is-entitled-to-feature/evaluators/direct-entitlement.evaluator.ts +28 -0
- package/src/user-entitlements/is-entitled-to-feature/evaluators/feature-flag.evaluator.ts +24 -0
- package/src/user-entitlements/is-entitled-to-feature/evaluators/index.ts +8 -0
- package/src/user-entitlements/is-entitled-to-feature/evaluators/plan-targeting-rules.evaluator.ts +30 -0
- package/src/user-entitlements/is-entitled-to-feature/evaluators/tests/direct-entitlements.evaluator.spec.ts +81 -0
- package/src/user-entitlements/is-entitled-to-feature/evaluators/tests/feature-flag.evaluator.spec.ts +57 -0
- package/src/user-entitlements/is-entitled-to-feature/evaluators/tests/plan-targeting-rules.evaluator.spec.ts +70 -0
- package/src/user-entitlements/is-entitled-to-feature/evaluators/types.ts +7 -0
- package/src/user-entitlements/is-entitled-to-feature/index.ts +1 -0
- package/src/user-entitlements/is-entitled-to-feature/is-entitled-to-feature.evaluator.ts +18 -0
- package/src/user-entitlements/is-entitled-to-feature/tests/is-entitled-to-feature.evaluator.spec.ts +68 -0
- package/src/user-entitlements/is-entitled-to-permission/index.ts +1 -0
- package/src/user-entitlements/is-entitled-to-permission/is-entitled-to-permission.evaluator.ts +38 -0
- package/src/user-entitlements/is-entitled-to-permission/tests/is-entitled-to-permission.evaluator.spec.ts +79 -0
- package/src/user-entitlements/types.ts +11 -4
- package/src/user-entitlements/{attributes.utils.ts → utils/attributes.utils.ts} +1 -1
- package/src/user-entitlements/utils/entitlement-results.utils.ts +22 -0
- package/src/user-entitlements/utils/index.ts +3 -0
- package/src/user-entitlements/{permissions.utils.ts → utils/permissions.utils.ts} +1 -1
- package/src/user-entitlements/{tests → utils/tests}/attributes.utils.spec.ts +1 -1
- package/src/user-entitlements/utils/tests/entitlement-results.utils.spec.ts +46 -0
- package/src/user-entitlements/{tests → utils/tests}/permissions.utils.spec.ts +1 -1
- package/dist/user-entitlements/attributes.utils.js.map +0 -1
- package/dist/user-entitlements/flatten.utils.js.map +0 -1
- package/dist/user-entitlements/is-entitled.evaluator.js +0 -59
- package/dist/user-entitlements/is-entitled.evaluator.js.map +0 -1
- package/dist/user-entitlements/permissions.utils.js.map +0 -1
- package/src/user-entitlements/is-entitled.evaluator.ts +0 -82
- package/src/user-entitlements/tests/is-entitled.evaluator.spec.ts +0 -298
- /package/dist/user-entitlements/{attributes.utils.js → utils/attributes.utils.js} +0 -0
- /package/dist/user-entitlements/{flatten.utils.d.ts → utils/flatten.utils.d.ts} +0 -0
- /package/dist/user-entitlements/{flatten.utils.js → utils/flatten.utils.js} +0 -0
- /package/dist/user-entitlements/{permissions.utils.js → utils/permissions.utils.js} +0 -0
- /package/src/user-entitlements/{flatten.utils.ts → utils/flatten.utils.ts} +0 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { ConditionLogicEnum, TreatmentEnum } from '../../rules';
|
|
2
|
+
import { OperationEnum } from '../../operations/types';
|
|
3
|
+
import { Plan } from '../types';
|
|
4
|
+
import { evaluatePlan } from '../plan.evaluator';
|
|
5
|
+
|
|
6
|
+
type PlanCreator = (data?: Partial<Plan>) => Plan;
|
|
7
|
+
const planFactory: PlanCreator = (data?: Partial<Plan>) => ({
|
|
8
|
+
defaultTreatment: TreatmentEnum.False,
|
|
9
|
+
...data,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe('evaluatePlan', () => {
|
|
13
|
+
describe('default treatment', () => {
|
|
14
|
+
it('should return false when no rule is valid and defaultTreatment is false', () => {
|
|
15
|
+
const plan: Plan = planFactory({
|
|
16
|
+
rules: [
|
|
17
|
+
{
|
|
18
|
+
treatment: TreatmentEnum.True,
|
|
19
|
+
conditions: [
|
|
20
|
+
{
|
|
21
|
+
attribute: 'test',
|
|
22
|
+
op: 'not supported' as any,
|
|
23
|
+
value: { list: ['test'] },
|
|
24
|
+
negate: false,
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
conditionLogic: ConditionLogicEnum.And,
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const result = evaluatePlan(plan, {});
|
|
33
|
+
|
|
34
|
+
expect(result).toEqual({ treatment: TreatmentEnum.False });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should return true when no rule is valid and defaultTreatment is true', () => {
|
|
38
|
+
const plan: Plan = planFactory({
|
|
39
|
+
defaultTreatment: TreatmentEnum.True,
|
|
40
|
+
rules: [
|
|
41
|
+
{
|
|
42
|
+
treatment: TreatmentEnum.True,
|
|
43
|
+
conditions: [
|
|
44
|
+
{
|
|
45
|
+
attribute: 'test',
|
|
46
|
+
op: 'not supported' as any,
|
|
47
|
+
value: { list: ['test'] },
|
|
48
|
+
negate: false,
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
conditionLogic: ConditionLogicEnum.And,
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const result = evaluatePlan(plan, {});
|
|
57
|
+
|
|
58
|
+
expect(result).toEqual({ treatment: TreatmentEnum.True });
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('complete evaluation', () => {
|
|
63
|
+
it('should return false according to first treatable rule', () => {
|
|
64
|
+
const plan: Plan = planFactory({
|
|
65
|
+
rules: [
|
|
66
|
+
{
|
|
67
|
+
treatment: TreatmentEnum.False,
|
|
68
|
+
conditions: [
|
|
69
|
+
{
|
|
70
|
+
attribute: 'attribute',
|
|
71
|
+
op: OperationEnum.InList,
|
|
72
|
+
value: { list: ['test'] },
|
|
73
|
+
negate: false,
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
conditionLogic: ConditionLogicEnum.And,
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const result = evaluatePlan(plan, { attribute: 'test' });
|
|
82
|
+
|
|
83
|
+
expect(result).toEqual({ treatment: TreatmentEnum.False });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should return true according to first treatable rule', () => {
|
|
87
|
+
const plan: Plan = planFactory({
|
|
88
|
+
rules: [
|
|
89
|
+
{
|
|
90
|
+
treatment: TreatmentEnum.False,
|
|
91
|
+
conditions: [
|
|
92
|
+
{
|
|
93
|
+
attribute: 'test',
|
|
94
|
+
op: 'not supported' as any,
|
|
95
|
+
value: { list: ['test'] },
|
|
96
|
+
negate: false,
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
conditionLogic: ConditionLogicEnum.And,
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
treatment: TreatmentEnum.True,
|
|
103
|
+
conditions: [
|
|
104
|
+
{
|
|
105
|
+
attribute: 'attribute',
|
|
106
|
+
op: OperationEnum.InList,
|
|
107
|
+
value: { list: ['test'] },
|
|
108
|
+
negate: false,
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
conditionLogic: ConditionLogicEnum.And,
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const result = evaluatePlan(plan, { attribute: 'test' });
|
|
117
|
+
|
|
118
|
+
expect(result).toEqual({ treatment: TreatmentEnum.True });
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
});
|
package/src/user-entitlements/is-entitled-to-feature/evaluators/direct-entitlement.evaluator.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Attributes,
|
|
3
|
+
EntitlementResult,
|
|
4
|
+
NO_EXPIRATION_TIME,
|
|
5
|
+
NotEntitledJustification,
|
|
6
|
+
UserEntitlementsContext,
|
|
7
|
+
} from '../../types';
|
|
8
|
+
|
|
9
|
+
export function directEntitlementEvalutor(
|
|
10
|
+
featureKey: string,
|
|
11
|
+
userEntitlementsContext: UserEntitlementsContext,
|
|
12
|
+
attributes: Attributes = {},
|
|
13
|
+
): EntitlementResult {
|
|
14
|
+
const feature = userEntitlementsContext.features[featureKey];
|
|
15
|
+
let hasExpired = false;
|
|
16
|
+
if (feature && feature.expireTime !== null) {
|
|
17
|
+
hasExpired = feature.expireTime !== NO_EXPIRATION_TIME && feature.expireTime < Date.now();
|
|
18
|
+
|
|
19
|
+
if (!hasExpired) {
|
|
20
|
+
return { isEntitled: true };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
isEntitled: false,
|
|
26
|
+
justification: hasExpired ? NotEntitledJustification.BUNDLE_EXPIRED : NotEntitledJustification.MISSING_FEATURE,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Attributes, EntitlementResult, NotEntitledJustification, UserEntitlementsContext } from '../../types';
|
|
2
|
+
import { evaluateFeatureFlag } from '../../../feature-flags';
|
|
3
|
+
import { prepareAttributes } from '../../utils/attributes.utils';
|
|
4
|
+
import { TreatmentEnum } from '../../../rules';
|
|
5
|
+
|
|
6
|
+
export function featureFlagEvaluator(
|
|
7
|
+
featureKey: string,
|
|
8
|
+
userEntitlementsContext: UserEntitlementsContext,
|
|
9
|
+
attributes: Attributes = {},
|
|
10
|
+
): EntitlementResult {
|
|
11
|
+
const feature = userEntitlementsContext.features[featureKey];
|
|
12
|
+
if (feature && feature.featureFlag) {
|
|
13
|
+
const preparedAttributes = prepareAttributes(attributes);
|
|
14
|
+
const { treatment } = evaluateFeatureFlag(feature.featureFlag, preparedAttributes);
|
|
15
|
+
if (treatment === TreatmentEnum.True) {
|
|
16
|
+
return { isEntitled: true };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
isEntitled: false,
|
|
22
|
+
justification: NotEntitledJustification.MISSING_FEATURE,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { directEntitlementEvalutor } from './direct-entitlement.evaluator';
|
|
2
|
+
import { featureFlagEvaluator } from './feature-flag.evaluator';
|
|
3
|
+
import { planTargetingRulesEvalutor } from './plan-targeting-rules.evaluator';
|
|
4
|
+
import { IsEntitledEvaluator } from './types';
|
|
5
|
+
|
|
6
|
+
export function getIsEntitledEvaluators(): IsEntitledEvaluator[] {
|
|
7
|
+
return [directEntitlementEvalutor, featureFlagEvaluator, planTargetingRulesEvalutor];
|
|
8
|
+
}
|
package/src/user-entitlements/is-entitled-to-feature/evaluators/plan-targeting-rules.evaluator.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Attributes, EntitlementResult, NotEntitledJustification, UserEntitlementsContext } from '../../types';
|
|
2
|
+
import { prepareAttributes } from '../../utils/attributes.utils';
|
|
3
|
+
import { TreatmentEnum } from '../../../rules';
|
|
4
|
+
import { evaluatePlan } from '../../../plans';
|
|
5
|
+
|
|
6
|
+
export function planTargetingRulesEvalutor(
|
|
7
|
+
featureKey: string,
|
|
8
|
+
userEntitlementsContext: UserEntitlementsContext,
|
|
9
|
+
attributes: Attributes = {},
|
|
10
|
+
): EntitlementResult {
|
|
11
|
+
const feature = userEntitlementsContext.features[featureKey];
|
|
12
|
+
if (feature && feature.planIds && feature.planIds.length > 0) {
|
|
13
|
+
const preparedAttributes = prepareAttributes(attributes);
|
|
14
|
+
const plans = userEntitlementsContext.plans;
|
|
15
|
+
for (const planId of feature.planIds) {
|
|
16
|
+
const plan = plans[planId];
|
|
17
|
+
if (plan) {
|
|
18
|
+
const { treatment } = evaluatePlan(plan, preparedAttributes);
|
|
19
|
+
if (treatment === TreatmentEnum.True) {
|
|
20
|
+
return { isEntitled: true };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
isEntitled: false,
|
|
28
|
+
justification: NotEntitledJustification.MISSING_FEATURE,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { NO_EXPIRATION_TIME, NotEntitledJustification } from '../../../types';
|
|
2
|
+
import { directEntitlementEvalutor } from '../direct-entitlement.evaluator';
|
|
3
|
+
|
|
4
|
+
describe('directEntitlementEvalutor', () => {
|
|
5
|
+
describe('entitled', () => {
|
|
6
|
+
test('feature is entitled with no expiration date', async () => {
|
|
7
|
+
const result = directEntitlementEvalutor('feature-key', {
|
|
8
|
+
features: {
|
|
9
|
+
'feature-key': {
|
|
10
|
+
expireTime: NO_EXPIRATION_TIME,
|
|
11
|
+
linkedPermissions: [],
|
|
12
|
+
planIds: [],
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
permissions: {},
|
|
16
|
+
plans: {},
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
expect(result).toEqual({ isEntitled: true });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('feature is entitled with a valid expiration date', async () => {
|
|
23
|
+
const result = directEntitlementEvalutor('feature-key', {
|
|
24
|
+
features: {
|
|
25
|
+
'feature-key': {
|
|
26
|
+
expireTime: Date.now() + 3600,
|
|
27
|
+
linkedPermissions: [],
|
|
28
|
+
planIds: [],
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
permissions: {},
|
|
32
|
+
plans: {},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
expect(result).toEqual({ isEntitled: true });
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
describe('not-entitled', () => {
|
|
39
|
+
test('feature is not entitled', async () => {
|
|
40
|
+
const result = directEntitlementEvalutor('feature-key', {
|
|
41
|
+
features: {
|
|
42
|
+
'feature-key': {
|
|
43
|
+
expireTime: null,
|
|
44
|
+
linkedPermissions: [],
|
|
45
|
+
planIds: [],
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
permissions: {},
|
|
49
|
+
plans: {},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(result).toEqual({ isEntitled: false, justification: NotEntitledJustification.MISSING_FEATURE });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('feature is entitled with an expired expiration date', async () => {
|
|
56
|
+
const result = directEntitlementEvalutor('feature-key', {
|
|
57
|
+
features: {
|
|
58
|
+
'feature-key': {
|
|
59
|
+
expireTime: Date.now() - 3600,
|
|
60
|
+
linkedPermissions: [],
|
|
61
|
+
planIds: [],
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
permissions: {},
|
|
65
|
+
plans: {},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(result).toEqual({ isEntitled: false, justification: NotEntitledJustification.BUNDLE_EXPIRED });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('feature does not exist', async () => {
|
|
72
|
+
const result = directEntitlementEvalutor('feature-key', {
|
|
73
|
+
features: {},
|
|
74
|
+
permissions: {},
|
|
75
|
+
plans: {},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(result).toEqual({ isEntitled: false, justification: NotEntitledJustification.MISSING_FEATURE });
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
package/src/user-entitlements/is-entitled-to-feature/evaluators/tests/feature-flag.evaluator.spec.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import * as AttributesUtils from '../../../utils/attributes.utils';
|
|
2
|
+
import * as FeatureFlagEvaluator from '../../../../feature-flags/feature-flag.evaluator';
|
|
3
|
+
import { TreatmentEnum } from '../../../../rules';
|
|
4
|
+
import { featureFlagEvaluator } from '../feature-flag.evaluator';
|
|
5
|
+
import { NotEntitledJustification } from '../../../types';
|
|
6
|
+
describe('featureFlagEvaluator', () => {
|
|
7
|
+
beforeAll(async () => {
|
|
8
|
+
jest.spyOn(AttributesUtils, 'prepareAttributes').mockReturnValue({ testAttribute: 'test-value' });
|
|
9
|
+
});
|
|
10
|
+
describe('entitled', () => {
|
|
11
|
+
test('feature flag evaluation return truthy', async () => {
|
|
12
|
+
jest.spyOn(FeatureFlagEvaluator, 'evaluateFeatureFlag').mockReturnValue({ treatment: TreatmentEnum.True });
|
|
13
|
+
const result = featureFlagEvaluator('feature-key', {
|
|
14
|
+
features: {
|
|
15
|
+
'feature-key': {
|
|
16
|
+
expireTime: null,
|
|
17
|
+
linkedPermissions: [],
|
|
18
|
+
planIds: [],
|
|
19
|
+
featureFlag: { defaultTreatment: TreatmentEnum.True, on: true, offTreatment: TreatmentEnum.False },
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
permissions: {},
|
|
23
|
+
plans: {},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
expect(result).toEqual({ isEntitled: true });
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
describe('not-entitled', () => {
|
|
30
|
+
test('feature flag evaluation return falsy', async () => {
|
|
31
|
+
jest.spyOn(FeatureFlagEvaluator, 'evaluateFeatureFlag').mockReturnValue({ treatment: TreatmentEnum.False });
|
|
32
|
+
const result = featureFlagEvaluator('feature-key', {
|
|
33
|
+
features: {
|
|
34
|
+
'feature-key': {
|
|
35
|
+
expireTime: null,
|
|
36
|
+
linkedPermissions: [],
|
|
37
|
+
planIds: [],
|
|
38
|
+
featureFlag: { defaultTreatment: TreatmentEnum.False, on: true, offTreatment: TreatmentEnum.False },
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
permissions: {},
|
|
42
|
+
plans: {},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
expect(result).toEqual({ isEntitled: false, justification: NotEntitledJustification.MISSING_FEATURE });
|
|
46
|
+
});
|
|
47
|
+
test('feataure does not exist', async () => {
|
|
48
|
+
const result = featureFlagEvaluator('feature-key', {
|
|
49
|
+
features: {},
|
|
50
|
+
permissions: {},
|
|
51
|
+
plans: {},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
expect(result).toEqual({ isEntitled: false, justification: NotEntitledJustification.MISSING_FEATURE });
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import * as PlanEvaluator from '../../../../plans/plan.evaluator';
|
|
2
|
+
import { TreatmentEnum } from '../../../../rules';
|
|
3
|
+
import { NotEntitledJustification } from '../../../types';
|
|
4
|
+
import { planTargetingRulesEvalutor } from '../plan-targeting-rules.evaluator';
|
|
5
|
+
describe('planTargetingRulesEvalutor', () => {
|
|
6
|
+
describe('entitled', () => {
|
|
7
|
+
test('some of the plans targeting rules return truthy', async () => {
|
|
8
|
+
jest.spyOn(PlanEvaluator, 'evaluatePlan').mockReturnValueOnce({ treatment: TreatmentEnum.True });
|
|
9
|
+
jest.spyOn(PlanEvaluator, 'evaluatePlan').mockReturnValueOnce({ treatment: TreatmentEnum.False });
|
|
10
|
+
|
|
11
|
+
const result = planTargetingRulesEvalutor('feature-key', {
|
|
12
|
+
features: {
|
|
13
|
+
'feature-key': {
|
|
14
|
+
expireTime: null,
|
|
15
|
+
linkedPermissions: [],
|
|
16
|
+
planIds: ['plan-1', 'plan-2'],
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
permissions: {},
|
|
20
|
+
plans: {
|
|
21
|
+
'plan-1': {
|
|
22
|
+
defaultTreatment: TreatmentEnum.True,
|
|
23
|
+
},
|
|
24
|
+
'plan-2': {
|
|
25
|
+
defaultTreatment: TreatmentEnum.False,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
expect(result).toEqual({ isEntitled: true });
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
describe('not-entitled', () => {
|
|
34
|
+
test('all of the plans targeting rules return falsy', async () => {
|
|
35
|
+
jest.spyOn(PlanEvaluator, 'evaluatePlan').mockReturnValueOnce({ treatment: TreatmentEnum.False });
|
|
36
|
+
jest.spyOn(PlanEvaluator, 'evaluatePlan').mockReturnValueOnce({ treatment: TreatmentEnum.False });
|
|
37
|
+
|
|
38
|
+
const result = planTargetingRulesEvalutor('feature-key', {
|
|
39
|
+
features: {
|
|
40
|
+
'feature-key': {
|
|
41
|
+
expireTime: null,
|
|
42
|
+
linkedPermissions: [],
|
|
43
|
+
planIds: ['plan-1', 'plan-2'],
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
permissions: {},
|
|
47
|
+
plans: {
|
|
48
|
+
'plan-1': {
|
|
49
|
+
defaultTreatment: TreatmentEnum.False,
|
|
50
|
+
},
|
|
51
|
+
'plan-2': {
|
|
52
|
+
defaultTreatment: TreatmentEnum.False,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
expect(result).toEqual({ isEntitled: false, justification: NotEntitledJustification.MISSING_FEATURE });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('feature does not exist', async () => {
|
|
61
|
+
const result = planTargetingRulesEvalutor('feature-key', {
|
|
62
|
+
features: {},
|
|
63
|
+
permissions: {},
|
|
64
|
+
plans: {},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(result).toEqual({ isEntitled: false, justification: NotEntitledJustification.MISSING_FEATURE });
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './is-entitled-to-feature.evaluator';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { EntitlementResult, NotEntitledJustification, UserEntitlementsContext, Attributes } from '../types';
|
|
2
|
+
import { getResult, shouldContinue } from '../utils';
|
|
3
|
+
import { getIsEntitledEvaluators } from './evaluators';
|
|
4
|
+
export function evaluateIsEntitledToFeature(
|
|
5
|
+
featureKey: string,
|
|
6
|
+
userEntitlementsContext: UserEntitlementsContext,
|
|
7
|
+
attributes: Attributes = {},
|
|
8
|
+
): EntitlementResult {
|
|
9
|
+
const entitlementResults: EntitlementResult[] = [];
|
|
10
|
+
for (const evaluator of getIsEntitledEvaluators()) {
|
|
11
|
+
entitlementResults.push(evaluator(featureKey, userEntitlementsContext, attributes));
|
|
12
|
+
if (!shouldContinue(entitlementResults)) {
|
|
13
|
+
break;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return getResult(entitlementResults);
|
|
18
|
+
}
|
package/src/user-entitlements/is-entitled-to-feature/tests/is-entitled-to-feature.evaluator.spec.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import * as IsEntitledEvaluators from '../evaluators';
|
|
2
|
+
import { evaluateIsEntitledToFeature } from '../is-entitled-to-feature.evaluator';
|
|
3
|
+
import { EntitlementResult, NotEntitledJustification, NO_EXPIRATION_TIME, UserEntitlementsContext } from '../../types';
|
|
4
|
+
|
|
5
|
+
describe('evaluateIsEntitledToFeature', () => {
|
|
6
|
+
const mockTruthyEvaluator = (): EntitlementResult => ({ isEntitled: true });
|
|
7
|
+
const mockFalsyMissingFeatureEvaluator = (): EntitlementResult => ({
|
|
8
|
+
isEntitled: false,
|
|
9
|
+
justification: NotEntitledJustification.MISSING_PERMISSION,
|
|
10
|
+
});
|
|
11
|
+
const mockFalsyExpiredEvaluator = (): EntitlementResult => ({
|
|
12
|
+
isEntitled: false,
|
|
13
|
+
justification: NotEntitledJustification.BUNDLE_EXPIRED,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('entitled', () => {
|
|
17
|
+
test('given all evaluators return truthy, feature is entitled', async () => {
|
|
18
|
+
jest
|
|
19
|
+
.spyOn(IsEntitledEvaluators, 'getIsEntitledEvaluators')
|
|
20
|
+
.mockReturnValue([mockTruthyEvaluator, mockTruthyEvaluator]);
|
|
21
|
+
const result = evaluateIsEntitledToFeature('feauture-key', {
|
|
22
|
+
features: {},
|
|
23
|
+
permissions: {},
|
|
24
|
+
plans: {},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
expect(result).toEqual({ isEntitled: true });
|
|
28
|
+
});
|
|
29
|
+
test('given some evaluators return truthy, feature is entitled', async () => {
|
|
30
|
+
jest
|
|
31
|
+
.spyOn(IsEntitledEvaluators, 'getIsEntitledEvaluators')
|
|
32
|
+
.mockReturnValue([mockTruthyEvaluator, mockFalsyMissingFeatureEvaluator]);
|
|
33
|
+
const result = evaluateIsEntitledToFeature('feauture-key', {
|
|
34
|
+
features: {},
|
|
35
|
+
permissions: {},
|
|
36
|
+
plans: {},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(result).toEqual({ isEntitled: true });
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
describe('not-entitled', () => {
|
|
43
|
+
test('given all evaluators return falsy with missing feature, feature is not entitled, justified with missing feature', async () => {
|
|
44
|
+
jest
|
|
45
|
+
.spyOn(IsEntitledEvaluators, 'getIsEntitledEvaluators')
|
|
46
|
+
.mockReturnValue([mockFalsyMissingFeatureEvaluator, mockFalsyMissingFeatureEvaluator]);
|
|
47
|
+
const result = evaluateIsEntitledToFeature('feauture-key', {
|
|
48
|
+
features: {},
|
|
49
|
+
permissions: {},
|
|
50
|
+
plans: {},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(result).toEqual({ isEntitled: false, justification: NotEntitledJustification.MISSING_FEATURE });
|
|
54
|
+
});
|
|
55
|
+
test('given all evaluators return falsy, some with missing feature, some with bundle expired, feature is not entitled, justified with bundle expired', async () => {
|
|
56
|
+
jest
|
|
57
|
+
.spyOn(IsEntitledEvaluators, 'getIsEntitledEvaluators')
|
|
58
|
+
.mockReturnValue([mockFalsyMissingFeatureEvaluator, mockFalsyExpiredEvaluator]);
|
|
59
|
+
const result = evaluateIsEntitledToFeature('feauture-key', {
|
|
60
|
+
features: {},
|
|
61
|
+
permissions: {},
|
|
62
|
+
plans: {},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(result).toEqual({ isEntitled: false, justification: NotEntitledJustification.BUNDLE_EXPIRED });
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './is-entitled-to-permission.evaluator';
|
package/src/user-entitlements/is-entitled-to-permission/is-entitled-to-permission.evaluator.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { evaluateIsEntitledToFeature } from '../is-entitled-to-feature/is-entitled-to-feature.evaluator';
|
|
2
|
+
import { UserEntitlementsContext, Attributes, EntitlementResult, NotEntitledJustification } from '../types';
|
|
3
|
+
import { getResult, shouldContinue } from '../utils';
|
|
4
|
+
import { checkPermission } from '../utils/permissions.utils';
|
|
5
|
+
|
|
6
|
+
export function evaluateIsEntitledToPermissions(
|
|
7
|
+
permissionKey: string,
|
|
8
|
+
userEntitlementsContext: UserEntitlementsContext,
|
|
9
|
+
attributes?: Attributes,
|
|
10
|
+
): EntitlementResult {
|
|
11
|
+
const hasPermission = checkPermission(userEntitlementsContext.permissions, permissionKey);
|
|
12
|
+
if (!hasPermission) {
|
|
13
|
+
return { isEntitled: false, justification: NotEntitledJustification.MISSING_PERMISSION };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const linkedFeatures = getLinkedFeatures(permissionKey, userEntitlementsContext);
|
|
17
|
+
|
|
18
|
+
if (!linkedFeatures.length) {
|
|
19
|
+
return { isEntitled: true };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const entitlementResults: EntitlementResult[] = [];
|
|
23
|
+
for (const featureKey of linkedFeatures) {
|
|
24
|
+
entitlementResults.push(evaluateIsEntitledToFeature(featureKey, userEntitlementsContext, attributes));
|
|
25
|
+
|
|
26
|
+
if (!shouldContinue(entitlementResults)) {
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return getResult(entitlementResults);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getLinkedFeatures(permissionKey: string, userEntitlementsContext: UserEntitlementsContext): string[] {
|
|
35
|
+
return Object.keys(userEntitlementsContext.features).filter((featureKey) =>
|
|
36
|
+
userEntitlementsContext.features[featureKey].linkedPermissions.includes(permissionKey),
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { evaluateIsEntitledToPermissions } from '../is-entitled-to-permission.evaluator';
|
|
2
|
+
import { NO_EXPIRATION_TIME, NotEntitledJustification, UserEntitlementsContext } from '../../types';
|
|
3
|
+
import * as IsEntitledToFeatureEvaluator from '../../is-entitled-to-feature/is-entitled-to-feature.evaluator';
|
|
4
|
+
describe('evaluateIsEntitledToPermission', () => {
|
|
5
|
+
describe('entitled', () => {
|
|
6
|
+
test('permission granted, no linked feature/s to it', async () => {
|
|
7
|
+
const userEntitlementContext: UserEntitlementsContext = {
|
|
8
|
+
features: {},
|
|
9
|
+
plans: {},
|
|
10
|
+
|
|
11
|
+
permissions: { 'test.permission': true },
|
|
12
|
+
};
|
|
13
|
+
const result = evaluateIsEntitledToPermissions('test.permission', userEntitlementContext, {});
|
|
14
|
+
|
|
15
|
+
expect(result).toEqual({ isEntitled: true });
|
|
16
|
+
});
|
|
17
|
+
test('permission granted with linked feature/s, feature is entitled', async () => {
|
|
18
|
+
jest.spyOn(IsEntitledToFeatureEvaluator, 'evaluateIsEntitledToFeature').mockReturnValue({ isEntitled: true });
|
|
19
|
+
|
|
20
|
+
const userEntitlementContext: UserEntitlementsContext = {
|
|
21
|
+
features: {
|
|
22
|
+
'test-feature': { planIds: [], expireTime: NO_EXPIRATION_TIME, linkedPermissions: ['test.permission'] },
|
|
23
|
+
},
|
|
24
|
+
plans: {},
|
|
25
|
+
|
|
26
|
+
permissions: { 'test.permission': true },
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const result = evaluateIsEntitledToPermissions('test.permission', userEntitlementContext, {});
|
|
30
|
+
|
|
31
|
+
expect(result).toEqual({ isEntitled: true });
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('not entitled', () => {
|
|
36
|
+
test('permission not granted', async () => {
|
|
37
|
+
const userEntitlementContext: UserEntitlementsContext = {
|
|
38
|
+
features: {},
|
|
39
|
+
plans: {},
|
|
40
|
+
permissions: {},
|
|
41
|
+
};
|
|
42
|
+
const result = evaluateIsEntitledToPermissions('test.permission', userEntitlementContext, {});
|
|
43
|
+
|
|
44
|
+
expect(result).toEqual({ isEntitled: false, justification: NotEntitledJustification.MISSING_PERMISSION });
|
|
45
|
+
});
|
|
46
|
+
test('permission granted with linked feature/s, no feature is entiteld', async () => {
|
|
47
|
+
jest
|
|
48
|
+
.spyOn(IsEntitledToFeatureEvaluator, 'evaluateIsEntitledToFeature')
|
|
49
|
+
.mockReturnValue({ isEntitled: false, justification: NotEntitledJustification.MISSING_FEATURE });
|
|
50
|
+
|
|
51
|
+
const userEntitlementContext: UserEntitlementsContext = {
|
|
52
|
+
features: { 'test-feature': { planIds: [], expireTime: null, linkedPermissions: ['test.permission'] } },
|
|
53
|
+
plans: {},
|
|
54
|
+
permissions: { 'test.permission': true },
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const result = evaluateIsEntitledToPermissions('test.permission', userEntitlementContext, {});
|
|
58
|
+
|
|
59
|
+
expect(result).toEqual({ isEntitled: false, justification: NotEntitledJustification.MISSING_FEATURE });
|
|
60
|
+
});
|
|
61
|
+
test('permission granted with linked feature/s, no feature is entiteld with expired bundle', async () => {
|
|
62
|
+
jest
|
|
63
|
+
.spyOn(IsEntitledToFeatureEvaluator, 'evaluateIsEntitledToFeature')
|
|
64
|
+
.mockReturnValue({ isEntitled: false, justification: NotEntitledJustification.BUNDLE_EXPIRED });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const userEntitlementContext: UserEntitlementsContext = {
|
|
68
|
+
features: {
|
|
69
|
+
'test-feature': { planIds: [], expireTime: Date.now() - 3600, linkedPermissions: ['test.permission'] },
|
|
70
|
+
},
|
|
71
|
+
plans: {},
|
|
72
|
+
permissions: { 'test.permission': true },
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const result = evaluateIsEntitledToPermissions('test.permission', userEntitlementContext, {});
|
|
76
|
+
|
|
77
|
+
expect(result).toEqual({ isEntitled: false, justification: NotEntitledJustification.BUNDLE_EXPIRED });
|
|
78
|
+
});
|
|
79
|
+
});
|