@tehw0lf/yaft 0.0.3 → 0.0.4

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.
@@ -0,0 +1,391 @@
1
+ import { FeatureToggle, FeatureToggleBase, FeatureProvider, Feature } from '../FeatureToggle';
2
+ import { LocalStorageFeatureProvider } from '../examples/LocalStorageFeatureProvider';
3
+ import { ApiServiceFeatureProvider } from '../examples/ApiServiceFeatureProvider';
4
+ import axios from 'axios';
5
+
6
+ // Mock axios for API provider tests
7
+ jest.mock('axios');
8
+ const mockedAxios = axios as jest.Mocked<typeof axios>;
9
+
10
+ describe('Error Handling and Edge Cases', () => {
11
+ let mockProvider: FeatureProvider<boolean>;
12
+
13
+ beforeEach(() => {
14
+ mockProvider = {
15
+ data: {},
16
+ getConfig: jest.fn(),
17
+ isEnabled: jest.fn()
18
+ };
19
+ FeatureToggleBase.featureProvider = mockProvider;
20
+ jest.clearAllMocks();
21
+ });
22
+
23
+ describe('Provider Initialization Errors', () => {
24
+ it('should handle missing provider gracefully', () => {
25
+ FeatureToggleBase.featureProvider = null as any;
26
+
27
+ expect(() => {
28
+ class TestClass {
29
+ @FeatureToggle('test')
30
+ method() {}
31
+ }
32
+ }).toThrow('FeatureToggleProvider not set');
33
+ });
34
+
35
+ it('should handle undefined provider gracefully', () => {
36
+ FeatureToggleBase.featureProvider = undefined as any;
37
+
38
+ expect(() => {
39
+ class TestClass {
40
+ @FeatureToggle('test')
41
+ method() {}
42
+ }
43
+ }).toThrow('FeatureToggleProvider not set');
44
+ });
45
+ });
46
+
47
+ describe('LocalStorageFeatureProvider Error Handling', () => {
48
+ it('should handle non-existent configuration files', () => {
49
+ const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
50
+
51
+ const provider = new LocalStorageFeatureProvider('./non-existent-file.json');
52
+
53
+ expect(provider.data).toEqual({});
54
+ expect(provider.isEnabled('anyKey')).toBe(false);
55
+ expect(consoleSpy).toHaveBeenCalledWith(
56
+ 'Failed to load configuration from local file:',
57
+ expect.objectContaining({
58
+ code: 'MODULE_NOT_FOUND'
59
+ })
60
+ );
61
+
62
+ consoleSpy.mockRestore();
63
+ });
64
+
65
+ it('should handle corrupted JSON files', () => {
66
+ const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
67
+ const originalRequire = require;
68
+
69
+ // Mock require to throw JSON parsing error
70
+ (global as any).require = jest.fn(() => {
71
+ throw new SyntaxError('Unexpected token in JSON');
72
+ });
73
+
74
+ const provider = new LocalStorageFeatureProvider('./corrupted.json');
75
+
76
+ expect(provider.data).toEqual({});
77
+ expect(provider.isEnabled('anyKey')).toBe(false);
78
+ expect(consoleSpy).toHaveBeenCalled();
79
+
80
+ // Restore
81
+ (global as any).require = originalRequire;
82
+ consoleSpy.mockRestore();
83
+ });
84
+
85
+ it('should handle features with malformed date strings', () => {
86
+ const provider = new (class extends LocalStorageFeatureProvider {
87
+ constructor() {
88
+ super('');
89
+ this.data = {
90
+ 'malformedDates': {
91
+ key: 'malformedDates',
92
+ value: 'true',
93
+ activeAt: 'invalid-date',
94
+ disabledAt: 'also-invalid-date'
95
+ }
96
+ };
97
+ }
98
+ })();
99
+
100
+ // Date.parse returns NaN for invalid dates, which should be handled
101
+ const result = provider.isEnabled('malformedDates');
102
+ expect(typeof result).toBe('boolean');
103
+ });
104
+
105
+ it('should handle features with null/undefined date values', () => {
106
+ const provider = new (class extends LocalStorageFeatureProvider {
107
+ constructor() {
108
+ super('');
109
+ this.data = {
110
+ 'nullDates': {
111
+ key: 'nullDates',
112
+ value: 'true',
113
+ activeAt: null as any,
114
+ disabledAt: undefined as any
115
+ }
116
+ };
117
+ }
118
+ })();
119
+
120
+ const result = provider.isEnabled('nullDates');
121
+ expect(typeof result).toBe('boolean');
122
+ });
123
+
124
+ it('should handle features with invalid boolean values', () => {
125
+ const provider = new (class extends LocalStorageFeatureProvider {
126
+ constructor() {
127
+ super('');
128
+ this.data = {
129
+ 'invalidBoolean': {
130
+ key: 'invalidBoolean',
131
+ value: 'not-a-boolean',
132
+ activeAt: '',
133
+ disabledAt: ''
134
+ }
135
+ };
136
+ }
137
+ })();
138
+
139
+ expect(() => {
140
+ provider.isEnabled('invalidBoolean');
141
+ }).toThrow(); // JSON.parse should throw for invalid boolean strings
142
+ });
143
+ });
144
+
145
+ describe('ApiServiceFeatureProvider Error Handling', () => {
146
+ it('should handle network failures gracefully', async () => {
147
+ const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
148
+ mockedAxios.get.mockRejectedValue(new Error('Network error'));
149
+
150
+ const provider = new ApiServiceFeatureProvider('https://api.test.com', 'test-uuid');
151
+
152
+ // Allow async operations to complete
153
+ await new Promise(resolve => setTimeout(resolve, 0));
154
+
155
+ expect(consoleSpy).toHaveBeenCalledWith(
156
+ 'Failed to fetch feature toggle from API:',
157
+ expect.any(Error)
158
+ );
159
+
160
+ consoleSpy.mockRestore();
161
+ });
162
+
163
+ it('should handle API returning invalid data format', async () => {
164
+ const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
165
+ mockedAxios.get
166
+ .mockResolvedValueOnce({ data: { value: 'invalid-hash' } }) // collection hash
167
+ .mockResolvedValueOnce({ data: { value: 'not-an-object' } }); // features
168
+
169
+ const provider = new ApiServiceFeatureProvider('https://api.test.com', 'test-uuid');
170
+
171
+ await new Promise(resolve => setTimeout(resolve, 0));
172
+
173
+ // Should handle gracefully without crashing
174
+ expect(provider.data).toBeDefined();
175
+
176
+ consoleSpy.mockRestore();
177
+ });
178
+
179
+ it('should handle HTTP error responses', async () => {
180
+ const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
181
+ mockedAxios.get.mockRejectedValue({
182
+ response: { status: 404, statusText: 'Not Found' }
183
+ });
184
+
185
+ const provider = new ApiServiceFeatureProvider('https://api.test.com', 'test-uuid');
186
+
187
+ await new Promise(resolve => setTimeout(resolve, 0));
188
+
189
+ expect(consoleSpy).toHaveBeenCalled();
190
+
191
+ consoleSpy.mockRestore();
192
+ });
193
+
194
+ it('should handle timeout errors', async () => {
195
+ const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
196
+ mockedAxios.get.mockRejectedValue({ code: 'ECONNABORTED', message: 'timeout' });
197
+
198
+ const provider = new ApiServiceFeatureProvider('https://api.test.com', 'test-uuid');
199
+
200
+ await new Promise(resolve => setTimeout(resolve, 0));
201
+
202
+ expect(consoleSpy).toHaveBeenCalled();
203
+
204
+ consoleSpy.mockRestore();
205
+ });
206
+ });
207
+
208
+ describe('Decorator Edge Cases', () => {
209
+ beforeEach(() => {
210
+ (mockProvider.isEnabled as jest.Mock).mockReturnValue(false);
211
+ });
212
+
213
+ it('should handle methods throwing errors in fallbacks', () => {
214
+ function errorFallback() {
215
+ throw new Error('Fallback error');
216
+ }
217
+
218
+ class TestClass {
219
+ @FeatureToggle('disabled', errorFallback)
220
+ method() {
221
+ return 'original';
222
+ }
223
+ }
224
+
225
+ const instance = new TestClass();
226
+
227
+ expect(() => {
228
+ instance.method();
229
+ }).toThrow('Fallback error');
230
+ });
231
+
232
+ it('should handle async methods throwing errors', async () => {
233
+ (mockProvider.isEnabled as jest.Mock).mockReturnValue(true);
234
+
235
+ class TestClass {
236
+ @FeatureToggle('enabled')
237
+ async asyncMethod() {
238
+ throw new Error('Async error');
239
+ }
240
+ }
241
+
242
+ const instance = new TestClass();
243
+
244
+ await expect(instance.asyncMethod()).rejects.toThrow('Async error');
245
+ });
246
+
247
+ it('should handle circular reference in fallback class', () => {
248
+ class CircularClass {
249
+ constructor() {
250
+ (this as any).self = this;
251
+ }
252
+ }
253
+
254
+ @FeatureToggle('disabled', CircularClass)
255
+ class TestClass {
256
+ constructor() {}
257
+ }
258
+
259
+ expect(() => {
260
+ new TestClass();
261
+ }).not.toThrow();
262
+ });
263
+
264
+ it('should handle fallback class constructor throwing errors', () => {
265
+ class ErrorClass {
266
+ constructor() {
267
+ throw new Error('Constructor error');
268
+ }
269
+ }
270
+
271
+ @FeatureToggle('disabled', ErrorClass)
272
+ class TestClass {
273
+ constructor() {}
274
+ }
275
+
276
+ expect(() => {
277
+ new TestClass();
278
+ }).toThrow('Constructor error');
279
+ });
280
+
281
+ it('should handle deeply nested method calls', () => {
282
+ (mockProvider.isEnabled as jest.Mock).mockReturnValue(true);
283
+
284
+ class TestClass {
285
+ @FeatureToggle('enabled')
286
+ level1() {
287
+ return this.level2();
288
+ }
289
+
290
+ level2() {
291
+ return this.level3();
292
+ }
293
+
294
+ level3() {
295
+ return 'deep result';
296
+ }
297
+ }
298
+
299
+ const instance = new TestClass();
300
+ expect(instance.level1()).toBe('deep result');
301
+ });
302
+ });
303
+
304
+ describe('Memory and Performance Edge Cases', () => {
305
+ it('should handle large number of decorated methods', () => {
306
+ (mockProvider.isEnabled as jest.Mock).mockReturnValue(true);
307
+
308
+ class TestClass {
309
+ @FeatureToggle('feature1') method1() { return 1; }
310
+ @FeatureToggle('feature2') method2() { return 2; }
311
+ @FeatureToggle('feature3') method3() { return 3; }
312
+ @FeatureToggle('feature4') method4() { return 4; }
313
+ @FeatureToggle('feature5') method5() { return 5; }
314
+ // ... many more methods would be here in a real test
315
+ }
316
+
317
+ const instance = new TestClass();
318
+
319
+ expect(instance.method1()).toBe(1);
320
+ expect(instance.method5()).toBe(5);
321
+ expect(mockProvider.isEnabled).toHaveBeenCalledTimes(5);
322
+ });
323
+
324
+ it('should handle rapid instantiation of decorated classes', () => {
325
+ (mockProvider.isEnabled as jest.Mock).mockReturnValue(false);
326
+
327
+ @FeatureToggle('disabled')
328
+ class TestClass {
329
+ value = Math.random();
330
+ }
331
+
332
+ const instances = Array.from({ length: 100 }, () => new TestClass());
333
+
334
+ expect(instances).toHaveLength(100);
335
+ expect(mockProvider.isEnabled).toHaveBeenCalledTimes(1); // Called once during decoration
336
+ });
337
+
338
+ it('should handle very long feature keys', () => {
339
+ const longKey = 'a'.repeat(10000);
340
+ (mockProvider.isEnabled as jest.Mock).mockReturnValue(true);
341
+
342
+ class TestClass {
343
+ @FeatureToggle(longKey)
344
+ method() {
345
+ return 'result';
346
+ }
347
+ }
348
+
349
+ const instance = new TestClass();
350
+ expect(instance.method()).toBe('result');
351
+ expect(mockProvider.isEnabled).toHaveBeenCalledWith(longKey);
352
+ });
353
+ });
354
+
355
+ describe('Type Safety Edge Cases', () => {
356
+ it('should handle mixed data types in providers', () => {
357
+ const mixedProvider = {
358
+ data: {
359
+ 'stringFeature': 'true',
360
+ 'booleanFeature': true,
361
+ 'numberFeature': 1,
362
+ 'objectFeature': { enabled: true },
363
+ 'nullFeature': null,
364
+ 'undefinedFeature': undefined
365
+ },
366
+ getConfig: jest.fn(),
367
+ isEnabled: (key: string) => {
368
+ const value = mixedProvider.data[key];
369
+ if (value === null || value === undefined) return false;
370
+ return Boolean(value);
371
+ }
372
+ };
373
+
374
+ FeatureToggleBase.featureProvider = mixedProvider;
375
+
376
+ class TestClass {
377
+ @FeatureToggle('stringFeature') stringMethod() { return 'string'; }
378
+ @FeatureToggle('booleanFeature') booleanMethod() { return 'boolean'; }
379
+ @FeatureToggle('numberFeature') numberMethod() { return 'number'; }
380
+ @FeatureToggle('nullFeature') nullMethod() { return 'null'; }
381
+ }
382
+
383
+ const instance = new TestClass();
384
+
385
+ expect(instance.stringMethod()).toBe('string');
386
+ expect(instance.booleanMethod()).toBe('boolean');
387
+ expect(instance.numberMethod()).toBe('number');
388
+ expect(instance.nullMethod()).toBeUndefined(); // null should be falsy
389
+ });
390
+ });
391
+ });
@@ -0,0 +1,102 @@
1
+ import { Feature } from '../FeatureToggle';
2
+
3
+ describe('Feature Data Type', () => {
4
+ describe('Feature structure validation', () => {
5
+ it('should accept valid Feature objects', () => {
6
+ const validFeature: Feature = {
7
+ key: 'testFeature',
8
+ value: 'true',
9
+ activeAt: '2023-01-01T00:00:00Z',
10
+ disabledAt: '2023-12-31T23:59:59Z'
11
+ };
12
+
13
+ expect(validFeature.key).toBe('testFeature');
14
+ expect(validFeature.value).toBe('true');
15
+ expect(validFeature.activeAt).toBe('2023-01-01T00:00:00Z');
16
+ expect(validFeature.disabledAt).toBe('2023-12-31T23:59:59Z');
17
+ });
18
+
19
+ it('should accept Feature with empty date strings', () => {
20
+ const featureWithEmptyDates: Feature = {
21
+ key: 'testFeature',
22
+ value: 'false',
23
+ activeAt: '',
24
+ disabledAt: ''
25
+ };
26
+
27
+ expect(featureWithEmptyDates.activeAt).toBe('');
28
+ expect(featureWithEmptyDates.disabledAt).toBe('');
29
+ });
30
+
31
+ it('should accept boolean-like string values', () => {
32
+ const trueBooleanFeature: Feature = {
33
+ key: 'trueFeature',
34
+ value: 'true',
35
+ activeAt: '',
36
+ disabledAt: ''
37
+ };
38
+
39
+ const falseBooleanFeature: Feature = {
40
+ key: 'falseFeature',
41
+ value: 'false',
42
+ activeAt: '',
43
+ disabledAt: ''
44
+ };
45
+
46
+ expect(JSON.parse(trueBooleanFeature.value)).toBe(true);
47
+ expect(JSON.parse(falseBooleanFeature.value)).toBe(false);
48
+ });
49
+
50
+ it('should handle various date formats', () => {
51
+ const isoDateFeature: Feature = {
52
+ key: 'isoFeature',
53
+ value: 'true',
54
+ activeAt: '2023-01-01T00:00:00.000Z',
55
+ disabledAt: '2023-12-31T23:59:59.999Z'
56
+ };
57
+
58
+ const simpleDateFeature: Feature = {
59
+ key: 'simpleFeature',
60
+ value: 'true',
61
+ activeAt: '2023-01-01',
62
+ disabledAt: '2023-12-31'
63
+ };
64
+
65
+ expect(Date.parse(isoDateFeature.activeAt)).not.toBeNaN();
66
+ expect(Date.parse(isoDateFeature.disabledAt)).not.toBeNaN();
67
+ expect(Date.parse(simpleDateFeature.activeAt)).not.toBeNaN();
68
+ expect(Date.parse(simpleDateFeature.disabledAt)).not.toBeNaN();
69
+ });
70
+ });
71
+
72
+ describe('Feature collection structures', () => {
73
+ it('should handle Record<string, Feature> collections', () => {
74
+ const featureCollection: Record<string, Feature> = {
75
+ 'feature1': {
76
+ key: 'feature1',
77
+ value: 'true',
78
+ activeAt: '',
79
+ disabledAt: ''
80
+ },
81
+ 'feature2': {
82
+ key: 'feature2',
83
+ value: 'false',
84
+ activeAt: '2023-01-01',
85
+ disabledAt: '2023-12-31'
86
+ }
87
+ };
88
+
89
+ expect(Object.keys(featureCollection)).toHaveLength(2);
90
+ expect(featureCollection['feature1']).toBeDefined();
91
+ expect(featureCollection['feature2']).toBeDefined();
92
+ expect(featureCollection['nonexistent']).toBeUndefined();
93
+ });
94
+
95
+ it('should handle empty feature collections', () => {
96
+ const emptyCollection: Record<string, Feature> = {};
97
+
98
+ expect(Object.keys(emptyCollection)).toHaveLength(0);
99
+ expect(emptyCollection['anyKey']).toBeUndefined();
100
+ });
101
+ });
102
+ });