flagsmith-nodejs 3.1.1 → 3.3.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.
@@ -0,0 +1,93 @@
1
+ {
2
+ "api_key": "B62qaMZNwfiqT76p38ggrQ",
3
+ "project": {
4
+ "name": "Test project",
5
+ "organisation": {
6
+ "feature_analytics": false,
7
+ "name": "Test Org",
8
+ "id": 1,
9
+ "persist_trait_data": true,
10
+ "stop_serving_flags": false
11
+ },
12
+ "id": 1,
13
+ "hide_disabled_flags": false,
14
+ "segments": [
15
+ {
16
+ "name": "regular_segment",
17
+ "feature_states": [
18
+ {
19
+ "feature_state_value": "segment_override",
20
+ "multivariate_feature_state_values": [],
21
+ "django_id": 81027,
22
+ "feature": {
23
+ "name": "some_feature",
24
+ "type": "STANDARD",
25
+ "id": 1
26
+ },
27
+ "enabled": false
28
+ }
29
+ ],
30
+ "id": 1,
31
+ "rules": [
32
+ {
33
+ "type": "ALL",
34
+ "conditions": [],
35
+ "rules": [
36
+ {
37
+ "type": "ANY",
38
+ "conditions": [
39
+ {
40
+ "value": "40",
41
+ "property_": "age",
42
+ "operator": "LESS_THAN"
43
+ }
44
+ ],
45
+ "rules": []
46
+ }
47
+ ]
48
+ }
49
+ ]
50
+ }
51
+ ]
52
+ },
53
+ "segment_overrides": [],
54
+ "id": 1,
55
+ "feature_states": [
56
+ {
57
+ "multivariate_feature_state_values": [],
58
+ "feature_state_value": "offline-value",
59
+ "id": 1,
60
+ "featurestate_uuid": "40eb539d-3713-4720-bbd4-829dbef10d51",
61
+ "feature": {
62
+ "name": "some_feature",
63
+ "type": "STANDARD",
64
+ "id": 1
65
+ },
66
+ "feature_segment": null,
67
+ "enabled": true
68
+ },
69
+ {
70
+ "multivariate_feature_state_values": [
71
+ {
72
+ "percentage_allocation": 100,
73
+ "multivariate_feature_option": {
74
+ "value": "bar",
75
+ "id": 1
76
+ },
77
+ "mv_fs_value_uuid": "42d5cdf9-8ec9-4b8d-a3ca-fd43c64d5f05",
78
+ "id": 1
79
+ }
80
+ ],
81
+ "feature_state_value": "foo",
82
+ "feature": {
83
+ "name": "mv_feature",
84
+ "type": "MULTIVARIATE",
85
+ "id": 2
86
+ },
87
+ "feature_segment": null,
88
+ "featurestate_uuid": "96fc3503-09d7-48f1-a83b-2dc903d5c08a",
89
+ "enabled": false
90
+ }
91
+ ]
92
+ }
93
+
@@ -1,12 +1,12 @@
1
1
  import Flagsmith from '../../sdk';
2
2
  import { EnvironmentDataPollingManager } from '../../sdk/polling_manager';
3
- import fetch, {RequestInit} from 'node-fetch';
3
+ import fetch, { RequestInit } from 'node-fetch';
4
4
  import { environmentJSON, environmentModel, flagsJSON, flagsmith, identitiesJSON } from './utils';
5
- import { DefaultFlag } from '../../sdk/models';
6
- import {delay, retryFetch} from '../../sdk/utils';
7
- import * as utils from '../../sdk/utils';
5
+ import { DefaultFlag, Flags } from '../../sdk/models';
6
+ import { delay } from '../../sdk/utils';
8
7
  import { EnvironmentModel } from '../../flagsmith-engine/environments/models';
9
8
  import https from 'https'
9
+ import { BaseOfflineHandler } from '../../sdk/offline_handlers';
10
10
 
11
11
  jest.mock('node-fetch');
12
12
  jest.mock('../../sdk/polling_manager');
@@ -47,9 +47,6 @@ test('test_update_environment_sets_environment', async () => {
47
47
 
48
48
  const model = environmentModel(JSON.parse(environmentJSON()));
49
49
 
50
- wipeFeatureStateUUIDs(flg.environment)
51
- wipeFeatureStateUUIDs(model)
52
-
53
50
  expect(flg.environment).toStrictEqual(model);
54
51
  });
55
52
 
@@ -57,8 +54,8 @@ test('test_set_agent_options', async () => {
57
54
  const agent = new https.Agent({})
58
55
 
59
56
  // @ts-ignore
60
- fetch.mockImplementation((url:string, options:RequestInit)=>{
61
- if(options.agent!==agent) {
57
+ fetch.mockImplementation((url: string, options: RequestInit) => {
58
+ if (options.agent !== agent) {
62
59
  throw new Error("Agent has not been set on retry fetch")
63
60
  }
64
61
  return Promise.resolve(new Response(environmentJSON()))
@@ -202,7 +199,7 @@ test('request timeout uses default if not provided', async () => {
202
199
  expect(flg.requestTimeoutMs).toBe(10000);
203
200
  })
204
201
 
205
- test('test_throws_when_no_identity_flags_returned_due_to_error', async () => {
202
+ test('test_throws_when_no_identityFlags_returned_due_to_error', async () => {
206
203
  // @ts-ignore
207
204
  fetch.mockReturnValue(Promise.resolve(new Response('bad data')));
208
205
 
@@ -275,27 +272,118 @@ test('getIdentitySegments throws error if identifier is empty string', () => {
275
272
  })
276
273
 
277
274
 
278
- async function wipeFeatureStateUUIDs (enviromentModel: EnvironmentModel) {
279
- // TODO: this has been pulled out of tests above as a helper function.
280
- // I'm not entirely sure why it's necessary, however, we should look to remove.
281
- enviromentModel.featureStates.forEach(fs => {
282
- // @ts-ignore
283
- fs.featurestateUUID = undefined;
284
- fs.multivariateFeatureStateValues.forEach(mvfsv => {
285
- // @ts-ignore
286
- mvfsv.mvFsValueUuid = undefined;
287
- })
275
+ test('offline_mode', async () => {
276
+ // Given
277
+ const environment: EnvironmentModel = environmentModel(JSON.parse(environmentJSON('offline-environment.json')));
278
+
279
+ class DummyOfflineHandler extends BaseOfflineHandler {
280
+ getEnvironment(): EnvironmentModel {
281
+ return environment;
282
+ }
283
+ }
284
+
285
+ // When
286
+ const flagsmith = new Flagsmith({ offlineMode: true, offlineHandler: new DummyOfflineHandler() });
287
+
288
+ // Then
289
+ // we can request the flags from the client successfully
290
+ const environmentFlags: Flags = await flagsmith.getEnvironmentFlags();
291
+ let flag = environmentFlags.getFlag('some_feature');
292
+ expect(flag.isDefault).toBe(false);
293
+ expect(flag.enabled).toBe(true);
294
+ expect(flag.value).toBe('offline-value');
295
+
296
+
297
+ const identityFlags: Flags = await flagsmith.getIdentityFlags("identity");
298
+ flag = identityFlags.getFlag('some_feature');
299
+ expect(flag.isDefault).toBe(false);
300
+ expect(flag.enabled).toBe(true);
301
+ expect(flag.value).toBe('offline-value');
302
+ });
303
+
304
+
305
+ test('test_flagsmith_uses_offline_handler_if_set_and_no_api_response', async () => {
306
+ // Given
307
+ const environment: EnvironmentModel = environmentModel(JSON.parse(environmentJSON('offline-environment.json')));
308
+ const api_url = 'http://some.flagsmith.com/api/v1/';
309
+ const mock_offline_handler = new BaseOfflineHandler() as jest.Mocked<BaseOfflineHandler>;
310
+
311
+ jest.spyOn(mock_offline_handler, 'getEnvironment').mockReturnValue(environment);
312
+
313
+ const flagsmith = new Flagsmith({
314
+ environmentKey: 'some-key',
315
+ apiUrl: api_url,
316
+ offlineHandler: mock_offline_handler,
317
+ });
318
+
319
+ jest.spyOn(flagsmith, 'getEnvironmentFlags');
320
+ jest.spyOn(flagsmith, 'getIdentityFlags');
321
+
322
+
323
+ flagsmith.environmentFlagsUrl = 'http://some.flagsmith.com/api/v1/environment-flags';
324
+ flagsmith.identitiesUrl = 'http://some.flagsmith.com/api/v1/identities';
325
+
326
+ // Mock a 500 Internal Server Error response
327
+ const errorResponse = new Response(null, {
328
+ status: 500,
329
+ statusText: 'Internal Server Error',
288
330
  });
289
- enviromentModel.project.segments.forEach(s => {
290
- s.featureStates.forEach(fs => {
291
- // @ts-ignore
292
- fs.featurestateUUID = undefined;
293
- })
294
- })
295
- }
331
+
332
+ // @ts-ignore
333
+ fetch.mockReturnValue(Promise.resolve(errorResponse));
334
+
335
+ // When
336
+ const environmentFlags: Flags = await flagsmith.getEnvironmentFlags();
337
+ const identityFlags: Flags = await flagsmith.getIdentityFlags('identity', {});
338
+
339
+ // Then
340
+ expect(mock_offline_handler.getEnvironment).toHaveBeenCalledTimes(1);
341
+ expect(flagsmith.getEnvironmentFlags).toHaveBeenCalled();
342
+ expect(flagsmith.getIdentityFlags).toHaveBeenCalled();
343
+
344
+ expect(environmentFlags.isFeatureEnabled('some_feature')).toBe(true);
345
+ expect(environmentFlags.getFeatureValue('some_feature')).toBe('offline-value');
346
+
347
+ expect(identityFlags.isFeatureEnabled('some_feature')).toBe(true);
348
+ expect(identityFlags.getFeatureValue('some_feature')).toBe('offline-value');
349
+ });
350
+
351
+ test('cannot use offline mode without offline handler', () => {
352
+ // When and Then
353
+ expect(() => new Flagsmith({ offlineMode: true, offlineHandler: undefined })).toThrowError(
354
+ 'ValueError: offlineHandler must be provided to use offline mode.'
355
+ );
356
+ });
357
+
358
+ test('cannot use both default handler and offline handler', () => {
359
+ // When and Then
360
+ expect(() => new Flagsmith({
361
+ offlineHandler: new BaseOfflineHandler(),
362
+ defaultFlagHandler: (flagName) => new DefaultFlag('foo', true)
363
+ })).toThrowError('ValueError: Cannot use both defaultFlagHandler and offlineHandler.');
364
+ });
365
+
366
+ test('cannot create Flagsmith client in remote evaluation without API key', () => {
367
+ // When and Then
368
+ // @ts-ignore
369
+ expect(() => new Flagsmith()).toThrowError('ValueError: environmentKey is required.');
370
+ });
371
+
296
372
 
297
373
  function sleep(ms: number) {
298
374
  return new Promise((resolve) => {
299
375
  setTimeout(resolve, ms);
300
376
  });
301
377
  }
378
+ test('test_localEvaluation_true__identity_overrides_evaluated', async () => {
379
+ // @ts-ignore
380
+ fetch.mockReturnValue(Promise.resolve(new Response(environmentJSON())));
381
+
382
+ const flg = new Flagsmith({
383
+ environmentKey: 'ser.key',
384
+ enableLocalEvaluation: true,
385
+ });
386
+
387
+ const flags = await flg.getIdentityFlags("overridden-id");
388
+ expect(flags.getFeatureValue("some_feature")).toEqual("some-overridden-value");
389
+ });
@@ -0,0 +1,33 @@
1
+ import * as fs from 'fs';
2
+ import { LocalFileHandler } from '../../sdk/offline_handlers';
3
+ import { EnvironmentModel } from '../../flagsmith-engine';
4
+
5
+ const offlineEnvironment = require('./data/offline-environment.json');
6
+
7
+ jest.mock('fs')
8
+
9
+ const offlineEnvironmentString = JSON.stringify(offlineEnvironment)
10
+
11
+ test('local file handler', () => {
12
+ const environmentDocumentFilePath = '/some/path/environment.json';
13
+
14
+ // Mock the fs.readFileSync function to return environmentJson
15
+
16
+ // @ts-ignore
17
+ const readFileSyncMock = jest.spyOn(fs, 'readFileSync');
18
+ readFileSyncMock.mockImplementation(() => offlineEnvironmentString);
19
+
20
+ // Given
21
+ const localFileHandler = new LocalFileHandler(environmentDocumentFilePath);
22
+
23
+ // When
24
+ const environmentModel = localFileHandler.getEnvironment();
25
+
26
+ // Then
27
+ expect(environmentModel).toBeInstanceOf(EnvironmentModel);
28
+ expect(environmentModel.apiKey).toBe('B62qaMZNwfiqT76p38ggrQ');
29
+ expect(readFileSyncMock).toHaveBeenCalledWith(environmentDocumentFilePath, 'utf8');
30
+
31
+ // Restore the original implementation of fs.readFileSync
32
+ readFileSyncMock.mockRestore();
33
+ });
@@ -42,8 +42,8 @@ export function flagsmith(params = {}) {
42
42
  });
43
43
  }
44
44
 
45
- export function environmentJSON() {
46
- return readFileSync(DATA_DIR + 'environment.json', 'utf-8');
45
+ export function environmentJSON(environmentFilename: string = 'environment.json') {
46
+ return readFileSync(DATA_DIR + environmentFilename, 'utf-8');
47
47
  }
48
48
 
49
49
  export function environmentModel(environmentJSON: any) {
@@ -1,4 +0,0 @@
1
- export declare class IntegrationModel {
2
- api_key?: string;
3
- base_url?: string;
4
- }
@@ -1,11 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.IntegrationModel = void 0;
4
- var IntegrationModel = /** @class */ (function () {
5
- function IntegrationModel() {
6
- this.api_key = undefined;
7
- this.base_url = undefined;
8
- }
9
- return IntegrationModel;
10
- }());
11
- exports.IntegrationModel = IntegrationModel;
@@ -1,3 +0,0 @@
1
- # Flagsmith NodeJS Examples
2
-
3
- Check out our [NodeJS Examples repository](https://github.com/Flagsmith/flagsmith-nodejs-examples).
@@ -1,4 +0,0 @@
1
- export class IntegrationModel {
2
- api_key?: string = undefined;
3
- base_url?: string = undefined;
4
- }