@tehw0lf/yaft 0.0.2 → 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,472 @@
1
+ import { FeatureToggle, FeatureToggleBase, Feature } from '../FeatureToggle';
2
+ import { LocalStorageFeatureProvider } from '../examples/LocalStorageFeatureProvider';
3
+ import { LocalStorageBooleanProvider } from '../examples/LocalStorageBooleanProvider';
4
+
5
+ describe('Integration Tests', () => {
6
+ describe('Real-world Usage Scenarios', () => {
7
+ describe('E-commerce Feature Toggles', () => {
8
+ beforeEach(() => {
9
+ // Mock feature configuration for e-commerce scenarios
10
+ const ecommerceFeatures: Record<string, Feature> = {
11
+ 'checkout_v2': {
12
+ key: 'checkout_v2',
13
+ value: 'true',
14
+ activeAt: new Date(Date.now() - 86400000).toISOString(), // 1 day ago
15
+ disabledAt: new Date(Date.now() + 86400000).toISOString() // 1 day from now
16
+ },
17
+ 'premium_features': {
18
+ key: 'premium_features',
19
+ value: 'false',
20
+ activeAt: '',
21
+ disabledAt: ''
22
+ },
23
+ 'beta_ui': {
24
+ key: 'beta_ui',
25
+ value: 'true',
26
+ activeAt: new Date(Date.now() - 86400000).toISOString(), // 1 day ago
27
+ disabledAt: new Date(Date.now() + 86400000).toISOString() // 1 day from now
28
+ }
29
+ };
30
+
31
+ const provider = new (class extends LocalStorageFeatureProvider {
32
+ constructor() {
33
+ super('');
34
+ this.data = ecommerceFeatures;
35
+ }
36
+ })();
37
+
38
+ FeatureToggleBase.featureProvider = provider;
39
+ });
40
+
41
+ it('should handle checkout flow feature toggle', () => {
42
+ class LegacyCheckout {
43
+ process(items: string[]) {
44
+ return `Legacy checkout processing ${items.length} items`;
45
+ }
46
+ }
47
+
48
+ class NewCheckout {
49
+ process(items: string[]) {
50
+ return `New checkout v2 processing ${items.length} items`;
51
+ }
52
+ }
53
+
54
+ @FeatureToggle('checkout_v2', LegacyCheckout)
55
+ class CheckoutService {
56
+ process(items: string[]) {
57
+ return `Enhanced checkout processing ${items.length} items`;
58
+ }
59
+ }
60
+
61
+ const checkout = new CheckoutService();
62
+ const result = checkout.process(['item1', 'item2']);
63
+
64
+ expect(result).toBe('Enhanced checkout processing 2 items');
65
+ });
66
+
67
+ it('should handle premium features with method-level toggles', () => {
68
+ function basicRecommendations() {
69
+ return ['Popular item 1', 'Popular item 2'];
70
+ }
71
+
72
+ class RecommendationService {
73
+ @FeatureToggle('premium_features', basicRecommendations)
74
+ getPersonalizedRecommendations(userId: string) {
75
+ return [`AI recommendation for ${userId}`, `Premium suggestion for ${userId}`];
76
+ }
77
+
78
+ getBasicRecommendations() {
79
+ return ['Basic item 1', 'Basic item 2'];
80
+ }
81
+ }
82
+
83
+ const service = new RecommendationService();
84
+ const recommendations = service.getPersonalizedRecommendations('user123');
85
+
86
+ expect(recommendations).toEqual(['Popular item 1', 'Popular item 2']);
87
+ });
88
+
89
+ it('should handle UI feature toggles', () => {
90
+ class StandardUI {
91
+ render() {
92
+ return '<div>Standard UI</div>';
93
+ }
94
+ }
95
+
96
+ @FeatureToggle('beta_ui', StandardUI)
97
+ class BetaUI {
98
+ render() {
99
+ return '<div>Beta UI with new features</div>';
100
+ }
101
+ }
102
+
103
+ const ui = new BetaUI();
104
+ const rendered = ui.render();
105
+
106
+ expect(rendered).toBe('<div>Beta UI with new features</div>');
107
+ });
108
+ });
109
+
110
+ describe('Microservice Feature Coordination', () => {
111
+ it('should handle multiple services with coordinated feature toggles', () => {
112
+ const serviceFeatures = {
113
+ 'service_a_feature': true,
114
+ 'service_b_feature': false,
115
+ 'shared_feature': true
116
+ };
117
+
118
+ const provider = new (class extends LocalStorageBooleanProvider {
119
+ constructor() {
120
+ super('');
121
+ this.data = serviceFeatures;
122
+ }
123
+ })();
124
+
125
+ FeatureToggleBase.featureProvider = provider;
126
+
127
+ class ServiceA {
128
+ @FeatureToggle('service_a_feature')
129
+ processData(data: any) {
130
+ return `ServiceA: Enhanced processing of ${JSON.stringify(data)}`;
131
+ }
132
+
133
+ @FeatureToggle('shared_feature')
134
+ sharedFunction() {
135
+ return 'ServiceA: Shared feature active';
136
+ }
137
+ }
138
+
139
+ class ServiceB {
140
+ @FeatureToggle('service_b_feature')
141
+ transformData(data: any) {
142
+ return `ServiceB: New transformation of ${JSON.stringify(data)}`;
143
+ }
144
+
145
+ @FeatureToggle('shared_feature')
146
+ sharedFunction() {
147
+ return 'ServiceB: Shared feature active';
148
+ }
149
+ }
150
+
151
+ const serviceA = new ServiceA();
152
+ const serviceB = new ServiceB();
153
+
154
+ expect(serviceA.processData({ test: true })).toBe('ServiceA: Enhanced processing of {"test":true}');
155
+ expect(serviceB.transformData({ test: true })).toBeUndefined(); // Feature disabled
156
+ expect(serviceA.sharedFunction()).toBe('ServiceA: Shared feature active');
157
+ expect(serviceB.sharedFunction()).toBe('ServiceB: Shared feature active');
158
+ });
159
+ });
160
+
161
+ describe('A/B Testing Scenarios', () => {
162
+ it('should support A/B testing with multiple variants', () => {
163
+ // This test demonstrates A/B testing where the feature evaluation
164
+ // happens at class definition time, not method call time
165
+
166
+ function variantA() {
167
+ return 'Algorithm A result';
168
+ }
169
+
170
+ function variantB() {
171
+ return 'Algorithm B result';
172
+ }
173
+
174
+ const abTestFeatures = {
175
+ 'algorithm_variant_a': false,
176
+ 'algorithm_variant_b': true
177
+ };
178
+
179
+ const provider = new (class extends LocalStorageBooleanProvider {
180
+ constructor() {
181
+ super('');
182
+ this.data = abTestFeatures;
183
+ }
184
+ })();
185
+
186
+ // Set provider BEFORE defining the class (critical for decorator evaluation)
187
+ FeatureToggleBase.featureProvider = provider;
188
+
189
+ // Define class with enabled variant
190
+ class AlgorithmB {
191
+ @FeatureToggle('algorithm_variant_b', variantB)
192
+ calculate(input: number) {
193
+ return 'Default algorithm result';
194
+ }
195
+ }
196
+
197
+ // Define class with disabled variant
198
+ class AlgorithmA {
199
+ @FeatureToggle('algorithm_variant_a', variantA)
200
+ calculate(input: number) {
201
+ return 'Default algorithm result';
202
+ }
203
+ }
204
+
205
+ const algorithmB = new AlgorithmB();
206
+ const algorithmA = new AlgorithmA();
207
+
208
+ const resultB = algorithmB.calculate(42);
209
+ const resultA = algorithmA.calculate(42);
210
+
211
+ // Enabled variant should use original method (returns "Default algorithm result")
212
+ // Disabled variant should use fallback method
213
+ expect(resultB).toBe('Default algorithm result'); // enabled, uses original
214
+ expect(resultA).toBe('Algorithm A result'); // disabled, uses fallback
215
+ });
216
+ });
217
+ });
218
+
219
+ describe('Performance and Load Testing', () => {
220
+ it('should handle high-frequency feature toggle evaluations', () => {
221
+ const provider = new (class extends LocalStorageBooleanProvider {
222
+ constructor() {
223
+ super('');
224
+ this.data = { 'myToggle': true };
225
+ }
226
+ })();
227
+ FeatureToggleBase.featureProvider = provider;
228
+
229
+ class HighFrequencyService {
230
+ @FeatureToggle('myToggle')
231
+ processItem(item: any) {
232
+ return `Processed: ${item}`;
233
+ }
234
+ }
235
+
236
+ const service = new HighFrequencyService();
237
+ const items = Array.from({ length: 1000 }, (_, i) => `item${i}`);
238
+
239
+ const start = performance.now();
240
+ const results = items.map(item => service.processItem(item));
241
+ const end = performance.now();
242
+
243
+ expect(results).toHaveLength(1000);
244
+ expect(results[0]).toBe('Processed: item0');
245
+ expect(results[999]).toBe('Processed: item999');
246
+
247
+ // Performance check - should complete within reasonable time
248
+ expect(end - start).toBeLessThan(100); // 100ms for 1000 operations
249
+ });
250
+
251
+ it('should handle concurrent access to feature toggles', async () => {
252
+ const provider = new (class extends LocalStorageBooleanProvider {
253
+ constructor() {
254
+ super('');
255
+ this.data = { 'myToggle': true };
256
+ }
257
+ })();
258
+ FeatureToggleBase.featureProvider = provider;
259
+
260
+ class ConcurrentService {
261
+ @FeatureToggle('myToggle')
262
+ async asyncProcess(id: number) {
263
+ await new Promise(resolve => setTimeout(resolve, 1));
264
+ return `Async result ${id}`;
265
+ }
266
+ }
267
+
268
+ const service = new ConcurrentService();
269
+ const promises = Array.from({ length: 50 }, (_, i) => service.asyncProcess(i));
270
+
271
+ const results = await Promise.all(promises);
272
+
273
+ expect(results).toHaveLength(50);
274
+ expect(results[0]).toBe('Async result 0');
275
+ expect(results[49]).toBe('Async result 49');
276
+ });
277
+ });
278
+
279
+ describe('Cross-Provider Compatibility', () => {
280
+ it('should work consistently across different provider types', () => {
281
+ // Test with Feature provider using mock data that enables myMethodToggle
282
+ const featureProvider = new (class extends LocalStorageFeatureProvider {
283
+ constructor() {
284
+ super('');
285
+ this.data = {
286
+ 'myMethodToggle': {
287
+ key: 'myMethodToggle',
288
+ value: 'true',
289
+ activeAt: new Date(Date.now() - 86400000).toISOString(),
290
+ disabledAt: new Date(Date.now() + 86400000).toISOString()
291
+ }
292
+ };
293
+ }
294
+ })();
295
+ FeatureToggleBase.featureProvider = featureProvider;
296
+
297
+ class TestService {
298
+ @FeatureToggle('myMethodToggle')
299
+ testMethod() {
300
+ return 'feature provider result';
301
+ }
302
+ }
303
+
304
+ let service = new TestService();
305
+ let result = service.testMethod();
306
+ expect(result).toBe('feature provider result');
307
+
308
+ // Switch to Boolean provider
309
+ const booleanProvider = new (class extends LocalStorageBooleanProvider {
310
+ constructor() {
311
+ super('');
312
+ this.data = { 'myToggle': true };
313
+ }
314
+ })();
315
+ FeatureToggleBase.featureProvider = booleanProvider;
316
+
317
+ // Create new class instance to trigger re-evaluation
318
+ class TestService2 {
319
+ @FeatureToggle('myToggle')
320
+ testMethod() {
321
+ return 'boolean provider result';
322
+ }
323
+ }
324
+
325
+ service = new TestService2();
326
+ result = service.testMethod();
327
+ expect(result).toBe('boolean provider result');
328
+ });
329
+ });
330
+
331
+ describe('Real-world Integration Patterns', () => {
332
+ it('should support factory pattern with feature toggles', () => {
333
+ const factoryFeatures = {
334
+ 'use_redis_cache': true,
335
+ 'use_memory_cache': false
336
+ };
337
+
338
+ const provider = new (class extends LocalStorageBooleanProvider {
339
+ constructor() {
340
+ super('');
341
+ this.data = factoryFeatures;
342
+ }
343
+ })();
344
+
345
+ FeatureToggleBase.featureProvider = provider;
346
+
347
+ class MemoryCache {
348
+ get(key: string) { return `memory:${key}`; }
349
+ }
350
+
351
+ class RedisCache {
352
+ get(key: string) { return `redis:${key}`; }
353
+ }
354
+
355
+ class CacheFactory {
356
+ @FeatureToggle('use_redis_cache', () => new MemoryCache())
357
+ createCache() {
358
+ return new RedisCache();
359
+ }
360
+ }
361
+
362
+ const factory = new CacheFactory();
363
+ const cache = factory.createCache();
364
+
365
+ expect(cache).toBeInstanceOf(RedisCache);
366
+ expect(cache.get('test')).toBe('redis:test');
367
+ });
368
+
369
+ it('should support middleware pattern with feature toggles', () => {
370
+ const middlewareFeatures = {
371
+ 'auth_middleware': true,
372
+ 'logging_middleware': false,
373
+ 'rate_limit_middleware': true
374
+ };
375
+
376
+ const provider = new (class extends LocalStorageBooleanProvider {
377
+ constructor() {
378
+ super('');
379
+ this.data = middlewareFeatures;
380
+ }
381
+ })();
382
+
383
+ FeatureToggleBase.featureProvider = provider;
384
+
385
+ class RequestProcessor {
386
+ private logs: string[] = [];
387
+
388
+ @FeatureToggle('auth_middleware')
389
+ authenticate(request: any) {
390
+ this.logs.push('auth executed');
391
+ return { ...request, authenticated: true };
392
+ }
393
+
394
+ @FeatureToggle('logging_middleware')
395
+ log(request: any) {
396
+ this.logs.push('logging executed');
397
+ return request;
398
+ }
399
+
400
+ @FeatureToggle('rate_limit_middleware')
401
+ rateLimit(request: any) {
402
+ this.logs.push('rate limit executed');
403
+ return request;
404
+ }
405
+
406
+ process(request: any) {
407
+ let processedRequest = request;
408
+ processedRequest = this.authenticate(processedRequest) || processedRequest;
409
+ processedRequest = this.log(processedRequest) || processedRequest;
410
+ processedRequest = this.rateLimit(processedRequest) || processedRequest;
411
+ return processedRequest;
412
+ }
413
+
414
+ getLogs() { return this.logs; }
415
+ }
416
+
417
+ const processor = new RequestProcessor();
418
+ const result = processor.process({ id: 1 });
419
+
420
+ expect(result.authenticated).toBe(true);
421
+ expect(processor.getLogs()).toEqual(['auth executed', 'rate limit executed']);
422
+ });
423
+
424
+ it('should support plugin architecture with feature toggles', () => {
425
+ const pluginFeatures = {
426
+ 'analytics_plugin': false,
427
+ 'social_sharing_plugin': true,
428
+ 'comments_plugin': true
429
+ };
430
+
431
+ const provider = new (class extends LocalStorageBooleanProvider {
432
+ constructor() {
433
+ super('');
434
+ this.data = pluginFeatures;
435
+ }
436
+ })();
437
+
438
+ FeatureToggleBase.featureProvider = provider;
439
+
440
+ class PluginManager {
441
+ private activePlugins: string[] = [];
442
+
443
+ @FeatureToggle('analytics_plugin')
444
+ loadAnalytics() {
445
+ this.activePlugins.push('analytics');
446
+ }
447
+
448
+ @FeatureToggle('social_sharing_plugin')
449
+ loadSocialSharing() {
450
+ this.activePlugins.push('social-sharing');
451
+ }
452
+
453
+ @FeatureToggle('comments_plugin')
454
+ loadComments() {
455
+ this.activePlugins.push('comments');
456
+ }
457
+
458
+ initialize() {
459
+ this.loadAnalytics();
460
+ this.loadSocialSharing();
461
+ this.loadComments();
462
+ return this.activePlugins;
463
+ }
464
+ }
465
+
466
+ const manager = new PluginManager();
467
+ const activePlugins = manager.initialize();
468
+
469
+ expect(activePlugins).toEqual(['social-sharing', 'comments']);
470
+ });
471
+ });
472
+ });
@@ -0,0 +1,218 @@
1
+ import { Feature, FeatureProvider } from '../FeatureToggle';
2
+ import { LocalStorageFeatureProvider } from '../examples/LocalStorageFeatureProvider';
3
+ import { LocalStorageBooleanProvider } from '../examples/LocalStorageBooleanProvider';
4
+
5
+ describe('Feature Providers', () => {
6
+ describe('LocalStorageFeatureProvider', () => {
7
+ let provider: LocalStorageFeatureProvider;
8
+
9
+ beforeEach(() => {
10
+ provider = new (class extends LocalStorageFeatureProvider {
11
+ constructor() {
12
+ super('');
13
+ this.data = {
14
+ 'myMethodToggle': {
15
+ key: 'myMethodToggle',
16
+ value: 'true',
17
+ activeAt: '',
18
+ disabledAt: ''
19
+ },
20
+ 'myClassToggle': {
21
+ key: 'myClassToggle',
22
+ value: 'false',
23
+ activeAt: '',
24
+ disabledAt: ''
25
+ },
26
+ 'myOtherMethodToggle': {
27
+ key: 'myOtherMethodToggle',
28
+ value: 'false',
29
+ activeAt: '',
30
+ disabledAt: ''
31
+ }
32
+ };
33
+ }
34
+ })();
35
+ });
36
+
37
+ it('should load configuration from file on construction', () => {
38
+ expect(provider.data).toBeDefined();
39
+ expect(typeof provider.data).toBe('object');
40
+ });
41
+
42
+ it('should return correct isEnabled for existing features', () => {
43
+ expect(provider.isEnabled('myMethodToggle')).toBe(true);
44
+ expect(provider.isEnabled('myClassToggle')).toBe(false);
45
+ expect(provider.isEnabled('myOtherMethodToggle')).toBe(false);
46
+ });
47
+
48
+ it('should return false for non-existent features', () => {
49
+ expect(provider.isEnabled('nonExistentFeature')).toBe(false);
50
+ expect(provider.isEnabled('')).toBe(false);
51
+ });
52
+
53
+ it('should handle null/undefined features', () => {
54
+ const mockProvider = new LocalStorageFeatureProvider('./non-existent-file.json');
55
+ expect(mockProvider.isEnabled('anyKey')).toBe(false);
56
+ });
57
+
58
+ describe('time-based feature evaluation', () => {
59
+ let timeProvider: LocalStorageFeatureProvider;
60
+
61
+ beforeEach(() => {
62
+ const pastDate = new Date(Date.now() - 86400000).toISOString(); // 1 day ago
63
+ const futureDate = new Date(Date.now() + 86400000).toISOString(); // 1 day from now
64
+
65
+ // Mock require to return time-based features
66
+ const originalRequire = require;
67
+ jest.doMock('./time-test-features.json', () => ({
68
+ 'activeFeature': {
69
+ key: 'activeFeature',
70
+ value: 'true',
71
+ activeAt: pastDate,
72
+ disabledAt: futureDate
73
+ },
74
+ 'disabledFeature': {
75
+ key: 'disabledFeature',
76
+ value: 'true',
77
+ activeAt: pastDate,
78
+ disabledAt: pastDate // Already disabled
79
+ },
80
+ 'futureFeature': {
81
+ key: 'futureFeature',
82
+ value: 'true',
83
+ activeAt: futureDate, // Not yet active
84
+ disabledAt: ''
85
+ }
86
+ }), { virtual: true });
87
+
88
+ timeProvider = new LocalStorageFeatureProvider('./time-test-features.json');
89
+ });
90
+
91
+ afterEach(() => {
92
+ jest.clearAllMocks();
93
+ });
94
+
95
+ it('should respect activeAt timestamps', () => {
96
+ // This test would need actual time-based test data
97
+ // For now, test the logic with the existing test data
98
+ expect(timeProvider.isEnabled).toBeDefined();
99
+ });
100
+
101
+ it('should respect disabledAt timestamps', () => {
102
+ // This test would need actual time-based test data
103
+ expect(timeProvider.isEnabled).toBeDefined();
104
+ });
105
+ });
106
+ });
107
+
108
+ describe('LocalStorageBooleanProvider', () => {
109
+ let provider: LocalStorageBooleanProvider;
110
+
111
+ beforeEach(() => {
112
+ provider = new (class extends LocalStorageBooleanProvider {
113
+ constructor() {
114
+ super('');
115
+ this.data = { 'myToggle': true };
116
+ }
117
+ })();
118
+ });
119
+
120
+ it('should load boolean configuration from file', () => {
121
+ expect(provider.data).toBeDefined();
122
+ expect(typeof provider.data).toBe('object');
123
+ });
124
+
125
+ it('should return correct boolean values', () => {
126
+ expect(provider.isEnabled('myToggle')).toBe(true);
127
+ });
128
+
129
+ it('should return false for non-existent boolean features', () => {
130
+ expect(provider.isEnabled('nonExistentToggle')).toBe(false);
131
+ });
132
+
133
+ it('should handle various boolean representations', () => {
134
+ // Test with mock data containing different boolean formats
135
+ const mockProvider = new (class extends LocalStorageBooleanProvider {
136
+ constructor() {
137
+ super('');
138
+ this.data = {
139
+ 'trueString': true,
140
+ 'falseString': false,
141
+ 'trueBoolean': true,
142
+ 'falseBoolean': false,
143
+ 'truthy': true,
144
+ 'falsy': false
145
+ };
146
+ }
147
+ })();
148
+
149
+ expect(mockProvider.isEnabled('trueBoolean')).toBe(true);
150
+ expect(mockProvider.isEnabled('falseBoolean')).toBe(false);
151
+ });
152
+ });
153
+
154
+ describe('FeatureProvider Interface', () => {
155
+ it('should define correct interface structure', () => {
156
+ class TestProvider implements FeatureProvider<boolean> {
157
+ data: Record<string, boolean> = {};
158
+
159
+ getConfig(configPathOrUrl: string): void {
160
+ this.data = { test: true };
161
+ }
162
+
163
+ isEnabled(key: string): boolean {
164
+ return this.data[key] || false;
165
+ }
166
+ }
167
+
168
+ const provider = new TestProvider();
169
+ provider.getConfig('test-path');
170
+
171
+ expect(provider.isEnabled('test')).toBe(true);
172
+ expect(provider.isEnabled('missing')).toBe(false);
173
+ });
174
+
175
+ it('should support optional properties', () => {
176
+ class TestProviderWithOptional implements FeatureProvider<Feature> {
177
+ apiUrl = 'https://api.example.com';
178
+ baseUUID = 'test-uuid';
179
+ data: Record<string, Feature> = {};
180
+
181
+ getConfig(configPathOrUrl: string): void {}
182
+
183
+ getCollectionHash(configPathOrUrl: string): void {}
184
+
185
+ isEnabled(key: string): boolean {
186
+ return false;
187
+ }
188
+ }
189
+
190
+ const provider = new TestProviderWithOptional();
191
+ expect(provider.apiUrl).toBe('https://api.example.com');
192
+ expect(provider.baseUUID).toBe('test-uuid');
193
+ expect(provider.getCollectionHash).toBeDefined();
194
+ });
195
+ });
196
+
197
+ describe('Provider Error Handling', () => {
198
+ it('should handle file not found gracefully', () => {
199
+ const provider = new LocalStorageFeatureProvider('./non-existent-file.json');
200
+ expect(provider.data).toEqual({});
201
+ expect(provider.isEnabled('anyKey')).toBe(false);
202
+ });
203
+
204
+ it('should handle invalid JSON gracefully', () => {
205
+ // Mock require to throw error
206
+ const originalConsoleError = console.error;
207
+ console.error = jest.fn();
208
+
209
+ try {
210
+ const provider = new LocalStorageFeatureProvider('./invalid-json-file.json');
211
+ expect(provider.data).toEqual({});
212
+ expect(console.error).toHaveBeenCalled();
213
+ } finally {
214
+ console.error = originalConsoleError;
215
+ }
216
+ });
217
+ });
218
+ });
@@ -0,0 +1 @@
1
+ { "myToggle": true }