@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.
- package/.github/workflows/build.yml +1 -0
- package/CLAUDE.md +138 -0
- package/jest.config.js +13 -0
- package/logo.svg +1 -1
- package/package.json +7 -2
- package/src/test/decorator-behavior.spec.ts +359 -0
- package/src/test/error-handling.spec.ts +391 -0
- package/src/test/feature-data.spec.ts +102 -0
- package/src/test/integration.spec.ts +472 -0
- package/src/test/providers.spec.ts +218 -0
- package/src/test/test-boolean.json +1 -0
- package/src/test/test-feature.json +20 -0
- package/src/test/test-setup.ts +2 -0
- package/src/test/yaft.demo.spec.ts +53 -0
- package/tsconfig.json +1 -1
- package/tsconfig.spec.json +10 -0
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Project Overview
|
|
6
|
+
|
|
7
|
+
YaFT (Yet another Feature Toggle) is a TypeScript library that provides decorator-based feature toggles for classes and methods. It integrates with backend services (Go/PostgreSQL by default) or local data sources to enable/disable features at runtime.
|
|
8
|
+
|
|
9
|
+
## Development Commands
|
|
10
|
+
|
|
11
|
+
### Building
|
|
12
|
+
```bash
|
|
13
|
+
npm run build # Compile TypeScript and prepare package for publishing
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### Testing
|
|
17
|
+
```bash
|
|
18
|
+
npm test # Run Jest tests with ts-jest transform
|
|
19
|
+
npx jest # Alternative test command
|
|
20
|
+
npx jest --watch # Run tests in watch mode
|
|
21
|
+
npx jest --coverage # Run tests with coverage report
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Running Single Tests
|
|
25
|
+
```bash
|
|
26
|
+
npx jest yaft.spec.ts # Run specific test file
|
|
27
|
+
npx jest -t "test name" # Run specific test by name pattern
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Architecture
|
|
31
|
+
|
|
32
|
+
### Core Components
|
|
33
|
+
|
|
34
|
+
**FeatureToggle Decorator (`src/FeatureToggle.ts`)**
|
|
35
|
+
- Main decorator function that can be applied to classes or methods
|
|
36
|
+
- Evaluates feature state at decoration time using the configured provider
|
|
37
|
+
- Returns fallback implementations when features are disabled
|
|
38
|
+
|
|
39
|
+
**FeatureToggleBase Class**
|
|
40
|
+
- Static container for the global `featureProvider` instance
|
|
41
|
+
- Must be configured before using any `@FeatureToggle` decorators
|
|
42
|
+
|
|
43
|
+
**FeatureProvider Interface**
|
|
44
|
+
- Defines contract for data sources: `isEnabled(key: string): boolean`
|
|
45
|
+
- Optional API integration with `apiUrl`, `baseUUID`, and collection hash support
|
|
46
|
+
- Flexible data structure support via generic type parameter
|
|
47
|
+
|
|
48
|
+
### Provider Implementations
|
|
49
|
+
|
|
50
|
+
**API Providers** (`src/examples/ApiService*Provider.ts`)
|
|
51
|
+
- Integrate with YaFT Go backend via HTTP API
|
|
52
|
+
- Support collection hash for efficient cache invalidation
|
|
53
|
+
- Handle both boolean and Feature data types
|
|
54
|
+
- Include error handling for network failures
|
|
55
|
+
|
|
56
|
+
**Local Storage Providers** (`src/examples/LocalStorage*Provider.ts`)
|
|
57
|
+
- Load configuration from local JSON files using `require()`
|
|
58
|
+
- Support both boolean and Feature data types
|
|
59
|
+
- Fallback to empty configuration on file load errors
|
|
60
|
+
|
|
61
|
+
### Feature Data Model
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
type Feature = {
|
|
65
|
+
key: string; // Unique feature identifier
|
|
66
|
+
value: string; // Boolean value as string
|
|
67
|
+
activeAt: string; // ISO date when feature becomes active
|
|
68
|
+
disabledAt: string; // ISO date when feature gets disabled
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Features support time-based activation/deactivation logic evaluated at runtime.
|
|
73
|
+
|
|
74
|
+
### Decorator Behavior
|
|
75
|
+
|
|
76
|
+
**Method Decoration:**
|
|
77
|
+
- Replaces method implementation based on feature state
|
|
78
|
+
- Supports fallback method when feature is disabled
|
|
79
|
+
- Returns empty function if no fallback provided
|
|
80
|
+
|
|
81
|
+
**Class Decoration:**
|
|
82
|
+
- Replaces entire class constructor and prototype
|
|
83
|
+
- Supports fallback class when feature is disabled
|
|
84
|
+
- Creates EmptyClass with stub methods if no fallback provided
|
|
85
|
+
- Preserves async method signatures in EmptyClass stubs
|
|
86
|
+
|
|
87
|
+
## Usage Patterns
|
|
88
|
+
|
|
89
|
+
### Initialization
|
|
90
|
+
```typescript
|
|
91
|
+
import { FeatureToggleBase } from "@tehw0lf/yaft";
|
|
92
|
+
import { LocalStorageFeatureProvider } from "./examples/LocalStorageFeatureProvider";
|
|
93
|
+
|
|
94
|
+
FeatureToggleBase.featureProvider = new LocalStorageFeatureProvider("./config.json");
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Method Toggle
|
|
98
|
+
```typescript
|
|
99
|
+
@FeatureToggle("myFeature", fallbackMethod)
|
|
100
|
+
myMethod() {
|
|
101
|
+
// Implementation when feature is enabled
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Class Toggle
|
|
106
|
+
```typescript
|
|
107
|
+
@FeatureToggle("myFeature", FallbackClass)
|
|
108
|
+
class MyClass {
|
|
109
|
+
// Implementation when feature is enabled
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Testing Strategy
|
|
114
|
+
|
|
115
|
+
Tests use LocalStorageFeatureProvider with test configuration files (`src/test/test-*.json`). The test suite demonstrates:
|
|
116
|
+
- Class decoration with fallback classes
|
|
117
|
+
- Method decoration with and without fallbacks
|
|
118
|
+
- Async method handling
|
|
119
|
+
- Provider configuration and feature evaluation
|
|
120
|
+
|
|
121
|
+
## Build Configuration
|
|
122
|
+
|
|
123
|
+
- **TypeScript**: CommonJS output targeting ES2022
|
|
124
|
+
- **Decorators**: Experimental decorators enabled for `@FeatureToggle` syntax
|
|
125
|
+
- **Output**: Compiled to `dist/yaft/` with declaration files
|
|
126
|
+
- **Package**: Strips devDependencies during build process
|
|
127
|
+
|
|
128
|
+
## Dependencies
|
|
129
|
+
|
|
130
|
+
- **Runtime**: `reflect-metadata` for decorator metadata
|
|
131
|
+
- **Development**: TypeScript, Jest, ts-jest for testing
|
|
132
|
+
- **Optional**: `axios` for API provider implementations
|
|
133
|
+
|
|
134
|
+
## Pre-commit Validation
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
npm test && npm run build
|
|
138
|
+
```
|
package/jest.config.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** @type {import('ts-jest').JestConfigWithTsJest} **/
|
|
2
|
+
module.exports = {
|
|
3
|
+
displayName: 'yaft',
|
|
4
|
+
setupFilesAfterEnv: [
|
|
5
|
+
'<rootDir>/src/test/test-setup.ts'
|
|
6
|
+
],
|
|
7
|
+
globals: {},
|
|
8
|
+
coverageDirectory: 'coverage/yaft',
|
|
9
|
+
testEnvironment: "node",
|
|
10
|
+
transform: {
|
|
11
|
+
"^.+\.tsx?$": ["ts-jest",{}],
|
|
12
|
+
},
|
|
13
|
+
};
|
package/logo.svg
CHANGED
|
@@ -23,5 +23,5 @@
|
|
|
23
23
|
<!-- cross -->
|
|
24
24
|
<path d="M5,20 L20,5" stroke="red" stroke-width="4" fill="none" />
|
|
25
25
|
<!-- check -->
|
|
26
|
-
<path d="M40.8,20.8
|
|
26
|
+
<path d="M40.8,20.8 L30.8,10.8" stroke="green" stroke-width="4" fill="none" />
|
|
27
27
|
</svg>
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tehw0lf/yaft",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "YaFT - Feature Toggles using Go&PostgreSQL or any source!",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"build": "tsc && npm pkg delete devDependencies && cp package.json README.md logo.svg dist/"
|
|
7
|
+
"build": "tsc && npm pkg delete devDependencies && cp package.json README.md logo.svg dist/",
|
|
8
|
+
"test": "jest"
|
|
8
9
|
},
|
|
9
10
|
"repository": {
|
|
10
11
|
"type": "git",
|
|
@@ -26,8 +27,12 @@
|
|
|
26
27
|
"reflect-metadata": "^0.2.2"
|
|
27
28
|
},
|
|
28
29
|
"devDependencies": {
|
|
30
|
+
"@types/jest": "^29.5.14",
|
|
29
31
|
"@types/node": "^22.14.1",
|
|
30
32
|
"axios": "^1.8.4",
|
|
33
|
+
"jest": "^29.7.0",
|
|
34
|
+
"ts-jest": "^29.3.2",
|
|
35
|
+
"ts-node": "^10.9.2",
|
|
31
36
|
"typescript": "^4.2.3"
|
|
32
37
|
}
|
|
33
38
|
}
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { FeatureToggle, FeatureToggleBase, FeatureProvider } from '../FeatureToggle';
|
|
2
|
+
|
|
3
|
+
describe('FeatureToggle Decorator Behavior', () => {
|
|
4
|
+
let mockProvider: FeatureProvider<boolean>;
|
|
5
|
+
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
mockProvider = {
|
|
8
|
+
data: {},
|
|
9
|
+
getConfig: jest.fn(),
|
|
10
|
+
isEnabled: jest.fn()
|
|
11
|
+
};
|
|
12
|
+
FeatureToggleBase.featureProvider = mockProvider;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
jest.clearAllMocks();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('Method Decoration', () => {
|
|
20
|
+
it('should execute original method when feature is enabled', () => {
|
|
21
|
+
(mockProvider.isEnabled as jest.Mock).mockReturnValue(true);
|
|
22
|
+
let executionResult = '';
|
|
23
|
+
|
|
24
|
+
class TestClass {
|
|
25
|
+
@FeatureToggle('enabledFeature')
|
|
26
|
+
testMethod() {
|
|
27
|
+
executionResult = 'original method executed';
|
|
28
|
+
return 'original result';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const instance = new TestClass();
|
|
33
|
+
const result = instance.testMethod();
|
|
34
|
+
|
|
35
|
+
expect(executionResult).toBe('original method executed');
|
|
36
|
+
expect(result).toBe('original result');
|
|
37
|
+
expect(mockProvider.isEnabled).toHaveBeenCalledWith('enabledFeature');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should execute fallback method when feature is disabled', () => {
|
|
41
|
+
(mockProvider.isEnabled as jest.Mock).mockReturnValue(false);
|
|
42
|
+
let executionResult = '';
|
|
43
|
+
|
|
44
|
+
function fallbackMethod() {
|
|
45
|
+
executionResult = 'fallback method executed';
|
|
46
|
+
return 'fallback result';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
class TestClass {
|
|
50
|
+
@FeatureToggle('disabledFeature', fallbackMethod)
|
|
51
|
+
testMethod() {
|
|
52
|
+
executionResult = 'original method executed';
|
|
53
|
+
return 'original result';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const instance = new TestClass();
|
|
58
|
+
const result = instance.testMethod();
|
|
59
|
+
|
|
60
|
+
expect(executionResult).toBe('fallback method executed');
|
|
61
|
+
expect(result).toBe('fallback result');
|
|
62
|
+
expect(mockProvider.isEnabled).toHaveBeenCalledWith('disabledFeature');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should execute empty function when feature is disabled and no fallback provided', () => {
|
|
66
|
+
(mockProvider.isEnabled as jest.Mock).mockReturnValue(false);
|
|
67
|
+
let executionResult = '';
|
|
68
|
+
|
|
69
|
+
class TestClass {
|
|
70
|
+
@FeatureToggle('disabledFeature')
|
|
71
|
+
testMethod() {
|
|
72
|
+
executionResult = 'original method executed';
|
|
73
|
+
return 'original result';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const instance = new TestClass();
|
|
78
|
+
const result = instance.testMethod();
|
|
79
|
+
|
|
80
|
+
expect(executionResult).toBe('');
|
|
81
|
+
expect(result).toBeUndefined();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should preserve method context (this binding)', () => {
|
|
85
|
+
(mockProvider.isEnabled as jest.Mock).mockReturnValue(true);
|
|
86
|
+
|
|
87
|
+
class TestClass {
|
|
88
|
+
value = 'test value';
|
|
89
|
+
|
|
90
|
+
@FeatureToggle('enabledFeature')
|
|
91
|
+
getValue() {
|
|
92
|
+
return this.value;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const instance = new TestClass();
|
|
97
|
+
const result = instance.getValue();
|
|
98
|
+
|
|
99
|
+
expect(result).toBe('test value');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should handle method arguments correctly', () => {
|
|
103
|
+
(mockProvider.isEnabled as jest.Mock).mockReturnValue(true);
|
|
104
|
+
|
|
105
|
+
class TestClass {
|
|
106
|
+
@FeatureToggle('enabledFeature')
|
|
107
|
+
add(a: number, b: number) {
|
|
108
|
+
return a + b;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const instance = new TestClass();
|
|
113
|
+
const result = instance.add(5, 3);
|
|
114
|
+
|
|
115
|
+
expect(result).toBe(8);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should handle async methods', async () => {
|
|
119
|
+
(mockProvider.isEnabled as jest.Mock).mockReturnValue(true);
|
|
120
|
+
|
|
121
|
+
class TestClass {
|
|
122
|
+
@FeatureToggle('enabledFeature')
|
|
123
|
+
async asyncMethod() {
|
|
124
|
+
return Promise.resolve('async result');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const instance = new TestClass();
|
|
129
|
+
const result = await instance.asyncMethod();
|
|
130
|
+
|
|
131
|
+
expect(result).toBe('async result');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should handle async fallback methods', async () => {
|
|
135
|
+
(mockProvider.isEnabled as jest.Mock).mockReturnValue(false);
|
|
136
|
+
|
|
137
|
+
async function asyncFallback() {
|
|
138
|
+
return Promise.resolve('async fallback result');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
class TestClass {
|
|
142
|
+
@FeatureToggle('disabledFeature', asyncFallback)
|
|
143
|
+
async asyncMethod() {
|
|
144
|
+
return Promise.resolve('async original result');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const instance = new TestClass();
|
|
149
|
+
const result = await instance.asyncMethod();
|
|
150
|
+
|
|
151
|
+
expect(result).toBe('async fallback result');
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('Class Decoration', () => {
|
|
156
|
+
it('should use original class when feature is enabled', () => {
|
|
157
|
+
(mockProvider.isEnabled as jest.Mock).mockReturnValue(true);
|
|
158
|
+
|
|
159
|
+
@FeatureToggle('enabledFeature')
|
|
160
|
+
class TestClass {
|
|
161
|
+
value = 'original class';
|
|
162
|
+
|
|
163
|
+
getValue() {
|
|
164
|
+
return this.value;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const instance = new TestClass();
|
|
169
|
+
expect(instance.getValue()).toBe('original class');
|
|
170
|
+
expect(instance instanceof TestClass).toBe(true);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should use fallback class when feature is disabled', () => {
|
|
174
|
+
(mockProvider.isEnabled as jest.Mock).mockReturnValue(false);
|
|
175
|
+
|
|
176
|
+
class FallbackClass {
|
|
177
|
+
value = 'fallback class';
|
|
178
|
+
|
|
179
|
+
getValue() {
|
|
180
|
+
return this.value;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
@FeatureToggle('disabledFeature', FallbackClass)
|
|
185
|
+
class TestClass {
|
|
186
|
+
value = 'original class';
|
|
187
|
+
|
|
188
|
+
getValue() {
|
|
189
|
+
return this.value;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const instance = new TestClass();
|
|
194
|
+
expect(instance.getValue()).toBe('fallback class');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should use EmptyClass when feature is disabled and no fallback provided', () => {
|
|
198
|
+
(mockProvider.isEnabled as jest.Mock).mockReturnValue(false);
|
|
199
|
+
|
|
200
|
+
@FeatureToggle('disabledFeature')
|
|
201
|
+
class TestClass {
|
|
202
|
+
value = 'original class';
|
|
203
|
+
|
|
204
|
+
getValue() {
|
|
205
|
+
return this.value;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async getAsyncValue() {
|
|
209
|
+
return Promise.resolve(this.value);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const instance = new TestClass();
|
|
214
|
+
|
|
215
|
+
// Should have empty methods
|
|
216
|
+
expect(instance.getValue()).toBeUndefined();
|
|
217
|
+
|
|
218
|
+
// Should handle async methods
|
|
219
|
+
expect(instance.getAsyncValue()).toBeInstanceOf(Promise);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should preserve constructor arguments in fallback class', () => {
|
|
223
|
+
(mockProvider.isEnabled as jest.Mock).mockReturnValue(false);
|
|
224
|
+
|
|
225
|
+
class FallbackClass {
|
|
226
|
+
constructor(public name: string, public age: number) {}
|
|
227
|
+
|
|
228
|
+
getInfo() {
|
|
229
|
+
return `${this.name} is ${this.age}`;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
@FeatureToggle('disabledFeature', FallbackClass)
|
|
234
|
+
class TestClass {
|
|
235
|
+
constructor(public name: string, public age: number) {}
|
|
236
|
+
|
|
237
|
+
getInfo() {
|
|
238
|
+
return `Original: ${this.name} is ${this.age}`;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const instance = new TestClass('John', 30);
|
|
243
|
+
expect(instance.getInfo()).toBe('John is 30');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('should handle inheritance in fallback classes', () => {
|
|
247
|
+
(mockProvider.isEnabled as jest.Mock).mockReturnValue(false);
|
|
248
|
+
|
|
249
|
+
class BaseClass {
|
|
250
|
+
baseMethod() {
|
|
251
|
+
return 'base method';
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
class FallbackClass extends BaseClass {
|
|
256
|
+
fallbackMethod() {
|
|
257
|
+
return 'fallback method';
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
@FeatureToggle('disabledFeature', FallbackClass)
|
|
262
|
+
class TestClass {
|
|
263
|
+
originalMethod() {
|
|
264
|
+
return 'original method';
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const instance = new TestClass();
|
|
269
|
+
expect((instance as any).baseMethod()).toBe('base method');
|
|
270
|
+
expect((instance as any).fallbackMethod()).toBe('fallback method');
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
describe('Edge Cases and Error Conditions', () => {
|
|
275
|
+
it('should throw error when FeatureToggleProvider is not set', () => {
|
|
276
|
+
FeatureToggleBase.featureProvider = null as any;
|
|
277
|
+
|
|
278
|
+
expect(() => {
|
|
279
|
+
class TestClass {
|
|
280
|
+
@FeatureToggle('anyFeature')
|
|
281
|
+
testMethod() {}
|
|
282
|
+
}
|
|
283
|
+
}).toThrow('FeatureToggleProvider not set');
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('should handle provider throwing errors gracefully', () => {
|
|
287
|
+
(mockProvider.isEnabled as jest.Mock).mockImplementation(() => {
|
|
288
|
+
throw new Error('Provider error');
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
expect(() => {
|
|
292
|
+
class TestClass {
|
|
293
|
+
@FeatureToggle('errorFeature')
|
|
294
|
+
testMethod() {}
|
|
295
|
+
}
|
|
296
|
+
}).toThrow('Provider error');
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('should handle undefined/null feature keys', () => {
|
|
300
|
+
(mockProvider.isEnabled as jest.Mock).mockReturnValue(false);
|
|
301
|
+
|
|
302
|
+
class TestClass {
|
|
303
|
+
@FeatureToggle(undefined as any)
|
|
304
|
+
testMethod() {
|
|
305
|
+
return 'should not execute';
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const instance = new TestClass();
|
|
310
|
+
const result = instance.testMethod();
|
|
311
|
+
|
|
312
|
+
expect(result).toBeUndefined();
|
|
313
|
+
expect(mockProvider.isEnabled).toHaveBeenCalledWith(undefined);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('should handle empty string feature keys', () => {
|
|
317
|
+
(mockProvider.isEnabled as jest.Mock).mockReturnValue(false);
|
|
318
|
+
|
|
319
|
+
class TestClass {
|
|
320
|
+
@FeatureToggle('')
|
|
321
|
+
testMethod() {
|
|
322
|
+
return 'should not execute';
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const instance = new TestClass();
|
|
327
|
+
const result = instance.testMethod();
|
|
328
|
+
|
|
329
|
+
expect(result).toBeUndefined();
|
|
330
|
+
expect(mockProvider.isEnabled).toHaveBeenCalledWith('');
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
describe('Multiple Decorators on Same Class/Method', () => {
|
|
335
|
+
it('should handle multiple method decorators', () => {
|
|
336
|
+
(mockProvider.isEnabled as jest.Mock)
|
|
337
|
+
.mockReturnValueOnce(true) // First feature enabled
|
|
338
|
+
.mockReturnValueOnce(false); // Second feature disabled
|
|
339
|
+
|
|
340
|
+
function fallback() {
|
|
341
|
+
return 'fallback executed';
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
class TestClass {
|
|
345
|
+
@FeatureToggle('feature1')
|
|
346
|
+
@FeatureToggle('feature2', fallback)
|
|
347
|
+
testMethod() {
|
|
348
|
+
return 'original executed';
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const instance = new TestClass();
|
|
353
|
+
const result = instance.testMethod();
|
|
354
|
+
|
|
355
|
+
// The behavior depends on decorator execution order
|
|
356
|
+
expect(mockProvider.isEnabled).toHaveBeenCalledTimes(2);
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
});
|