@frontegg/entitlements-javascript-commons 1.0.0-alpha.1 → 1.0.0-alpha.11

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.
Files changed (157) hide show
  1. package/.github/workflows/publish.yaml +1 -1
  2. package/.releaserc.yaml +2 -2
  3. package/dist/conditions/condition.evaluator.d.ts +6 -0
  4. package/dist/conditions/condition.evaluator.js +27 -0
  5. package/dist/conditions/condition.evaluator.js.map +1 -0
  6. package/dist/conditions/index.d.ts +2 -0
  7. package/dist/conditions/index.js +19 -0
  8. package/dist/conditions/index.js.map +1 -0
  9. package/dist/conditions/types.d.ts +7 -0
  10. package/dist/conditions/types.js +3 -0
  11. package/dist/conditions/types.js.map +1 -0
  12. package/dist/feature-flags/feature-flag.evaluator.d.ts +2 -0
  13. package/dist/feature-flags/feature-flag.evaluator.js +24 -0
  14. package/dist/feature-flags/feature-flag.evaluator.js.map +1 -0
  15. package/dist/feature-flags/index.d.ts +2 -0
  16. package/dist/feature-flags/index.js +18 -0
  17. package/dist/feature-flags/index.js.map +1 -0
  18. package/dist/feature-flags/types.d.ts +10 -0
  19. package/dist/feature-flags/types.js +3 -0
  20. package/dist/feature-flags/types.js.map +1 -0
  21. package/dist/index.d.ts +5 -1
  22. package/dist/index.js +12 -5
  23. package/dist/index.js.map +1 -1
  24. package/dist/operations/boolean/index.d.ts +4 -0
  25. package/dist/operations/boolean/index.js +25 -0
  26. package/dist/operations/boolean/index.js.map +1 -0
  27. package/dist/operations/boolean/operations.d.ts +3 -0
  28. package/dist/operations/boolean/operations.js +8 -0
  29. package/dist/operations/boolean/operations.js.map +1 -0
  30. package/dist/operations/boolean/sanitizers.d.ts +4 -0
  31. package/dist/operations/boolean/sanitizers.js +17 -0
  32. package/dist/operations/boolean/sanitizers.js.map +1 -0
  33. package/dist/operations/boolean/types.d.ts +3 -0
  34. package/dist/operations/boolean/types.js +3 -0
  35. package/dist/operations/boolean/types.js.map +1 -0
  36. package/dist/operations/components/operation.resolver.d.ts +2 -0
  37. package/dist/operations/components/operation.resolver.js +19 -0
  38. package/dist/operations/components/operation.resolver.js.map +1 -0
  39. package/dist/operations/components/sanitizers.resolver.d.ts +2 -0
  40. package/dist/operations/components/sanitizers.resolver.js +18 -0
  41. package/dist/operations/components/sanitizers.resolver.js.map +1 -0
  42. package/dist/operations/date/index.d.ts +4 -0
  43. package/dist/operations/date/index.js +28 -0
  44. package/dist/operations/date/index.js.map +1 -0
  45. package/dist/operations/date/operations.d.ts +6 -0
  46. package/dist/operations/date/operations.js +22 -0
  47. package/dist/operations/date/operations.js.map +1 -0
  48. package/dist/operations/date/sanitizers.d.ts +5 -0
  49. package/dist/operations/date/sanitizers.js +27 -0
  50. package/dist/operations/date/sanitizers.js.map +1 -0
  51. package/dist/operations/date/types.d.ts +8 -0
  52. package/dist/operations/date/types.js +3 -0
  53. package/dist/operations/date/types.js.map +1 -0
  54. package/dist/operations/numeric/index.d.ts +5 -0
  55. package/dist/operations/numeric/index.js +31 -0
  56. package/dist/operations/numeric/index.js.map +1 -0
  57. package/dist/operations/numeric/operations.d.ts +8 -0
  58. package/dist/operations/numeric/operations.js +28 -0
  59. package/dist/operations/numeric/operations.js.map +1 -0
  60. package/dist/operations/numeric/sanitizers.d.ts +5 -0
  61. package/dist/operations/numeric/sanitizers.js +32 -0
  62. package/dist/operations/numeric/sanitizers.js.map +1 -0
  63. package/dist/operations/numeric/types.d.ts +8 -0
  64. package/dist/operations/numeric/types.js +3 -0
  65. package/dist/operations/numeric/types.js.map +1 -0
  66. package/dist/operations/string/index.d.ts +4 -0
  67. package/dist/operations/string/index.js +29 -0
  68. package/dist/operations/string/index.js.map +1 -0
  69. package/dist/operations/string/operations.d.ts +7 -0
  70. package/dist/operations/string/operations.js +33 -0
  71. package/dist/operations/string/operations.js.map +1 -0
  72. package/dist/operations/string/sanitizers.d.ts +5 -0
  73. package/dist/operations/string/sanitizers.js +31 -0
  74. package/dist/operations/string/sanitizers.js.map +1 -0
  75. package/dist/operations/string/types.d.ts +7 -0
  76. package/dist/operations/string/types.js +3 -0
  77. package/dist/operations/string/types.js.map +1 -0
  78. package/dist/operations/types/index.d.ts +24 -0
  79. package/dist/operations/types/index.js +18 -0
  80. package/dist/operations/types/index.js.map +1 -0
  81. package/dist/operations/types/operation.enum.d.ts +18 -0
  82. package/dist/operations/types/operation.enum.js +27 -0
  83. package/dist/operations/types/operation.enum.js.map +1 -0
  84. package/dist/rules/index.d.ts +2 -0
  85. package/dist/rules/index.js +19 -0
  86. package/dist/rules/index.js.map +1 -0
  87. package/dist/rules/rule.evaluator.d.ts +2 -0
  88. package/dist/rules/rule.evaluator.js +16 -0
  89. package/dist/rules/rule.evaluator.js.map +1 -0
  90. package/dist/rules/types.d.ts +21 -0
  91. package/dist/rules/types.js +18 -0
  92. package/dist/rules/types.js.map +1 -0
  93. package/dist/user-entitlements/attributes.utils.d.ts +11 -0
  94. package/dist/user-entitlements/attributes.utils.js +43 -0
  95. package/dist/user-entitlements/attributes.utils.js.map +1 -0
  96. package/dist/user-entitlements/index.d.ts +3 -0
  97. package/dist/user-entitlements/index.js +20 -0
  98. package/dist/user-entitlements/index.js.map +1 -0
  99. package/dist/user-entitlements/is-entitled.evaluator.d.ts +3 -0
  100. package/dist/user-entitlements/is-entitled.evaluator.js +58 -0
  101. package/dist/user-entitlements/is-entitled.evaluator.js.map +1 -0
  102. package/dist/user-entitlements/types.d.ts +31 -0
  103. package/dist/user-entitlements/types.js +11 -0
  104. package/dist/user-entitlements/types.js.map +1 -0
  105. package/docs/CHANGELOG.md +75 -0
  106. package/jest.config.js +26 -0
  107. package/package.json +6 -2
  108. package/src/conditions/condition.evaluator.ts +35 -0
  109. package/src/conditions/index.ts +2 -0
  110. package/src/conditions/tests/condition.evaluator.spec.ts +52 -0
  111. package/src/conditions/types.ts +8 -0
  112. package/src/feature-flags/feature-flag.evaluator.ts +27 -0
  113. package/src/feature-flags/index.ts +2 -0
  114. package/src/feature-flags/tests/feature-flag.evaluator.spec.ts +176 -0
  115. package/src/feature-flags/types.ts +12 -0
  116. package/src/index.ts +16 -3
  117. package/src/operations/boolean/index.ts +10 -0
  118. package/src/operations/boolean/operations.ts +6 -0
  119. package/src/operations/boolean/sanitizers.ts +18 -0
  120. package/src/operations/boolean/tests/operations.spec.ts +13 -0
  121. package/src/operations/boolean/tests/sanitizers.spec.ts +22 -0
  122. package/src/operations/boolean/types.ts +3 -0
  123. package/src/operations/components/operation.resolver.ts +18 -0
  124. package/src/operations/components/sanitizers.resolver.ts +16 -0
  125. package/src/operations/components/tests/operation.resolver.spec.ts +8 -0
  126. package/src/operations/components/tests/sanitizers.resolver.spec.ts +14 -0
  127. package/src/operations/date/index.ts +18 -0
  128. package/src/operations/date/operations.ts +20 -0
  129. package/src/operations/date/sanitizers.ts +27 -0
  130. package/src/operations/date/tests/operations.spec.ts +45 -0
  131. package/src/operations/date/tests/sanitizers.spec.ts +43 -0
  132. package/src/operations/date/types.ts +10 -0
  133. package/src/operations/numeric/index.ts +23 -0
  134. package/src/operations/numeric/operations.ts +26 -0
  135. package/src/operations/numeric/sanitizers.ts +34 -0
  136. package/src/operations/numeric/tests/operations.spec.ts +64 -0
  137. package/src/operations/numeric/tests/sanitizers.spec.ts +44 -0
  138. package/src/operations/numeric/types.ts +10 -0
  139. package/src/operations/string/index.ts +20 -0
  140. package/src/operations/string/operations.ts +32 -0
  141. package/src/operations/string/sanitizers.ts +33 -0
  142. package/src/operations/string/tests/operations.spec.ts +38 -0
  143. package/src/operations/string/tests/sanitizers.spec.ts +45 -0
  144. package/src/operations/string/types.ts +9 -0
  145. package/src/operations/types/index.ts +30 -0
  146. package/src/operations/types/operation.enum.ts +25 -0
  147. package/src/rules/index.ts +2 -0
  148. package/src/rules/rule.evaluator.ts +14 -0
  149. package/src/rules/tests/rule.evaluator.spec.ts +135 -0
  150. package/src/rules/types.ts +27 -0
  151. package/src/user-entitlements/attributes.utils.ts +44 -0
  152. package/src/user-entitlements/index.ts +3 -0
  153. package/src/user-entitlements/is-entitled.evaluator.ts +82 -0
  154. package/src/user-entitlements/tests/attributes.utils.spec.ts +76 -0
  155. package/src/user-entitlements/tests/is-entitled.evaluator.spec.ts +298 -0
  156. package/src/user-entitlements/types.ts +36 -0
  157. package/src/index.spec.ts +0 -5
@@ -0,0 +1,135 @@
1
+ import { ConditionLogicEnum, Rule, RuleEvaluationResultEnum, TreatmentEnum } from '../types';
2
+ import { createRuleEvaluator } from '../rule.evaluator';
3
+ import { OperationEnum } from '../../operations/types';
4
+
5
+ describe('RuleEvaluator', () => {
6
+ it('should return RuleEvaluationResultEnum.Insufficient when conditions are invalid', () => {
7
+ const rule: Rule = {
8
+ treatment: TreatmentEnum.True,
9
+ conditions: [
10
+ {
11
+ attribute: 'test',
12
+ op: 'not supported' as any,
13
+ value: { list: ['test'] },
14
+ negate: false,
15
+ },
16
+ ],
17
+ conditionLogic: ConditionLogicEnum.And,
18
+ };
19
+
20
+ const ruleEvaluator = createRuleEvaluator({ rule });
21
+ const result = ruleEvaluator({});
22
+
23
+ expect(result).toEqual(RuleEvaluationResultEnum.Insufficient);
24
+ });
25
+
26
+ it('should return RuleEvaluationResultEnum.Insufficient when no (valid) condition passes validation', () => {
27
+ const rule: Rule = {
28
+ treatment: TreatmentEnum.True,
29
+ conditions: [
30
+ {
31
+ attribute: 'attribute',
32
+ op: OperationEnum.StartsWith,
33
+ value: { list: ['test'] },
34
+ negate: false,
35
+ },
36
+ {
37
+ attribute: 'attribute',
38
+ op: OperationEnum.Contains,
39
+ value: { list: ['test'] },
40
+ negate: false,
41
+ },
42
+ ],
43
+ conditionLogic: ConditionLogicEnum.And,
44
+ };
45
+
46
+ const ruleEvaluator = createRuleEvaluator({ rule });
47
+ const result = ruleEvaluator({ attribute: '1' });
48
+
49
+ expect(result).toEqual(RuleEvaluationResultEnum.Insufficient);
50
+ });
51
+
52
+ it('should return RuleEvaluationResultEnum.Insufficient when some (valid) conditions pass validation', () => {
53
+ const rule: Rule = {
54
+ treatment: TreatmentEnum.True,
55
+ conditions: [
56
+ {
57
+ attribute: 'attribute',
58
+ op: OperationEnum.StartsWith,
59
+ value: { list: ['test'] },
60
+ negate: false,
61
+ },
62
+ {
63
+ attribute: 'attribute',
64
+ op: OperationEnum.Contains,
65
+ value: { list: ['test'] },
66
+ negate: false,
67
+ },
68
+ ],
69
+ conditionLogic: ConditionLogicEnum.And,
70
+ };
71
+
72
+ const ruleEvaluator = createRuleEvaluator({ rule });
73
+ const result = ruleEvaluator({ attribute: 'te' });
74
+
75
+ expect(result).toEqual(RuleEvaluationResultEnum.Insufficient);
76
+ });
77
+
78
+ it('should return RuleEvaluationResultEnum.Treatable when all (valid) conditions pass validation', () => {
79
+ const rule: Rule = {
80
+ treatment: TreatmentEnum.True,
81
+ conditions: [
82
+ {
83
+ attribute: 'attribute',
84
+ op: OperationEnum.InList,
85
+ value: { list: ['test'] },
86
+ negate: false,
87
+ },
88
+ {
89
+ attribute: 'attribute',
90
+ op: OperationEnum.Contains,
91
+ value: { list: ['test'] },
92
+ negate: false,
93
+ },
94
+ ],
95
+ conditionLogic: ConditionLogicEnum.And,
96
+ };
97
+
98
+ const ruleEvaluator = createRuleEvaluator({ rule });
99
+ const result = ruleEvaluator({ attribute: 'test' });
100
+
101
+ expect(result).toEqual(RuleEvaluationResultEnum.Treatable);
102
+ });
103
+
104
+ it('should return RuleEvaluationResultEnum.Treatable when all (valid) conditions pass validation', () => {
105
+ const rule: Rule = {
106
+ treatment: TreatmentEnum.True,
107
+ conditions: [
108
+ {
109
+ attribute: 'attribute',
110
+ op: OperationEnum.InList,
111
+ value: { list: ['test'] },
112
+ negate: false,
113
+ },
114
+ {
115
+ attribute: 'attribute',
116
+ op: OperationEnum.Contains,
117
+ value: { list: ['not'] },
118
+ negate: true,
119
+ },
120
+ {
121
+ attribute: 'numeric',
122
+ op: OperationEnum.Equal,
123
+ value: { number: 2 },
124
+ negate: false,
125
+ },
126
+ ],
127
+ conditionLogic: ConditionLogicEnum.And,
128
+ };
129
+
130
+ const ruleEvaluator = createRuleEvaluator({ rule });
131
+ const result = ruleEvaluator({ attribute: 'test', numeric: 2 });
132
+
133
+ expect(result).toEqual(RuleEvaluationResultEnum.Treatable);
134
+ });
135
+ });
@@ -0,0 +1,27 @@
1
+ import { Condition } from '../conditions';
2
+
3
+ export interface Rule {
4
+ conditionLogic: ConditionLogicEnum.And;
5
+ conditions: Condition[];
6
+ treatment: TreatmentEnum;
7
+ }
8
+
9
+ export enum ConditionLogicEnum {
10
+ And = 'and',
11
+ }
12
+
13
+ export enum TreatmentEnum {
14
+ True = 'true',
15
+ False = 'false',
16
+ }
17
+
18
+ export enum RuleEvaluationResultEnum {
19
+ Treatable = 'treatable',
20
+ Insufficient = 'insufficient',
21
+ }
22
+
23
+ export interface CreateRuleEvaluatorPayload {
24
+ rule: Rule;
25
+ }
26
+
27
+ export type RuleEvaluator = (attributes: Record<string, unknown>) => RuleEvaluationResultEnum;
@@ -0,0 +1,44 @@
1
+ import { flatten } from 'flat';
2
+ import { Attributes, JwtAttributes, FronteggAttributes } from './types';
3
+
4
+ /**
5
+ * Merges both `custom` and `jwt` records, map Frontegg attributes and modifies record keys with corrisponding prefixes
6
+ *
7
+ * Example:
8
+ * Input: { 'custom': { 'customAttribute': 'someValue' }, 'jwt': { 'email': 'user@email.com', other: 'some-vaule' } }
9
+ * Output: { 'customAttribute': 'someValue', 'frontegg.email': 'user@email.com', 'jwt.email': 'user@email.com', 'jwt.other': 'some-vaule' }
10
+ */
11
+ export function prepareAttributes(
12
+ attributes: Attributes = {},
13
+ customFronteggAttributesMapper?: (jwtAttributes: JwtAttributes) => FronteggAttributes,
14
+ ): Record<string, unknown> {
15
+ const { custom = {}, jwt = {} } = attributes;
16
+ const flatJwtAttributes = flatten<JwtAttributes, JwtAttributes>(jwt);
17
+ const fronteggAttributes = customFronteggAttributesMapper
18
+ ? customFronteggAttributesMapper(jwt)
19
+ : defaultFronteggAttributesMapper(jwt);
20
+ const fronteggAttributesPrefix = 'frontegg.';
21
+ const jwtAttributesPrefix = 'jwt.';
22
+
23
+ return {
24
+ ...custom,
25
+ ...modifyObjectKeysWithPrefix(fronteggAttributes, fronteggAttributesPrefix),
26
+ ...modifyObjectKeysWithPrefix(flatJwtAttributes, jwtAttributesPrefix),
27
+ };
28
+ }
29
+
30
+ export function defaultFronteggAttributesMapper(jwt: JwtAttributes): FronteggAttributes {
31
+ return {
32
+ email: jwt.email as string,
33
+ emailVerified: jwt.email_verified as boolean,
34
+ tenantId: jwt.tenantId as string,
35
+ userId: jwt.userId as string,
36
+ };
37
+ }
38
+
39
+ export function modifyObjectKeysWithPrefix(object: Record<string, unknown>, prefix: string): Record<string, unknown> {
40
+ return Object.keys(object).reduce((modifiedObject, currentKey) => {
41
+ modifiedObject[`${prefix}${currentKey}`] = object[currentKey];
42
+ return modifiedObject;
43
+ }, {});
44
+ }
@@ -0,0 +1,3 @@
1
+ export * from './is-entitled.evaluator';
2
+ export * from './types';
3
+ export * from './attributes.utils';
@@ -0,0 +1,82 @@
1
+ import {
2
+ EntitlementResult,
3
+ NotEntitledJustification,
4
+ NO_EXPIRATION_TIME,
5
+ UserEntitlementsContext,
6
+ Attributes,
7
+ } from './types';
8
+
9
+ import { evaluateFeatureFlag } from '../feature-flags';
10
+ import { prepareAttributes } from './attributes.utils';
11
+ import { TreatmentEnum } from '../rules';
12
+ export function evaluateIsEntitledToFeature(
13
+ featureKey: string,
14
+ userEntitlementsContext: UserEntitlementsContext,
15
+ attributes: Attributes = {},
16
+ ): EntitlementResult {
17
+ const feature = userEntitlementsContext.features[featureKey];
18
+ let hasExpired = false;
19
+ if (feature && feature.expireTime !== null) {
20
+ hasExpired = feature.expireTime !== NO_EXPIRATION_TIME && feature.expireTime < Date.now();
21
+
22
+ if (!hasExpired) {
23
+ return { isEntitled: true };
24
+ }
25
+ }
26
+
27
+ if (feature && feature.featureFlag) {
28
+ const preparedAttributes = prepareAttributes(attributes);
29
+ const { treatment } = evaluateFeatureFlag(feature.featureFlag, preparedAttributes);
30
+ if (treatment === TreatmentEnum.True) {
31
+ return { isEntitled: true };
32
+ }
33
+ }
34
+
35
+ return {
36
+ isEntitled: false,
37
+ justification: hasExpired ? NotEntitledJustification.BUNDLE_EXPIRED : NotEntitledJustification.MISSING_FEATURE,
38
+ };
39
+ }
40
+
41
+ export function evaluateIsEntitledToPermissions(
42
+ permissionKey: string,
43
+ userEntitlementsContext: UserEntitlementsContext,
44
+ attributes?: Attributes,
45
+ ): EntitlementResult {
46
+ const permission = userEntitlementsContext.permissions[permissionKey];
47
+
48
+ if (!permission) {
49
+ return { isEntitled: false, justification: NotEntitledJustification.MISSING_PERMISSION };
50
+ }
51
+
52
+ const linkedFeatures = getLinkedFeatures(permissionKey, userEntitlementsContext);
53
+
54
+ if (!linkedFeatures.length) {
55
+ return { isEntitled: true };
56
+ }
57
+
58
+ let hasExpired = false;
59
+
60
+ for (const featureKey of linkedFeatures) {
61
+ const { isEntitled, justification } = evaluateIsEntitledToFeature(featureKey, userEntitlementsContext, attributes);
62
+
63
+ if (isEntitled) {
64
+ return { isEntitled: true };
65
+ }
66
+
67
+ if (justification === NotEntitledJustification.BUNDLE_EXPIRED) {
68
+ hasExpired = true;
69
+ }
70
+ }
71
+
72
+ return {
73
+ isEntitled: false,
74
+ justification: hasExpired ? NotEntitledJustification.BUNDLE_EXPIRED : NotEntitledJustification.MISSING_FEATURE,
75
+ };
76
+ }
77
+
78
+ function getLinkedFeatures(permissionKey: string, userEntitlementsContext: UserEntitlementsContext): string[] {
79
+ return Object.keys(userEntitlementsContext.features).filter((featureKey) =>
80
+ userEntitlementsContext.features[featureKey].linkedPermissions.includes(permissionKey),
81
+ );
82
+ }
@@ -0,0 +1,76 @@
1
+ import * as AttributesUtils from '../attributes.utils';
2
+ import { Attributes, FronteggAttributes, JwtAttributes } from '../types';
3
+ import { flatten } from 'flat';
4
+ describe('prepareAttributes', () => {
5
+ test('given custom & jwt attributes, expected is merged & flatten attributes record', () => {
6
+ const attributes: Attributes = {
7
+ custom: {
8
+ customAttribute: 'some-value',
9
+ },
10
+ jwt: {
11
+ userId: 'user-1',
12
+ tenantId: 'tenant-1',
13
+ email: 'test@email.com',
14
+ email_verified: true,
15
+ dummyAttribute: 'dummy',
16
+ },
17
+ };
18
+
19
+ const expectedPreparedAttributes = {
20
+ customAttribute: 'some-value',
21
+ 'frontegg.userId': 'user-1',
22
+ 'frontegg.tenantId': 'tenant-1',
23
+ 'frontegg.email': 'test@email.com',
24
+ 'frontegg.emailVerified': true,
25
+ 'jwt.userId': 'user-1',
26
+ 'jwt.tenantId': 'tenant-1',
27
+ 'jwt.email': 'test@email.com',
28
+ 'jwt.email_verified': true,
29
+ 'jwt.dummyAttribute': 'dummy',
30
+ };
31
+
32
+ const preparedAttributes = AttributesUtils.prepareAttributes(attributes);
33
+
34
+ expect(preparedAttributes).toEqual(expectedPreparedAttributes);
35
+ });
36
+ });
37
+
38
+ describe('defaultFronteggAttributesMapper', () => {
39
+ test('given jwt-attributes, expected mapped frontegg-attributes', async () => {
40
+ const jwtAttributes: JwtAttributes = {
41
+ userId: 'user-1',
42
+ tenantId: 'tenant-1',
43
+ email: 'test@email.com',
44
+ email_verified: true,
45
+ dummyAttribute: 'dummy',
46
+ };
47
+
48
+ const expectedFronteggAttributes: FronteggAttributes = {
49
+ userId: 'user-1',
50
+ tenantId: 'tenant-1',
51
+ email: 'test@email.com',
52
+ emailVerified: true,
53
+ };
54
+
55
+ const mappedAttributes = AttributesUtils.defaultFronteggAttributesMapper(jwtAttributes);
56
+
57
+ expect(mappedAttributes).toEqual(expectedFronteggAttributes);
58
+ });
59
+ });
60
+
61
+ describe('modifyObjectKeysWithPrefix', () => {
62
+ test('given object and prefix, object keys should altered with prefix', async () => {
63
+ const prefix = 'test.';
64
+ const obj = {
65
+ property: 'value',
66
+ };
67
+
68
+ const expectedModifiedObject = {
69
+ 'test.property': 'value',
70
+ };
71
+
72
+ const modifiedObject = AttributesUtils.modifyObjectKeysWithPrefix(obj, prefix);
73
+
74
+ expect(modifiedObject).toEqual(expectedModifiedObject);
75
+ });
76
+ });
@@ -0,0 +1,298 @@
1
+ import * as IsEntitledEvaluators from '../is-entitled.evaluator';
2
+ import * as FeatureFlagEvalutor from '../../feature-flags/feature-flag.evaluator';
3
+ import * as AttributesUtils from '../attributes.utils';
4
+ import { TreatmentEnum } from '../../rules';
5
+ import { EntitlementResult, NotEntitledJustification, NO_EXPIRATION_TIME, UserEntitlementsContext } from '../types';
6
+ import { FeatureFlag } from '../../feature-flags/types';
7
+
8
+ const mockFeatureFlag: FeatureFlag = {
9
+ on: true,
10
+ defaultTreatment: TreatmentEnum.True,
11
+ offTreatment: TreatmentEnum.False,
12
+ rules: [],
13
+ };
14
+ const truthyEntitlementResult: EntitlementResult = {
15
+ isEntitled: true,
16
+ };
17
+ const falsyEntitlementResultMissingFeature: EntitlementResult = {
18
+ isEntitled: false,
19
+ justification: NotEntitledJustification.MISSING_FEATURE,
20
+ };
21
+ const falsyEntitlementResultBundleExpired: EntitlementResult = {
22
+ isEntitled: false,
23
+ justification: NotEntitledJustification.BUNDLE_EXPIRED,
24
+ };
25
+ const falsyEntitlementResultMissingPermission: EntitlementResult = {
26
+ isEntitled: false,
27
+ justification: NotEntitledJustification.MISSING_PERMISSION,
28
+ };
29
+ describe('evaluateIsEntitledToFeature', () => {
30
+ beforeAll(() => {
31
+ jest.spyOn(AttributesUtils, 'prepareAttributes').mockReturnValue({ testAttribute: 'test-value' });
32
+ });
33
+ describe('entitled', () => {
34
+ describe('feature-flag evaluated truthy', () => {
35
+ beforeAll(async () => {
36
+ jest.spyOn(FeatureFlagEvalutor, 'evaluateFeatureFlag').mockReturnValue({ treatment: TreatmentEnum.True });
37
+ });
38
+
39
+ test('feature granted with valid expiration date', async () => {
40
+ const userEntitlementContext: UserEntitlementsContext = {
41
+ features: {
42
+ 'test-feature': {
43
+ expireTime: new Date().getTime() + 3600,
44
+ linkedPermissions: [],
45
+ featureFlag: mockFeatureFlag,
46
+ },
47
+ },
48
+ permissions: {},
49
+ };
50
+ const result = IsEntitledEvaluators.evaluateIsEntitledToFeature('test-feature', userEntitlementContext, {});
51
+
52
+ expect(result).toEqual(truthyEntitlementResult);
53
+ });
54
+ test('feature granted with expired expiration date', async () => {
55
+ const userEntitlementContext: UserEntitlementsContext = {
56
+ features: {
57
+ 'test-feature': {
58
+ expireTime: new Date().getTime() - 3600,
59
+ linkedPermissions: [],
60
+ featureFlag: mockFeatureFlag,
61
+ },
62
+ },
63
+ permissions: {},
64
+ };
65
+ const result = IsEntitledEvaluators.evaluateIsEntitledToFeature('test-feature', userEntitlementContext, {});
66
+
67
+ expect(result).toEqual(truthyEntitlementResult);
68
+ });
69
+ test('feature granted with no expiration date', async () => {
70
+ const userEntitlementContext: UserEntitlementsContext = {
71
+ features: {
72
+ 'test-feature': {
73
+ expireTime: NO_EXPIRATION_TIME,
74
+ linkedPermissions: [],
75
+ featureFlag: mockFeatureFlag,
76
+ },
77
+ },
78
+ permissions: {},
79
+ };
80
+ const result = IsEntitledEvaluators.evaluateIsEntitledToFeature('test-feature', userEntitlementContext, {});
81
+
82
+ expect(result).toEqual(truthyEntitlementResult);
83
+ });
84
+ test('feature has not been granted', async () => {
85
+ const userEntitlementContext: UserEntitlementsContext = {
86
+ features: {
87
+ 'test-feature': {
88
+ expireTime: null,
89
+ linkedPermissions: [],
90
+ featureFlag: mockFeatureFlag,
91
+ },
92
+ },
93
+ permissions: {},
94
+ };
95
+ const result = IsEntitledEvaluators.evaluateIsEntitledToFeature('test-feature', userEntitlementContext, {});
96
+
97
+ expect(result).toEqual(truthyEntitlementResult);
98
+ });
99
+ });
100
+ describe('feature-flag evaluated falsy', () => {
101
+ beforeAll(async () => {
102
+ jest.spyOn(FeatureFlagEvalutor, 'evaluateFeatureFlag').mockReturnValue({ treatment: TreatmentEnum.False });
103
+ });
104
+ test('feature granted with no expiration date', async () => {
105
+ const userEntitlementContext: UserEntitlementsContext = {
106
+ features: {
107
+ 'test-feature': {
108
+ expireTime: NO_EXPIRATION_TIME,
109
+ linkedPermissions: [],
110
+ featureFlag: mockFeatureFlag,
111
+ },
112
+ },
113
+ permissions: {},
114
+ };
115
+ const result = IsEntitledEvaluators.evaluateIsEntitledToFeature('test-feature', userEntitlementContext, {});
116
+
117
+ expect(result).toEqual(truthyEntitlementResult);
118
+ });
119
+ test('feature granted with valid expiration date', async () => {
120
+ const userEntitlementContext: UserEntitlementsContext = {
121
+ features: {
122
+ 'test-feature': {
123
+ expireTime: Date.now() + 3600,
124
+ linkedPermissions: [],
125
+ featureFlag: mockFeatureFlag,
126
+ },
127
+ },
128
+ permissions: {},
129
+ };
130
+ const result = IsEntitledEvaluators.evaluateIsEntitledToFeature('test-feature', userEntitlementContext, {});
131
+
132
+ expect(result).toEqual(truthyEntitlementResult);
133
+ });
134
+ });
135
+ describe('no feature flag', () => {
136
+ test('feature granted with no expiration date', async () => {
137
+ const userEntitlementContext: UserEntitlementsContext = {
138
+ features: {
139
+ 'test-feature': {
140
+ expireTime: NO_EXPIRATION_TIME,
141
+ linkedPermissions: [],
142
+ },
143
+ },
144
+ permissions: {},
145
+ };
146
+ const result = IsEntitledEvaluators.evaluateIsEntitledToFeature('test-feature', userEntitlementContext, {});
147
+
148
+ expect(result).toEqual(truthyEntitlementResult);
149
+ });
150
+ test('feature granted with valid expiration date', async () => {
151
+ const userEntitlementContext: UserEntitlementsContext = {
152
+ features: {
153
+ 'test-feature': {
154
+ expireTime: Date.now() + 3600,
155
+ linkedPermissions: [],
156
+ },
157
+ },
158
+ permissions: {},
159
+ };
160
+ const result = IsEntitledEvaluators.evaluateIsEntitledToFeature('test-feature', userEntitlementContext, {});
161
+
162
+ expect(result).toEqual(truthyEntitlementResult);
163
+ });
164
+ });
165
+ });
166
+
167
+ describe('not entitled', () => {
168
+ describe('feature-flag evaluated falsy', () => {
169
+ beforeAll(async () => {
170
+ jest.spyOn(FeatureFlagEvalutor, 'evaluateFeatureFlag').mockReturnValue({ treatment: TreatmentEnum.False });
171
+ });
172
+ test('feature has not been granted', async () => {
173
+ const userEntitlementContext: UserEntitlementsContext = {
174
+ features: {
175
+ 'test-feature': {
176
+ expireTime: null,
177
+ linkedPermissions: [],
178
+ featureFlag: mockFeatureFlag,
179
+ },
180
+ },
181
+ permissions: {},
182
+ };
183
+ const result = IsEntitledEvaluators.evaluateIsEntitledToFeature('test-feature', userEntitlementContext, {});
184
+
185
+ expect(result).toEqual(falsyEntitlementResultMissingFeature);
186
+ });
187
+ test('feature granted with expired expiration date', async () => {
188
+ const userEntitlementContext: UserEntitlementsContext = {
189
+ features: {
190
+ 'test-feature': {
191
+ expireTime: Date.now() - 3600,
192
+ linkedPermissions: [],
193
+ featureFlag: mockFeatureFlag,
194
+ },
195
+ },
196
+ permissions: {},
197
+ };
198
+ const result = IsEntitledEvaluators.evaluateIsEntitledToFeature('test-feature', userEntitlementContext, {});
199
+
200
+ expect(result).toEqual(falsyEntitlementResultBundleExpired);
201
+ });
202
+ });
203
+
204
+ describe('no feature', () => {
205
+ test('feature does not exist', async () => {
206
+ const userEntitlementContext: UserEntitlementsContext = {
207
+ features: {},
208
+ permissions: {},
209
+ };
210
+ const result = IsEntitledEvaluators.evaluateIsEntitledToFeature('test-feature', userEntitlementContext, {});
211
+
212
+ expect(result).toEqual(falsyEntitlementResultMissingFeature);
213
+ });
214
+ });
215
+ });
216
+ });
217
+
218
+ describe('evaluateIsEntitledToPermission', () => {
219
+ describe('entitled', () => {
220
+ test('permission granted, no linked feature/s to it', async () => {
221
+ const userEntitlementContext: UserEntitlementsContext = {
222
+ features: {},
223
+ permissions: { 'test.permission': true },
224
+ };
225
+ const result = IsEntitledEvaluators.evaluateIsEntitledToPermissions(
226
+ 'test.permission',
227
+ userEntitlementContext,
228
+ {},
229
+ );
230
+
231
+ expect(result).toEqual(truthyEntitlementResult);
232
+ });
233
+ test('permission granted with linked feature/s, feature is entitled', async () => {
234
+ jest.spyOn(IsEntitledEvaluators, 'evaluateIsEntitledToFeature').mockReturnValue({ isEntitled: true });
235
+
236
+ const userEntitlementContext: UserEntitlementsContext = {
237
+ features: { 'test-feature': { expireTime: NO_EXPIRATION_TIME, linkedPermissions: ['test.permission'] } },
238
+ permissions: { 'test.permission': true },
239
+ };
240
+
241
+ const result = IsEntitledEvaluators.evaluateIsEntitledToPermissions(
242
+ 'test.permission',
243
+ userEntitlementContext,
244
+ {},
245
+ );
246
+
247
+ expect(result).toEqual(truthyEntitlementResult);
248
+ });
249
+ });
250
+
251
+ describe('not entitled', () => {
252
+ test('permission not granted', async () => {
253
+ const userEntitlementContext: UserEntitlementsContext = {
254
+ features: {},
255
+ permissions: {},
256
+ };
257
+ const result = IsEntitledEvaluators.evaluateIsEntitledToPermissions(
258
+ 'test.permission',
259
+ userEntitlementContext,
260
+ {},
261
+ );
262
+
263
+ expect(result).toEqual(falsyEntitlementResultMissingPermission);
264
+ });
265
+ test('permission granted with linked feature/s, no feature is entiteld', async () => {
266
+ jest
267
+ .spyOn(IsEntitledEvaluators, 'evaluateIsEntitledToFeature')
268
+ .mockReturnValue({ isEntitled: false, justification: NotEntitledJustification.MISSING_FEATURE });
269
+
270
+ const userEntitlementContext: UserEntitlementsContext = {
271
+ features: { 'test-feature': { expireTime: null, linkedPermissions: ['test.permission'] } },
272
+ permissions: { 'test.permission': true },
273
+ };
274
+
275
+ const result = IsEntitledEvaluators.evaluateIsEntitledToPermissions(
276
+ 'test.permission',
277
+ userEntitlementContext,
278
+ {},
279
+ );
280
+
281
+ expect(result).toEqual(falsyEntitlementResultMissingFeature);
282
+ });
283
+ test('permission granted with linked feature/s, no feature is entiteld with expired bundle', async () => {
284
+ jest
285
+ .spyOn(IsEntitledEvaluators, 'evaluateIsEntitledToFeature')
286
+ .mockReturnValue({ isEntitled: false, justification: NotEntitledJustification.BUNDLE_EXPIRED });
287
+ });
288
+
289
+ const userEntitlementContext: UserEntitlementsContext = {
290
+ features: { 'test-feature': { expireTime: Date.now() - 3600, linkedPermissions: ['test.permission'] } },
291
+ permissions: { 'test.permission': true },
292
+ };
293
+
294
+ const result = IsEntitledEvaluators.evaluateIsEntitledToPermissions('test.permission', userEntitlementContext, {});
295
+
296
+ expect(result).toEqual(falsyEntitlementResultBundleExpired);
297
+ });
298
+ });