@featbit/openfeature-provider-node-server 1.0.0
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/publish-npm.yml +24 -0
- package/LICENSE +21 -0
- package/README.md +60 -0
- package/examples/console-app/README.md +25 -0
- package/examples/console-app/package-lock.json +839 -0
- package/examples/console-app/package.json +21 -0
- package/examples/console-app/src/commonjs.cjs +30 -0
- package/examples/console-app/src/esm.ts +34 -0
- package/jest.config.js +7 -0
- package/package.json +36 -0
- package/src/FbProvider.ts +203 -0
- package/src/SafeLogger.ts +67 -0
- package/src/index.ts +6 -0
- package/src/translateContext.ts +40 -0
- package/src/translateResult.ts +42 -0
- package/tests/FbProvider.test.ts +294 -0
- package/tests/SafeLogger.test.ts +36 -0
- package/tests/TestLogger.ts +26 -0
- package/tests/translateContext.test.ts +102 -0
- package/tests/translateResult.test.ts +51 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { FbProvider } from "../src";
|
|
2
|
+
import { Client, ErrorCode, OpenFeature, ProviderEvents, ProviderStatus } from "@openfeature/server-sdk";
|
|
3
|
+
import { IClientContext, IFbClient, integrations, ReasonKinds, IFallthrough, IStore } from "@featbit/node-server-sdk";
|
|
4
|
+
import { translateContext } from "../src/translateContext";
|
|
5
|
+
|
|
6
|
+
it('can be initialized', async () => {
|
|
7
|
+
const logger = new integrations.TestLogger();
|
|
8
|
+
const provider = new FbProvider({ sdkKey: 'sdk-key', offline: true, logger });
|
|
9
|
+
await provider.initialize({});
|
|
10
|
+
|
|
11
|
+
expect(provider.status).toEqual(ProviderStatus.READY);
|
|
12
|
+
expect(logger.logs.length).toEqual(2);
|
|
13
|
+
expect(logger.logs).toEqual(['Offline mode enabled. No data synchronization with the FeatBit server will occur.', 'FbClient started successfully.']);
|
|
14
|
+
await provider.onClose();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('can fail to initialize client', async () => {
|
|
18
|
+
const logger = new integrations.TestLogger();
|
|
19
|
+
const provider = new FbProvider({
|
|
20
|
+
sdkKey: 'sdk-key',
|
|
21
|
+
logger,
|
|
22
|
+
streamingUri: 'ws://localhost:6001',
|
|
23
|
+
eventsUri: 'http://localhost:6001',
|
|
24
|
+
dataSynchronizer: (
|
|
25
|
+
clientContext: IClientContext,
|
|
26
|
+
store: IStore,
|
|
27
|
+
dataSourceUpdates: any,
|
|
28
|
+
initSuccessHandler: VoidFunction,
|
|
29
|
+
errorHandler?: (e: Error) => void,
|
|
30
|
+
) => ({
|
|
31
|
+
start: () => {
|
|
32
|
+
setTimeout(() => errorHandler?.({ code: 401 } as any), 20);
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
});
|
|
36
|
+
try {
|
|
37
|
+
await provider.initialize({});
|
|
38
|
+
} catch (e) {
|
|
39
|
+
expect((e as Error).message).toEqual('Authentication failed. Double check your SDK key.');
|
|
40
|
+
}
|
|
41
|
+
expect(provider.status).toEqual(ProviderStatus.ERROR);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('emits events for flag changes', async () => {
|
|
45
|
+
const logger = new integrations.TestLogger();
|
|
46
|
+
const td = new integrations.TestData();
|
|
47
|
+
const provider = new FbProvider( {
|
|
48
|
+
sdkKey: 'sdk-key',
|
|
49
|
+
logger,
|
|
50
|
+
dataSynchronizer: td.getFactory(),
|
|
51
|
+
});
|
|
52
|
+
let count = 0;
|
|
53
|
+
provider.events.addHandler(ProviderEvents.ConfigurationChanged, (eventDetail) => {
|
|
54
|
+
expect(eventDetail?.flagsChanged).toEqual(['flagA']);
|
|
55
|
+
count += 1;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const fallthrough: IFallthrough = {
|
|
59
|
+
dispatchKey: "keyId",
|
|
60
|
+
includedInExpt: true,
|
|
61
|
+
variations: [
|
|
62
|
+
{
|
|
63
|
+
id: "trueId",
|
|
64
|
+
exptRollout: 1,
|
|
65
|
+
rollout: [0, 1]
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const flag = new integrations.FlagBuilder()
|
|
71
|
+
.key('flagA')
|
|
72
|
+
.isEnabled(true)
|
|
73
|
+
.disabledVariationId('falseId')
|
|
74
|
+
.fallthrough(fallthrough)
|
|
75
|
+
.variations([{id: 'trueId', value: 'true'}, {id: 'falseId', value: 'false'}])
|
|
76
|
+
.version(1)
|
|
77
|
+
.build();
|
|
78
|
+
|
|
79
|
+
await td.update(flag);
|
|
80
|
+
expect(await provider.getClient()
|
|
81
|
+
.stringVariation('flagA', { key: 'test-key' }, 'false'))
|
|
82
|
+
.toEqual('true');
|
|
83
|
+
expect(count).toEqual(1);
|
|
84
|
+
await provider.onClose();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('given a mock FbClient', () => {
|
|
88
|
+
const logger: integrations.TestLogger = new integrations.TestLogger();
|
|
89
|
+
let provider: FbProvider;
|
|
90
|
+
let fbClient: IFbClient;
|
|
91
|
+
let openFeatureClient: Client;
|
|
92
|
+
const basicContext = { targetingKey: 'the-key' };
|
|
93
|
+
const testFlagKey = 'a-key';
|
|
94
|
+
let testFlagBuilder;
|
|
95
|
+
|
|
96
|
+
const td = new integrations.TestData();
|
|
97
|
+
const factory = td.getFactory();
|
|
98
|
+
|
|
99
|
+
beforeEach(() => {
|
|
100
|
+
testFlagBuilder = new integrations.FlagBuilder()
|
|
101
|
+
.key(testFlagKey)
|
|
102
|
+
.disabledVariationId('invalidId')
|
|
103
|
+
.fallthrough({
|
|
104
|
+
dispatchKey: "keyId",
|
|
105
|
+
includedInExpt: true,
|
|
106
|
+
variations: [
|
|
107
|
+
{
|
|
108
|
+
id: "trueId",
|
|
109
|
+
exptRollout: 1,
|
|
110
|
+
rollout: [0, 1]
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
})
|
|
114
|
+
.targetUsers([{keyIds: [basicContext.targetingKey], variationId: 'trueId'}])
|
|
115
|
+
.variations([{id: 'trueId', value: 'true'}, {id: 'falseId', value: 'false'}, {id: 'invalidId', value: 'badness'}])
|
|
116
|
+
.version(1);
|
|
117
|
+
|
|
118
|
+
provider = new FbProvider({
|
|
119
|
+
sdkKey: 'sdk-key',
|
|
120
|
+
logger,
|
|
121
|
+
streamingUri: 'ws://localhost:5100',
|
|
122
|
+
eventsUri: 'http://localhost:5100',
|
|
123
|
+
dataSynchronizer: factory,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
fbClient = provider.getClient();
|
|
127
|
+
OpenFeature.setProvider(provider);
|
|
128
|
+
openFeatureClient = OpenFeature.getClient();
|
|
129
|
+
logger.reset();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
afterEach(() => {
|
|
133
|
+
jest.resetAllMocks();
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
afterAll(async () => {
|
|
137
|
+
await provider.onClose();
|
|
138
|
+
});
|
|
139
|
+
// String variations
|
|
140
|
+
it('calls the client correctly for string variations', async () => {
|
|
141
|
+
fbClient.evaluateCore<string> = jest.fn( () => ({
|
|
142
|
+
kind: ReasonKinds.Off,
|
|
143
|
+
reason: '',
|
|
144
|
+
value: 'some value'
|
|
145
|
+
}));
|
|
146
|
+
await openFeatureClient.getStringDetails(testFlagKey, 'default', basicContext);
|
|
147
|
+
expect(fbClient.evaluateCore)
|
|
148
|
+
.toHaveBeenCalledWith(testFlagKey, translateContext(logger, basicContext), 'default', expect.anything());
|
|
149
|
+
jest.clearAllMocks();
|
|
150
|
+
await openFeatureClient.getStringValue(testFlagKey, 'default', basicContext);
|
|
151
|
+
expect(fbClient.evaluateCore)
|
|
152
|
+
.toHaveBeenCalledWith(testFlagKey, translateContext(logger, basicContext), 'default', expect.anything());
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('handles correct return types for string variations', async () => {
|
|
156
|
+
await td.update(testFlagBuilder.isEnabled(true).build());
|
|
157
|
+
const res = await openFeatureClient.getStringDetails(testFlagKey, 'default', basicContext);
|
|
158
|
+
expect(res).toMatchObject({
|
|
159
|
+
flagKey: testFlagKey,
|
|
160
|
+
value: 'true',
|
|
161
|
+
reason: ReasonKinds.TargetMatch,
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Boolean variations
|
|
166
|
+
it('calls the client correctly for boolean variations', async () => {
|
|
167
|
+
fbClient.evaluateCore<boolean> = jest.fn( () => ({
|
|
168
|
+
kind: ReasonKinds.Off,
|
|
169
|
+
reason: '',
|
|
170
|
+
value: true
|
|
171
|
+
}));
|
|
172
|
+
await openFeatureClient.getBooleanDetails(testFlagKey, false, basicContext);
|
|
173
|
+
expect(fbClient.evaluateCore)
|
|
174
|
+
.toHaveBeenCalledWith(testFlagKey, translateContext(logger, basicContext), false, expect.anything());
|
|
175
|
+
jest.clearAllMocks();
|
|
176
|
+
await openFeatureClient.getBooleanValue(testFlagKey, false, basicContext);
|
|
177
|
+
expect(fbClient.evaluateCore)
|
|
178
|
+
.toHaveBeenCalledWith(testFlagKey, translateContext(logger, basicContext), false, expect.anything());
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('handles correct return types for boolean variations', async () => {
|
|
182
|
+
await td.update(testFlagBuilder.isEnabled(true).build());
|
|
183
|
+
const res = await openFeatureClient.getBooleanDetails(testFlagKey, false, basicContext);
|
|
184
|
+
expect(res).toMatchObject({
|
|
185
|
+
flagKey: testFlagKey,
|
|
186
|
+
value: true,
|
|
187
|
+
reason: ReasonKinds.TargetMatch,
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('handles incorrect return types for boolean variations', async () => {
|
|
192
|
+
await td.update(testFlagBuilder.isEnabled(false).build());
|
|
193
|
+
const res = await openFeatureClient.getBooleanDetails(testFlagKey, false, basicContext);
|
|
194
|
+
expect(res).toMatchObject({
|
|
195
|
+
flagKey: testFlagKey,
|
|
196
|
+
value: false,
|
|
197
|
+
reason: 'ERROR',
|
|
198
|
+
errorCode: 'TYPE_MISMATCH',
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Numeric variations
|
|
203
|
+
it('calls the client correctly for numeric variations', async () => {
|
|
204
|
+
fbClient.evaluateCore<number> = jest.fn( () => ({
|
|
205
|
+
kind: ReasonKinds.Off,
|
|
206
|
+
reason: '',
|
|
207
|
+
value: 1
|
|
208
|
+
}));
|
|
209
|
+
await openFeatureClient.getNumberDetails(testFlagKey, 0, basicContext);
|
|
210
|
+
expect(fbClient.evaluateCore)
|
|
211
|
+
.toHaveBeenCalledWith(testFlagKey, translateContext(logger, basicContext), 0, expect.anything());
|
|
212
|
+
jest.clearAllMocks();
|
|
213
|
+
await openFeatureClient.getNumberValue(testFlagKey, 0, basicContext);
|
|
214
|
+
expect(fbClient.evaluateCore)
|
|
215
|
+
.toHaveBeenCalledWith(testFlagKey, translateContext(logger, basicContext), 0, expect.anything());
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('handles correct return types for numeric variations', async () => {
|
|
219
|
+
await td.update(testFlagBuilder.variations([{id: 'invalidId', value: '1'}]).isEnabled(false).build());
|
|
220
|
+
const res = await openFeatureClient.getNumberDetails(testFlagKey, 0, basicContext);
|
|
221
|
+
expect(res).toMatchObject({
|
|
222
|
+
flagKey: testFlagKey,
|
|
223
|
+
value: 1,
|
|
224
|
+
reason: ReasonKinds.Off
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('handles incorrect return types for numeric variations', async () => {
|
|
229
|
+
await td.update(testFlagBuilder.isEnabled(true).build());
|
|
230
|
+
const res = await openFeatureClient.getNumberDetails(testFlagKey, 0, basicContext);
|
|
231
|
+
expect(res).toMatchObject({
|
|
232
|
+
flagKey: testFlagKey,
|
|
233
|
+
value: 0,
|
|
234
|
+
reason: 'ERROR',
|
|
235
|
+
errorCode: 'TYPE_MISMATCH',
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// JSON variations
|
|
240
|
+
it('calls the client correctly for object variations', async () => {
|
|
241
|
+
fbClient.evaluateCore<any> = jest.fn( () => ({
|
|
242
|
+
kind: ReasonKinds.Off,
|
|
243
|
+
reason: '',
|
|
244
|
+
value: { some: 'value' }
|
|
245
|
+
}));
|
|
246
|
+
await openFeatureClient.getObjectDetails(testFlagKey, {}, basicContext);
|
|
247
|
+
expect(fbClient.evaluateCore)
|
|
248
|
+
.toHaveBeenCalledWith(testFlagKey, translateContext(logger, basicContext), {}, expect.anything());
|
|
249
|
+
jest.clearAllMocks();
|
|
250
|
+
await openFeatureClient.getObjectValue(testFlagKey, {}, basicContext);
|
|
251
|
+
expect(fbClient.evaluateCore)
|
|
252
|
+
.toHaveBeenCalledWith(testFlagKey, translateContext(logger, basicContext), {}, expect.anything());
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('handles correct return types for object variations', async () => {
|
|
256
|
+
await td.update(testFlagBuilder.variations([{id: 'invalidId', value: '{"some": "value"}'}]).isEnabled(false).build());
|
|
257
|
+
const res = await openFeatureClient.getObjectDetails(testFlagKey, {}, basicContext);
|
|
258
|
+
expect(res).toMatchObject({
|
|
259
|
+
flagKey: testFlagKey,
|
|
260
|
+
value: { some: 'value' },
|
|
261
|
+
reason: ReasonKinds.Off
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('handles incorrect return types for object variations', async () => {
|
|
266
|
+
await td.update(testFlagBuilder.isEnabled(false).build());
|
|
267
|
+
const res = await openFeatureClient.getObjectDetails(testFlagKey, {}, basicContext);
|
|
268
|
+
expect(res).toMatchObject({
|
|
269
|
+
flagKey: testFlagKey,
|
|
270
|
+
value: {},
|
|
271
|
+
reason: 'ERROR',
|
|
272
|
+
errorCode: 'TYPE_MISMATCH',
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('not existing flag', async () => {
|
|
277
|
+
const flagKey = 'not_existing_flag';
|
|
278
|
+
const res = await openFeatureClient.getObjectDetails(flagKey, {}, basicContext);
|
|
279
|
+
expect(res).toMatchObject({
|
|
280
|
+
flagKey,
|
|
281
|
+
value: {},
|
|
282
|
+
reason: 'ERROR',
|
|
283
|
+
errorCode: ErrorCode.FLAG_NOT_FOUND,
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('logs information about missing keys', async () => {
|
|
288
|
+
await openFeatureClient.getObjectDetails(testFlagKey, {}, {});
|
|
289
|
+
expect(logger.logs[0]).toEqual("The EvaluationContext must contain either a 'targetingKey' "
|
|
290
|
+
+ "or a 'key' and the type must be a string.");
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import SafeLogger from "../src/SafeLogger";
|
|
2
|
+
import { BasicLogger, ILogger } from "@featbit/node-server-sdk";
|
|
3
|
+
|
|
4
|
+
it('throws when constructed with an invalid logger', () => {
|
|
5
|
+
expect(
|
|
6
|
+
() => new SafeLogger({} as ILogger, new BasicLogger({})),
|
|
7
|
+
).toThrow();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
describe('given a logger that throws in logs', () => {
|
|
11
|
+
const strings: string[] = [];
|
|
12
|
+
const logger = new SafeLogger({
|
|
13
|
+
info: () => { throw new Error('info'); },
|
|
14
|
+
debug: () => { throw new Error('info'); },
|
|
15
|
+
warn: () => { throw new Error('info'); },
|
|
16
|
+
error: () => { throw new Error('info'); },
|
|
17
|
+
}, new BasicLogger({
|
|
18
|
+
level: 'debug',
|
|
19
|
+
destination: (...args: any) => {
|
|
20
|
+
strings.push(args.join(' '));
|
|
21
|
+
},
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
it('uses the fallback logger', () => {
|
|
25
|
+
logger.debug('a');
|
|
26
|
+
logger.info('b');
|
|
27
|
+
logger.warn('c');
|
|
28
|
+
logger.error('d');
|
|
29
|
+
expect(strings).toEqual([
|
|
30
|
+
'debug: [FeatBit] a',
|
|
31
|
+
'info: [FeatBit] b',
|
|
32
|
+
'warn: [FeatBit] c',
|
|
33
|
+
'error: [FeatBit] d',
|
|
34
|
+
]);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// import { ILogger } from "@featbit/node-server-sdk";
|
|
2
|
+
//
|
|
3
|
+
//
|
|
4
|
+
// export default class TestLogger implements ILogger {
|
|
5
|
+
// public logs: string[] = [];
|
|
6
|
+
//
|
|
7
|
+
// error(...args: any[]): void {
|
|
8
|
+
// this.logs.push(args.join(' '));
|
|
9
|
+
// }
|
|
10
|
+
//
|
|
11
|
+
// warn(...args: any[]): void {
|
|
12
|
+
// this.logs.push(args.join(' '));
|
|
13
|
+
// }
|
|
14
|
+
//
|
|
15
|
+
// info(...args: any[]): void {
|
|
16
|
+
// this.logs.push(args.join(' '));
|
|
17
|
+
// }
|
|
18
|
+
//
|
|
19
|
+
// debug(...args: any[]): void {
|
|
20
|
+
// this.logs.push(args.join(' '));
|
|
21
|
+
// }
|
|
22
|
+
//
|
|
23
|
+
// reset() {
|
|
24
|
+
// this.logs = [];
|
|
25
|
+
// }
|
|
26
|
+
// }
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { translateContext } from "../src/translateContext";
|
|
2
|
+
import { integrations } from "@featbit/node-server-sdk";
|
|
3
|
+
|
|
4
|
+
const testLogger = new integrations.TestLogger();
|
|
5
|
+
it('Uses the targetingKey as the user key', () => {
|
|
6
|
+
expect(translateContext(testLogger, { targetingKey: 'the-key' })).toEqual({ key: 'the-key', name: '', customizedProperties: [] });
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('Uses the name as the user name', () => {
|
|
10
|
+
expect(translateContext(testLogger, { name: 'the-key' })).toEqual({ key: '', name: 'the-key', customizedProperties: [] });
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('gives targetingKey precedence over key', () => {
|
|
14
|
+
expect(translateContext(
|
|
15
|
+
testLogger,
|
|
16
|
+
{ targetingKey: 'target-key', key: 'key-key' },
|
|
17
|
+
)).toEqual({
|
|
18
|
+
key: 'target-key',
|
|
19
|
+
name: '',
|
|
20
|
+
customizedProperties: []
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe.each([
|
|
25
|
+
['firstName', 'value3'],
|
|
26
|
+
['lastName', 'value4'],
|
|
27
|
+
['email', 'value5'],
|
|
28
|
+
['avatar', 'value6'],
|
|
29
|
+
['ip', 'value7'],
|
|
30
|
+
['country', 'value8'],
|
|
31
|
+
['anonymous', true],
|
|
32
|
+
])('given custom attributes', (key, value) => {
|
|
33
|
+
it('accepts the custom attribute as customized property correctly', () => {
|
|
34
|
+
expect(translateContext(
|
|
35
|
+
testLogger,
|
|
36
|
+
{ targetingKey: 'the-key', name: 'abc', [key]: value },
|
|
37
|
+
)).toEqual({
|
|
38
|
+
key: 'the-key',
|
|
39
|
+
name: 'abc',
|
|
40
|
+
customizedProperties: [{
|
|
41
|
+
name: key, value
|
|
42
|
+
}]
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('Accepts array custom as customized properties', () => {
|
|
48
|
+
const context = {
|
|
49
|
+
key: 'the-key',
|
|
50
|
+
custom: [{ name: 'custom1', value: 'value1' }, { name: 'custom2', value: 'value2' }]
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
expect(translateContext(
|
|
54
|
+
testLogger,
|
|
55
|
+
context,
|
|
56
|
+
)).toEqual({
|
|
57
|
+
key: 'the-key',
|
|
58
|
+
name: '',
|
|
59
|
+
customizedProperties: context.custom
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('Accepts object custom as customized properties', () => {
|
|
64
|
+
const context = {
|
|
65
|
+
key: 'the-key',
|
|
66
|
+
custom: {
|
|
67
|
+
custom1: 'value1',
|
|
68
|
+
custom2: 'value2'
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
expect(translateContext(
|
|
73
|
+
testLogger,
|
|
74
|
+
context,
|
|
75
|
+
)).toEqual({
|
|
76
|
+
key: 'the-key',
|
|
77
|
+
name: '',
|
|
78
|
+
customizedProperties: [
|
|
79
|
+
{
|
|
80
|
+
name: 'custom1',
|
|
81
|
+
value: 'value1'
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'custom2',
|
|
85
|
+
value: 'value2'
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it.each(['key', 'targetingKey'])('handles key or targetingKey', (key) => {
|
|
92
|
+
expect(translateContext(
|
|
93
|
+
testLogger,
|
|
94
|
+
{ [key]: 'the-key' },
|
|
95
|
+
)).toEqual({
|
|
96
|
+
key: 'the-key',
|
|
97
|
+
name: '',
|
|
98
|
+
customizedProperties: []
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { translateResult } from "../src/translateResult";
|
|
2
|
+
import { IEvalDetail, ReasonKinds } from "@featbit/node-server-sdk"
|
|
3
|
+
import { ErrorCode, StandardResolutionReasons } from "@openfeature/server-sdk";
|
|
4
|
+
|
|
5
|
+
it.each([
|
|
6
|
+
true,
|
|
7
|
+
'potato',
|
|
8
|
+
42,
|
|
9
|
+
{ yes: 'no' },
|
|
10
|
+
])('puts the value into the result.', (value) => {
|
|
11
|
+
const evalDetail: IEvalDetail<typeof value> = {
|
|
12
|
+
value,
|
|
13
|
+
kind: ReasonKinds.FallThrough,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
expect(translateResult<typeof value>(evalDetail).value).toEqual(value);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it.each([
|
|
20
|
+
ReasonKinds.Off,
|
|
21
|
+
ReasonKinds.FallThrough,
|
|
22
|
+
ReasonKinds.TargetMatch,
|
|
23
|
+
ReasonKinds.RuleMatch
|
|
24
|
+
])('populates the resolution reason with kind', (kind: ReasonKinds) => {
|
|
25
|
+
expect(translateResult<boolean>({
|
|
26
|
+
value: true,
|
|
27
|
+
kind
|
|
28
|
+
}).reason).toEqual(kind);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it.each([
|
|
32
|
+
[ReasonKinds.WrongType, ErrorCode.TYPE_MISMATCH],
|
|
33
|
+
[ReasonKinds.Error, ErrorCode.GENERAL],
|
|
34
|
+
[ReasonKinds.ClientNotReady, ErrorCode.PROVIDER_NOT_READY],
|
|
35
|
+
])('populates the resolution reason with error', (kind: ReasonKinds, expectedErrorCode) => {
|
|
36
|
+
const result = translateResult<boolean>({
|
|
37
|
+
value: true,
|
|
38
|
+
kind
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
expect(result.reason).toEqual(StandardResolutionReasons.ERROR);
|
|
42
|
+
expect(result.errorCode).toEqual(expectedErrorCode);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('does not populate the errorCode when there is not an error', () => {
|
|
46
|
+
const translated = translateResult<boolean>({
|
|
47
|
+
value: true,
|
|
48
|
+
kind: ReasonKinds.FallThrough,
|
|
49
|
+
});
|
|
50
|
+
expect(translated.errorCode).toBeUndefined();
|
|
51
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"include": ["src/**/*"],
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"esModuleInterop": true,
|
|
6
|
+
"target": "es6",
|
|
7
|
+
"moduleResolution": "node",
|
|
8
|
+
"sourceMap": true,
|
|
9
|
+
"outDir": "dist",
|
|
10
|
+
"rootDir": "src",
|
|
11
|
+
"declaration": true,
|
|
12
|
+
"declarationMap": true // enables importers to jump to source
|
|
13
|
+
},
|
|
14
|
+
"lib": ["es2015"]
|
|
15
|
+
}
|