@tehw0lf/yaft 0.0.4 → 0.0.6

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.
@@ -14,6 +14,7 @@ jobs:
14
14
  actions: write
15
15
  contents: write
16
16
  packages: write
17
+ id-token: write
17
18
  with:
18
19
  tool: "npm"
19
20
  test: "run test"
@@ -22,5 +23,3 @@ jobs:
22
23
  event_name: ${{ github.event_name }}
23
24
  artifact_path: "dist"
24
25
  library_path: "dist"
25
- secrets:
26
- NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
package/jest.config.js CHANGED
@@ -7,6 +7,9 @@ module.exports = {
7
7
  globals: {},
8
8
  coverageDirectory: 'coverage/yaft',
9
9
  testEnvironment: "node",
10
+ testMatch: [
11
+ '<rootDir>/src/**/*.spec.ts'
12
+ ],
10
13
  transform: {
11
14
  "^.+\.tsx?$": ["ts-jest",{}],
12
15
  },
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@tehw0lf/yaft",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "YaFT - Feature Toggles using Go&PostgreSQL or any source!",
5
5
  "type": "commonjs",
6
6
  "scripts": {
7
7
  "build": "tsc && npm pkg delete devDependencies && cp package.json README.md logo.svg dist/",
8
- "test": "jest"
8
+ "test": "npx jest"
9
9
  },
10
10
  "repository": {
11
11
  "type": "git",
@@ -9,6 +9,7 @@ export type Feature = {
9
9
  value: string;
10
10
  activeAt: string;
11
11
  disabledAt: string;
12
+ tags?: string[];
12
13
  };
13
14
 
14
15
  export interface FeatureProvider<T> {
@@ -17,7 +17,7 @@ export class ApiServiceBooleanProvider implements FeatureProvider<boolean> {
17
17
  async getCollectionHash(configPathOrUrl: string): Promise<void> {
18
18
  try {
19
19
  const response = await axios.get(configPathOrUrl);
20
- const newHash = response.data.value;
20
+ const newHash = response.data.collectionHash || response.data.value;
21
21
  if (this.collectionHash !== newHash) {
22
22
  this.collectionHash = newHash;
23
23
  await this.getConfig(`${this.apiUrl}/features/${this.baseUUID}`);
@@ -30,7 +30,23 @@ export class ApiServiceBooleanProvider implements FeatureProvider<boolean> {
30
30
  async getConfig(configPathOrUrl: string): Promise<void> {
31
31
  try {
32
32
  const response = await axios.get(configPathOrUrl);
33
- this.data = response.data.value;
33
+ // Handle Go backend response format
34
+ const featuresArray = response.data.toggles || response.data.value || [];
35
+
36
+ // Convert array to keyed boolean object and handle capitalized field names
37
+ this.data = {};
38
+ if (Array.isArray(featuresArray)) {
39
+ featuresArray.forEach((feature: any) => {
40
+ const key = feature.key || feature.Key;
41
+ const value = feature.value || feature.Value;
42
+ if (key) {
43
+ this.data[key] = value === 'true' || value === true;
44
+ }
45
+ });
46
+ } else {
47
+ // Fallback for object format
48
+ this.data = featuresArray;
49
+ }
34
50
  } catch (error) {
35
51
  console.error("Failed to fetch feature toggle from API:", error);
36
52
  }
@@ -17,7 +17,7 @@ export class ApiServiceFeatureProvider implements FeatureProvider<Feature> {
17
17
  async getCollectionHash(configPathOrUrl: string): Promise<void> {
18
18
  try {
19
19
  const response = await axios.get(configPathOrUrl);
20
- const newHash = response.data.value;
20
+ const newHash = response.data.collectionHash || response.data.value;
21
21
  if (this.collectionHash !== newHash) {
22
22
  this.collectionHash = newHash;
23
23
  await this.getConfig(`${this.apiUrl}/features/${this.baseUUID}`);
@@ -30,7 +30,28 @@ export class ApiServiceFeatureProvider implements FeatureProvider<Feature> {
30
30
  async getConfig(configPathOrUrl: string): Promise<void> {
31
31
  try {
32
32
  const response = await axios.get(configPathOrUrl);
33
- this.data = response.data.value;
33
+ // Handle Go backend response format
34
+ const featuresArray = response.data.toggles || response.data.value || [];
35
+
36
+ // Convert array to keyed object and handle capitalized field names
37
+ this.data = {};
38
+ if (Array.isArray(featuresArray)) {
39
+ featuresArray.forEach((feature: any) => {
40
+ const normalizedFeature = {
41
+ key: feature.key || feature.Key,
42
+ value: feature.value || feature.Value,
43
+ activeAt: feature.activeAt || feature.ActiveAt,
44
+ disabledAt: feature.disabledAt || feature.DisabledAt,
45
+ tags: feature.tags || feature.Tags || [],
46
+ };
47
+ if (normalizedFeature.key) {
48
+ this.data[normalizedFeature.key] = normalizedFeature;
49
+ }
50
+ });
51
+ } else {
52
+ // Fallback for object format
53
+ this.data = featuresArray;
54
+ }
34
55
  } catch (error) {
35
56
  console.error("Failed to fetch feature toggle from API:", error);
36
57
  }
@@ -41,9 +62,26 @@ export class ApiServiceFeatureProvider implements FeatureProvider<Feature> {
41
62
 
42
63
  if (feature === undefined || feature === null) return false;
43
64
 
44
- if (Date.now() >= Date.parse(feature.disabledAt)) return false;
45
- if (Date.now() >= Date.parse(feature.activeAt)) return true;
65
+ // First check: value must be "true"
66
+ if (feature.value !== "true") return false;
67
+
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
+ }
46
83
 
47
- return JSON.parse(feature.value);
84
+ // All checks passed, feature is enabled
85
+ return true;
48
86
  }
49
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
- if (Date.now() >= Date.parse(feature.disabledAt)) return false;
26
- if (Date.now() >= Date.parse(feature.activeAt)) return true;
25
+ // First check: value must be "true"
26
+ if (feature.value !== "true") return false;
27
27
 
28
- return JSON.parse(feature.value);
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
  }
@@ -6,6 +6,7 @@ import axios from 'axios';
6
6
  // Mock axios for API provider tests
7
7
  jest.mock('axios');
8
8
  const mockedAxios = axios as jest.Mocked<typeof axios>;
9
+ mockedAxios.get = jest.fn();
9
10
 
10
11
  describe('Error Handling and Edge Cases', () => {
11
12
  let mockProvider: FeatureProvider<boolean>;
@@ -18,6 +19,8 @@ describe('Error Handling and Edge Cases', () => {
18
19
  };
19
20
  FeatureToggleBase.featureProvider = mockProvider;
20
21
  jest.clearAllMocks();
22
+ // Reset axios mock
23
+ (mockedAxios.get as jest.Mock).mockReset();
21
24
  });
22
25
 
23
26
  describe('Provider Initialization Errors', () => {
@@ -136,9 +139,8 @@ describe('Error Handling and Edge Cases', () => {
136
139
  }
137
140
  })();
138
141
 
139
- expect(() => {
140
- provider.isEnabled('invalidBoolean');
141
- }).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);
142
144
  });
143
145
  });
144
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
+ });
package/tsconfig.json CHANGED
@@ -6,5 +6,6 @@
6
6
  "declaration": true,
7
7
  "outDir": "./dist"
8
8
  },
9
- "include": ["**/**/*.ts"]
9
+ "include": ["src/**/*.ts"],
10
+ "exclude": ["**/*.spec.ts", "**/*.test.ts", "**/test/**/*"]
10
11
  }