flagsmith-nodejs 5.1.1 → 6.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.
@@ -1,6 +1,13 @@
1
1
  import Flagsmith from '../../sdk/index.js';
2
2
  import { EnvironmentDataPollingManager } from '../../sdk/polling_manager.js';
3
- import { environmentJSON, environmentModel, flagsJSON, flagsmith, fetch, offlineEnvironmentJSON } from './utils.js';
3
+ import {
4
+ environmentJSON,
5
+ environmentModel,
6
+ flagsmith,
7
+ fetch,
8
+ offlineEnvironmentJSON,
9
+ badFetch
10
+ } from './utils.js';
4
11
  import { DefaultFlag, Flags } from '../../sdk/models.js';
5
12
  import { delay } from '../../sdk/utils.js';
6
13
  import { EnvironmentModel } from '../../flagsmith-engine/environments/models.js';
@@ -8,12 +15,7 @@ import { BaseOfflineHandler } from '../../sdk/offline_handlers.js';
8
15
  import { Agent } from 'undici';
9
16
 
10
17
  vi.mock('../../sdk/polling_manager');
11
- beforeEach(() => {
12
- vi.clearAllMocks();
13
- });
14
-
15
18
  test('test_flagsmith_starts_polling_manager_on_init_if_enabled', () => {
16
- fetch.mockResolvedValue(new Response(environmentJSON));
17
19
  new Flagsmith({
18
20
  environmentKey: 'ser.key',
19
21
  enableLocalEvaluation: true
@@ -22,30 +24,26 @@ test('test_flagsmith_starts_polling_manager_on_init_if_enabled', () => {
22
24
  });
23
25
 
24
26
  test('test_flagsmith_local_evaluation_key_required', () => {
25
- fetch.mockResolvedValue(new Response(environmentJSON));
26
- console.error = vi.fn();
27
- new Flagsmith({
28
- environmentKey: 'bad.key',
29
- enableLocalEvaluation: true
30
- });
31
- expect(console.error).toBeCalled();
27
+ expect(() => {
28
+ new Flagsmith({
29
+ environmentKey: 'bad.key',
30
+ enableLocalEvaluation: true
31
+ });
32
+ }).toThrow('Using local evaluation requires a server-side environment key')
32
33
  });
33
34
 
34
35
  test('test_update_environment_sets_environment', async () => {
35
- fetch.mockResolvedValue(new Response(environmentJSON));
36
- const flg = flagsmith();
37
- await flg.updateEnvironment();
38
- expect(flg.environment).toBeDefined();
39
-
36
+ const flg = flagsmith({
37
+ environmentKey: 'ser.key',
38
+ });
40
39
  const model = environmentModel(JSON.parse(environmentJSON));
41
-
42
- expect(flg.environment).toStrictEqual(model);
40
+ expect(await flg.getEnvironment()).toStrictEqual(model);
43
41
  });
44
42
 
45
43
  test('test_set_agent_options', async () => {
46
44
  const agent = new Agent({})
47
45
 
48
- fetch.mockImplementation((url, options) => {
46
+ fetch.mockImplementationOnce((url, options) => {
49
47
  //@ts-ignore I give up
50
48
  if (options.dispatcher !== agent) {
51
49
  throw new Error("Agent has not been set on retry fetch")
@@ -58,11 +56,9 @@ test('test_set_agent_options', async () => {
58
56
  });
59
57
 
60
58
  await flg.updateEnvironment();
61
- expect(flg.environment).toBeDefined();
62
59
  });
63
60
 
64
61
  test('test_get_identity_segments', async () => {
65
- fetch.mockResolvedValue(new Response(environmentJSON));
66
62
  const flg = flagsmith({
67
63
  environmentKey: 'ser.key',
68
64
  enableLocalEvaluation: true
@@ -75,7 +71,6 @@ test('test_get_identity_segments', async () => {
75
71
 
76
72
 
77
73
  test('test_get_identity_segments_empty_without_local_eval', async () => {
78
- fetch.mockResolvedValue(new Response(environmentJSON));
79
74
  const flg = new Flagsmith({
80
75
  environmentKey: 'ser.key',
81
76
  enableLocalEvaluation: false
@@ -85,8 +80,6 @@ test('test_get_identity_segments_empty_without_local_eval', async () => {
85
80
  });
86
81
 
87
82
  test('test_update_environment_uses_req_when_inited', async () => {
88
- fetch.mockResolvedValue(new Response(environmentJSON));
89
-
90
83
  const flg = flagsmith({
91
84
  environmentKey: 'ser.key',
92
85
  enableLocalEvaluation: true,
@@ -100,7 +93,6 @@ test('test_update_environment_uses_req_when_inited', async () => {
100
93
  });
101
94
 
102
95
  test('test_isFeatureEnabled_environment', async () => {
103
- fetch.mockResolvedValue(new Response(environmentJSON));
104
96
  const defaultFlag = new DefaultFlag('some-default-value', true);
105
97
 
106
98
  const defaultFlagHandler = (featureName: string) => defaultFlag;
@@ -118,9 +110,7 @@ test('test_isFeatureEnabled_environment', async () => {
118
110
  });
119
111
 
120
112
  test('test_fetch_recovers_after_single_API_error', async () => {
121
- fetch
122
- .mockRejectedValue('Error during fetching the API response')
123
- .mockResolvedValue(new Response(flagsJSON));
113
+ fetch.mockRejectedValueOnce('Error during fetching the API response')
124
114
  const flg = flagsmith({
125
115
  environmentKey: 'key',
126
116
  });
@@ -132,36 +122,38 @@ test('test_fetch_recovers_after_single_API_error', async () => {
132
122
  expect(flag.value).toBe('some-value');
133
123
  });
134
124
 
135
- test('test_default_flag_used_after_multiple_API_errors', async () => {
136
- fetch
137
- .mockRejectedValue(new Error('Error during fetching the API response'));
138
- const defaultFlag = new DefaultFlag('some-default-value', true);
139
-
140
- const defaultFlagHandler = (featureName: string) => defaultFlag;
141
-
142
- const flg = new Flagsmith({
143
- environmentKey: 'key',
144
- defaultFlagHandler: defaultFlagHandler
145
- });
146
-
147
- const flags = await flg.getEnvironmentFlags();
148
- const flag = flags.getFlag('some_feature');
149
- expect(flag.isDefault).toBe(true);
150
- expect(flag.enabled).toBe(defaultFlag.enabled);
151
- expect(flag.value).toBe(defaultFlag.value);
152
- });
125
+ test.each([
126
+ [false, 'key'],
127
+ [true, 'ser.key']
128
+ ])(
129
+ 'default flag handler is used when API is unavailable (local evaluation = %s)',
130
+ async (enableLocalEvaluation, environmentKey) => {
131
+ const flg = flagsmith({
132
+ enableLocalEvaluation,
133
+ environmentKey,
134
+ defaultFlagHandler: () => new DefaultFlag('some-default-value', true),
135
+ fetch: badFetch,
136
+ });
137
+ const flags = await flg.getEnvironmentFlags();
138
+ const flag = flags.getFlag('some_feature');
139
+ expect(flag.isDefault).toBe(true);
140
+ expect(flag.enabled).toBe(true);
141
+ expect(flag.value).toBe('some-default-value');
142
+ }
143
+ );
153
144
 
154
145
  test('default flag handler used when timeout occurs', async () => {
155
146
  fetch.mockImplementation(async (...args) => {
156
- await sleep(10000)
157
- return fetch(...args)
147
+ const forever = new Promise(() => {})
148
+ await forever
149
+ throw new Error('waited forever')
158
150
  });
159
151
 
160
152
  const defaultFlag = new DefaultFlag('some-default-value', true);
161
153
 
162
- const defaultFlagHandler = (featureName: string) => defaultFlag;
154
+ const defaultFlagHandler = () => defaultFlag;
163
155
 
164
- const flg = new Flagsmith({
156
+ const flg = flagsmith({
165
157
  environmentKey: 'key',
166
158
  defaultFlagHandler: defaultFlagHandler,
167
159
  requestTimeoutSeconds: 0.0001,
@@ -184,10 +176,9 @@ test('request timeout uses default if not provided', async () => {
184
176
  })
185
177
 
186
178
  test('test_throws_when_no_identityFlags_returned_due_to_error', async () => {
187
- fetch.mockResolvedValue(new Response('bad data'));
188
-
189
- const flg = new Flagsmith({
179
+ const flg = flagsmith({
190
180
  environmentKey: 'key',
181
+ fetch: badFetch,
191
182
  });
192
183
 
193
184
  await expect(async () => await flg.getIdentityFlags('identifier'))
@@ -196,34 +187,32 @@ test('test_throws_when_no_identityFlags_returned_due_to_error', async () => {
196
187
  });
197
188
 
198
189
  test('test onEnvironmentChange is called when provided', async () => {
199
- const callback = {
200
- callback: (e: Error | null, result: EnvironmentModel) => { }
201
- };
202
- const callbackSpy = vi.spyOn(callback, 'callback');
190
+ const callback = vi.fn()
203
191
 
204
192
  const flg = new Flagsmith({
205
193
  environmentKey: 'ser.key',
206
194
  enableLocalEvaluation: true,
207
- onEnvironmentChange: callback.callback,
195
+ onEnvironmentChange: callback,
208
196
  });
209
197
 
210
- await delay(200);
198
+ fetch.mockRejectedValueOnce(new Error('API error'));
199
+ await flg.updateEnvironment().catch(() => {
200
+ // Expected rejection
201
+ });
211
202
 
212
- expect(callbackSpy).toBeCalled();
203
+ expect(callback).toBeCalled();
213
204
  });
214
205
 
215
206
  test('test onEnvironmentChange is called after error', async () => {
216
- const callback = vi.fn((e, result) => {})
217
-
207
+ const callback = vi.fn();
218
208
  const flg = new Flagsmith({
219
209
  environmentKey: 'ser.key',
220
210
  enableLocalEvaluation: true,
221
211
  onEnvironmentChange: callback,
212
+ fetch: badFetch,
222
213
  });
223
-
224
- await delay(200);
225
-
226
- expect(callback).toBeCalled();
214
+ await flg.updateEnvironment();
215
+ expect(callback).toHaveBeenCalled();
227
216
  });
228
217
 
229
218
  test('getIdentityFlags throws error if identifier is empty string', async () => {
@@ -234,15 +223,15 @@ test('getIdentityFlags throws error if identifier is empty string', async () =>
234
223
  await expect(flg.getIdentityFlags('')).rejects.toThrow('`identifier` argument is missing or invalid.');
235
224
  })
236
225
 
237
-
238
- test('getIdentitySegments throws error if identifier is empty string', () => {
226
+ test('getIdentitySegments throws error if identifier is empty string', async () => {
239
227
  const flg = flagsmith({
240
228
  environmentKey: 'key',
241
229
  });
242
230
 
243
- expect(() => { flg.getIdentitySegments(''); }).toThrow('`identifier` argument is missing or invalid.');
244
- })
245
-
231
+ await expect(flg.getIdentitySegments('')).rejects.toThrow(
232
+ '`identifier` argument is missing or invalid.'
233
+ );
234
+ });
246
235
 
247
236
  test('offline_mode', async () => {
248
237
  // Given
@@ -286,6 +275,7 @@ test('test_flagsmith_uses_offline_handler_if_set_and_no_api_response', async ()
286
275
  environmentKey: 'some-key',
287
276
  apiUrl: api_url,
288
277
  offlineHandler: mock_offline_handler,
278
+ offlineMode: true
289
279
  });
290
280
 
291
281
  vi.spyOn(flg, 'getEnvironmentFlags');
@@ -305,10 +295,10 @@ test('test_flagsmith_uses_offline_handler_if_set_and_no_api_response', async ()
305
295
 
306
296
  // When
307
297
  const environmentFlags: Flags = await flg.getEnvironmentFlags();
298
+ expect(mock_offline_handler.getEnvironment).toHaveBeenCalledTimes(1);
308
299
  const identityFlags: Flags = await flg.getIdentityFlags('identity', {});
309
300
 
310
301
  // Then
311
- expect(mock_offline_handler.getEnvironment).toHaveBeenCalledTimes(1);
312
302
  expect(flg.getEnvironmentFlags).toHaveBeenCalled();
313
303
  expect(flg.getIdentityFlags).toHaveBeenCalled();
314
304
 
@@ -336,22 +326,37 @@ test('cannot use both default handler and offline handler', () => {
336
326
 
337
327
  test('cannot create Flagsmith client in remote evaluation without API key', () => {
338
328
  // When and Then
339
- expect(() => new Flagsmith()).toThrowError('ValueError: environmentKey is required.');
329
+ expect(() => new Flagsmith({ environmentKey: '' })).toThrowError('ValueError: environmentKey is required.');
340
330
  });
341
331
 
342
332
 
343
- function sleep(ms: number) {
344
- return new Promise((resolve) => {
345
- setTimeout(resolve, ms);
346
- });
347
- }
348
333
  test('test_localEvaluation_true__identity_overrides_evaluated', async () => {
349
- fetch.mockResolvedValue(new Response(environmentJSON));
334
+ const flg = flagsmith({
335
+ environmentKey: 'ser.key',
336
+ enableLocalEvaluation: true
337
+ });
338
+
339
+ await flg.updateEnvironment()
340
+ const flags = await flg.getIdentityFlags('overridden-id');
341
+ expect(flags.getFeatureValue('some_feature')).toEqual('some-overridden-value');
342
+ });
343
+
344
+ test('getIdentityFlags succeeds if initial fetch failed then succeeded', async () => {
345
+ const defaultFlagHandler = vi.fn(() => new DefaultFlag('mock-default-value', true));
346
+
347
+ fetch.mockRejectedValue(new Error('Initial API error'));
350
348
  const flg = flagsmith({
351
349
  environmentKey: 'ser.key',
352
350
  enableLocalEvaluation: true,
351
+ defaultFlagHandler
353
352
  });
354
353
 
355
- const flags = await flg.getIdentityFlags("overridden-id");
356
- expect(flags.getFeatureValue("some_feature")).toEqual("some-overridden-value");
354
+ const defaultFlags = await flg.getIdentityFlags('test-user');
355
+ expect(defaultFlags.isFeatureEnabled('mock-default-value')).toBe(true);
356
+ expect(defaultFlagHandler).toHaveBeenCalled();
357
+
358
+ fetch.mockResolvedValue(new Response(environmentJSON));
359
+ await flg.getEnvironment();
360
+ const flags2 = await flg.getIdentityFlags('test-user');
361
+ expect(flags2.isFeatureEnabled('some_feature')).toBe(true);
357
362
  });
@@ -3,10 +3,6 @@ import { EnvironmentDataPollingManager } from '../../sdk/polling_manager.js';
3
3
  import { delay } from '../../sdk/utils.js';
4
4
  vi.mock('../../sdk');
5
5
 
6
- beforeEach(() => {
7
- vi.clearAllMocks()
8
- });
9
-
10
6
  test('test_polling_manager_correctly_stops_if_never_started', async () => {
11
7
  const flagsmith = new Flagsmith({
12
8
  environmentKey: 'key'
@@ -1,8 +1,8 @@
1
1
  import { readFileSync } from 'fs';
2
2
  import { buildEnvironmentModel } from '../../flagsmith-engine/environments/util.js';
3
3
  import { AnalyticsProcessor } from '../../sdk/analytics.js';
4
- import Flagsmith from '../../sdk/index.js';
5
- import { FlagsmithCache } from '../../sdk/types.js';
4
+ import Flagsmith, {FlagsmithConfig} from '../../sdk/index.js';
5
+ import { Fetch, FlagsmithCache } from '../../sdk/types.js';
6
6
  import { Flags } from '../../sdk/models.js';
7
7
 
8
8
  const DATA_DIR = __dirname + '/data/';
@@ -19,13 +19,33 @@ export class TestCache implements FlagsmithCache {
19
19
  }
20
20
  }
21
21
 
22
- export const fetch = vi.fn(global.fetch)
22
+ export const fetch = vi.fn((url: string, options?: RequestInit) => {
23
+ const headers = options?.headers as Record<string, string>;
24
+ if (!headers) throw new Error('missing request headers')
25
+ const env = headers['X-Environment-Key'];
26
+ if (!env) return Promise.resolve(new Response('missing x-environment-key header', { status: 404 }));
27
+ if (url.includes('/environment-document')) {
28
+ if (env.startsWith('ser.')) {
29
+ return Promise.resolve(new Response(environmentJSON, { status: 200 }))
30
+ }
31
+ return Promise.resolve(new Response('environment-document called without a server-side key', { status: 401 }))
32
+ }
33
+ if (url.includes("/flags")) {
34
+ return Promise.resolve(new Response(flagsJSON, { status: 200 }))
35
+ }
36
+ if (url.includes("/identities")) {
37
+ return Promise.resolve(new Response(identitiesJSON, { status: 200 }))
38
+ }
39
+ return Promise.resolve(new Response('unknown url ' + url, { status: 404 }))
40
+ });
41
+
42
+ export const badFetch: Fetch = () => { throw new Error('fetch failed')}
23
43
 
24
44
  export function analyticsProcessor() {
25
45
  return new AnalyticsProcessor({
26
46
  environmentKey: 'test-key',
27
47
  analyticsUrl: 'http://testUrl/analytics/flags/',
28
- fetch,
48
+ fetch: (url, options) => fetch(url.toString(), options),
29
49
  });
30
50
  }
31
51
 
@@ -33,10 +53,12 @@ export function apiKey(): string {
33
53
  return 'sometestfakekey';
34
54
  }
35
55
 
36
- export function flagsmith(params = {}) {
56
+ export function flagsmith(params: FlagsmithConfig = {}) {
37
57
  return new Flagsmith({
38
58
  environmentKey: apiKey(),
39
- fetch,
59
+ environmentRefreshIntervalSeconds: 0,
60
+ requestRetryDelayMilliseconds: 0,
61
+ fetch: (url, options) => fetch(url.toString(), options),
40
62
  ...params,
41
63
  });
42
64
  }
package/vitest.config.ts CHANGED
@@ -3,6 +3,7 @@ import { defineConfig } from 'vitest/config'
3
3
  export default defineConfig({
4
4
  test: {
5
5
  globals: true,
6
+ restoreMocks: true,
6
7
  coverage: {
7
8
  reporter: ['text'],
8
9
  exclude: [
File without changes