@tehw0lf/yaft 0.0.5 → 0.0.7
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.
|
@@ -12,15 +12,15 @@ jobs:
|
|
|
12
12
|
uses: tehw0lf/workflows/.github/workflows/build-test-publish.yml@main
|
|
13
13
|
permissions:
|
|
14
14
|
actions: write
|
|
15
|
+
attestations: write
|
|
15
16
|
contents: write
|
|
16
17
|
packages: write
|
|
18
|
+
id-token: write
|
|
19
|
+
security-events: write
|
|
17
20
|
with:
|
|
18
21
|
tool: "npm"
|
|
19
22
|
test: "run test"
|
|
20
23
|
build_branch: "run build"
|
|
21
24
|
build_main: "run build"
|
|
22
|
-
event_name: ${{ github.event_name }}
|
|
23
25
|
artifact_path: "dist"
|
|
24
26
|
library_path: "dist"
|
|
25
|
-
secrets:
|
|
26
|
-
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/package.json
CHANGED
|
@@ -62,9 +62,26 @@ export class ApiServiceFeatureProvider implements FeatureProvider<Feature> {
|
|
|
62
62
|
|
|
63
63
|
if (feature === undefined || feature === null) return false;
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
if (
|
|
65
|
+
// First check: value must be "true"
|
|
66
|
+
if (feature.value !== "true") return false;
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
// Second check: if activeAt is set and in future, not yet active
|
|
69
|
+
if (feature.activeAt && feature.activeAt !== "") {
|
|
70
|
+
const activeTime = Date.parse(feature.activeAt);
|
|
71
|
+
if (!isNaN(activeTime) && Date.now() < activeTime) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Third check: if disabledAt is set and in past, already disabled
|
|
77
|
+
if (feature.disabledAt && feature.disabledAt !== "") {
|
|
78
|
+
const disabledTime = Date.parse(feature.disabledAt);
|
|
79
|
+
if (!isNaN(disabledTime) && Date.now() >= disabledTime) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// All checks passed, feature is enabled
|
|
85
|
+
return true;
|
|
69
86
|
}
|
|
70
87
|
}
|
|
@@ -22,9 +22,26 @@ export class LocalStorageFeatureProvider implements FeatureProvider<Feature> {
|
|
|
22
22
|
|
|
23
23
|
if (feature === undefined || feature === null) return false;
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
if (
|
|
25
|
+
// First check: value must be "true"
|
|
26
|
+
if (feature.value !== "true") return false;
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
// Second check: if activeAt is set and in future, not yet active
|
|
29
|
+
if (feature.activeAt && feature.activeAt !== "") {
|
|
30
|
+
const activeTime = Date.parse(feature.activeAt);
|
|
31
|
+
if (!isNaN(activeTime) && Date.now() < activeTime) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Third check: if disabledAt is set and in past, already disabled
|
|
37
|
+
if (feature.disabledAt && feature.disabledAt !== "") {
|
|
38
|
+
const disabledTime = Date.parse(feature.disabledAt);
|
|
39
|
+
if (!isNaN(disabledTime) && Date.now() >= disabledTime) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// All checks passed, feature is enabled
|
|
45
|
+
return true;
|
|
29
46
|
}
|
|
30
47
|
}
|
|
@@ -139,9 +139,8 @@ describe('Error Handling and Edge Cases', () => {
|
|
|
139
139
|
}
|
|
140
140
|
})();
|
|
141
141
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}).toThrow(); // JSON.parse should throw for invalid boolean strings
|
|
142
|
+
// New behavior: safely returns false for any value that is not "true"
|
|
143
|
+
expect(provider.isEnabled('invalidBoolean')).toBe(false);
|
|
145
144
|
});
|
|
146
145
|
});
|
|
147
146
|
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { Feature } from '../FeatureToggle';
|
|
2
|
+
import { LocalStorageFeatureProvider } from '../examples/LocalStorageFeatureProvider';
|
|
3
|
+
|
|
4
|
+
describe('Time-based Feature Evaluation Logic', () => {
|
|
5
|
+
describe('Current Time Logic', () => {
|
|
6
|
+
it('should handle feature with value=true and no dates (always enabled)', () => {
|
|
7
|
+
const provider = new (class extends LocalStorageFeatureProvider {
|
|
8
|
+
constructor() {
|
|
9
|
+
super('');
|
|
10
|
+
this.data = {
|
|
11
|
+
'always-enabled': {
|
|
12
|
+
key: 'always-enabled',
|
|
13
|
+
value: 'true',
|
|
14
|
+
activeAt: '',
|
|
15
|
+
disabledAt: ''
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
})();
|
|
20
|
+
|
|
21
|
+
expect(provider.isEnabled('always-enabled')).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should handle feature with value=false (always disabled)', () => {
|
|
25
|
+
const provider = new (class extends LocalStorageFeatureProvider {
|
|
26
|
+
constructor() {
|
|
27
|
+
super('');
|
|
28
|
+
this.data = {
|
|
29
|
+
'always-disabled': {
|
|
30
|
+
key: 'always-disabled',
|
|
31
|
+
value: 'false',
|
|
32
|
+
activeAt: '',
|
|
33
|
+
disabledAt: ''
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
})();
|
|
38
|
+
|
|
39
|
+
expect(provider.isEnabled('always-disabled')).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should disable feature if disabledAt is in the past', () => {
|
|
43
|
+
const pastDate = new Date(Date.now() - 86400000).toISOString(); // 1 day ago
|
|
44
|
+
|
|
45
|
+
const provider = new (class extends LocalStorageFeatureProvider {
|
|
46
|
+
constructor() {
|
|
47
|
+
super('');
|
|
48
|
+
this.data = {
|
|
49
|
+
'past-disabled': {
|
|
50
|
+
key: 'past-disabled',
|
|
51
|
+
value: 'true',
|
|
52
|
+
activeAt: '',
|
|
53
|
+
disabledAt: pastDate
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
})();
|
|
58
|
+
|
|
59
|
+
expect(provider.isEnabled('past-disabled')).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should enable feature if activeAt is in the past and no disabledAt', () => {
|
|
63
|
+
const pastDate = new Date(Date.now() - 86400000).toISOString(); // 1 day ago
|
|
64
|
+
|
|
65
|
+
const provider = new (class extends LocalStorageFeatureProvider {
|
|
66
|
+
constructor() {
|
|
67
|
+
super('');
|
|
68
|
+
this.data = {
|
|
69
|
+
'past-active': {
|
|
70
|
+
key: 'past-active',
|
|
71
|
+
value: 'true',
|
|
72
|
+
activeAt: pastDate,
|
|
73
|
+
disabledAt: ''
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
})();
|
|
78
|
+
|
|
79
|
+
expect(provider.isEnabled('past-active')).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('BUG: should NOT enable feature if activeAt is in the future', () => {
|
|
83
|
+
const futureDate = new Date(Date.now() + 86400000).toISOString(); // 1 day from now
|
|
84
|
+
|
|
85
|
+
const provider = new (class extends LocalStorageFeatureProvider {
|
|
86
|
+
constructor() {
|
|
87
|
+
super('');
|
|
88
|
+
this.data = {
|
|
89
|
+
'future-active': {
|
|
90
|
+
key: 'future-active',
|
|
91
|
+
value: 'true',
|
|
92
|
+
activeAt: futureDate,
|
|
93
|
+
disabledAt: ''
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
})();
|
|
98
|
+
|
|
99
|
+
// CURRENT BUGGY BEHAVIOR: Returns true because it falls through to line 28
|
|
100
|
+
// Date.now() >= Date.parse(futureDate) is false, so it skips line 26
|
|
101
|
+
// Falls through to: return JSON.parse(feature.value); which returns true
|
|
102
|
+
|
|
103
|
+
// EXPECTED BEHAVIOR: Should return false because feature is not yet active
|
|
104
|
+
expect(provider.isEnabled('future-active')).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should enable feature if within activeAt and disabledAt window', () => {
|
|
108
|
+
const pastDate = new Date(Date.now() - 86400000).toISOString(); // 1 day ago
|
|
109
|
+
const futureDate = new Date(Date.now() + 86400000).toISOString(); // 1 day from now
|
|
110
|
+
|
|
111
|
+
const provider = new (class extends LocalStorageFeatureProvider {
|
|
112
|
+
constructor() {
|
|
113
|
+
super('');
|
|
114
|
+
this.data = {
|
|
115
|
+
'windowed-feature': {
|
|
116
|
+
key: 'windowed-feature',
|
|
117
|
+
value: 'true',
|
|
118
|
+
activeAt: pastDate,
|
|
119
|
+
disabledAt: futureDate
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
})();
|
|
124
|
+
|
|
125
|
+
expect(provider.isEnabled('windowed-feature')).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should handle NaN dates gracefully (empty strings)', () => {
|
|
129
|
+
const provider = new (class extends LocalStorageFeatureProvider {
|
|
130
|
+
constructor() {
|
|
131
|
+
super('');
|
|
132
|
+
this.data = {
|
|
133
|
+
'empty-dates': {
|
|
134
|
+
key: 'empty-dates',
|
|
135
|
+
value: 'true',
|
|
136
|
+
activeAt: '',
|
|
137
|
+
disabledAt: ''
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
})();
|
|
142
|
+
|
|
143
|
+
// Date.parse('') returns NaN
|
|
144
|
+
// Date.now() >= NaN is false for both checks
|
|
145
|
+
// Should fall through to: return JSON.parse(feature.value);
|
|
146
|
+
expect(provider.isEnabled('empty-dates')).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should handle invalid date strings gracefully', () => {
|
|
150
|
+
const provider = new (class extends LocalStorageFeatureProvider {
|
|
151
|
+
constructor() {
|
|
152
|
+
super('');
|
|
153
|
+
this.data = {
|
|
154
|
+
'invalid-dates': {
|
|
155
|
+
key: 'invalid-dates',
|
|
156
|
+
value: 'true',
|
|
157
|
+
activeAt: 'not-a-date',
|
|
158
|
+
disabledAt: 'also-not-a-date'
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
})();
|
|
163
|
+
|
|
164
|
+
// Date.parse('not-a-date') returns NaN
|
|
165
|
+
// Similar behavior to empty strings
|
|
166
|
+
expect(provider.isEnabled('invalid-dates')).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
});
|