Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v5

- name: Install Redis
run: |
sudo add-apt-repository ppa:redislabs/redis
sudo apt-get install -y redis-tools redis-server

- name: Check Redis
run: redis-cli ping

- name: Setup Node.js
uses: actions/setup-node@v4
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
dump.rdb

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
Expand Down
24 changes: 24 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"globals": "^16.3.0",
"jest": "^29.7.0",
"jiti": "^2.5.1",
"redis-server": "^1.2.2",
"replace": "^1.2.1",
"rimraf": "^3.0.2",
"ts-jest": "^29.4.1",
Expand Down
11 changes: 11 additions & 0 deletions src/__tests__/mocks/redis-commands.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
FLUSHDB
DEL 'REDIS_NODE_UT.SPLITIO.segment.UT_SEGMENT'
SADD 'REDIS_NODE_UT.SPLITIO.segment.UT_SEGMENT' UT_Segment_member
SET 'REDIS_NODE_UT.SPLITIO.segment.UT_SEGMENT.till' 1492721958710
SET 'REDIS_NODE_UT.SPLITIO.split.UT_IN_SEGMENT' '{"changeNumber":1492722104980,"trafficTypeName":"machine","name":"UT_IN_SEGMENT","seed":-202209840,"status":"ACTIVE","killed":false,"defaultTreatment":"off","conditions":[{"matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"","attribute":""},"matcherType":"IN_SEGMENT","negate":false,"userDefinedSegmentMatcherData":{"segmentName":"UT_SEGMENT"},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":{"whitelist":null},"betweenMatcherData":{"dataType":"","start":0,"end":0}}]},"partitions":[{"treatment":"on","size":100}],"label":"whitelisted segment"},{"matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"machine","attribute":""},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":{"segmentName":""},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":{"whitelist":null},"betweenMatcherData":{"dataType":"","start":0,"end":0}}]},"partitions":[{"treatment":"on","size":0},{"treatment":"off","size":100}],"label":"in segment all"}]}'
SET 'REDIS_NODE_UT.SPLITIO.split.UT_NOT_IN_SEGMENT' '{"changeNumber":1492722747908,"trafficTypeName":"machine","name":"UT_NOT_IN_SEGMENT","seed":-56653132,"status":"ACTIVE","killed":false,"defaultTreatment":"off","conditions":[{"matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"machine","attribute":""},"matcherType":"IN_SEGMENT","negate":true,"userDefinedSegmentMatcherData":{"segmentName":"UT_SEGMENT"},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":{"whitelist":null},"betweenMatcherData":{"dataType":"","start":0,"end":0}}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0}],"label":"not in segment UT_SEGMENT"}]}'
SET 'REDIS_NODE_UT.SPLITIO.split.UT_NOT_SET_MATCHER' '{"changeNumber":1492723024413,"trafficTypeName":"machine","name":"UT_NOT_SET_MATCHER","seed":-93553840,"status":"ACTIVE","killed":false,"defaultTreatment":"off","conditions":[{"matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"machine","attribute":"permissions"},"matcherType":"CONTAINS_ANY_OF_SET","negate":true,"userDefinedSegmentMatcherData":{"segmentName":""},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":{"whitelist":["create","delete","update"]},"betweenMatcherData":{"dataType":"","start":0,"end":0}}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0}],"label":"permissions does not contain any of [create, delete, ...]"}]}'
SET 'REDIS_NODE_UT.SPLITIO.split.UT_SET_MATCHER' '{"changeNumber":1492722926004,"trafficTypeName":"machine","name":"UT_SET_MATCHER","seed":-1995997836,"status":"ACTIVE","killed":false,"defaultTreatment":"off","conditions":[{"matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"machine","attribute":"permissions"},"matcherType":"CONTAINS_ANY_OF_SET","negate":false,"userDefinedSegmentMatcherData":{"segmentName":""},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":{"whitelist":["admin","premium","idol"]},"betweenMatcherData":{"dataType":"","start":0,"end":0}}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0}],"label":"permissions contains any of [admin, premium, ...]"}]}'
SET 'REDIS_NODE_UT.SPLITIO.split.always-on' '{"changeNumber":1487277320548,"trafficTypeName":"user","name":"always-on","seed":1684183541,"status":"ACTIVE","killed":false,"defaultTreatment":"off","conditions":[{"matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"user","attribute":""},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":{"segmentName":""},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":{"whitelist":null},"betweenMatcherData":{"dataType":"","start":0,"end":0}}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0}],"label":"in segment all"}]}'
SET 'REDIS_NODE_UT.SPLITIO.split.always-o.n-with-config' '{"changeNumber":1487277320548,"trafficTypeName":"user","name":"always-o.n-with-config","seed":1684183541,"status":"ACTIVE","killed":false,"defaultTreatment":"off","conditions":[{"matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"user","attribute":""},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":{"segmentName":""},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":{"whitelist":null},"betweenMatcherData":{"dataType":"","start":0,"end":0}}]},"partitions":[{"treatment":"o.n","size":100},{"treatment":"off","size":0}],"label":"in segment all"}],"configurations":{"o.n":"{\"color\":\"brown\"}"}}'
SET 'REDIS_NODE_UT.SPLITIO.splits.till' 1492723024413
4 changes: 2 additions & 2 deletions src/__tests__/nodeSuites/client.spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable jest/no-conditional-expect */
import { OpenFeatureSplitProvider } from '../../lib/js-split-provider';
import { getSplitClient } from '../testUtils';
import { getLocalHostSplitClient } from '../testUtils';

import { OpenFeature } from '@openfeature/server-sdk';

Expand All @@ -11,7 +11,7 @@ describe('client tests', () => {
let provider;

beforeEach(() => {
splitClient = getSplitClient();
splitClient = getLocalHostSplitClient();
provider = new OpenFeatureSplitProvider({ splitClient });

OpenFeature.setProvider(provider);
Expand Down
115 changes: 115 additions & 0 deletions src/__tests__/nodeSuites/client_redis.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import RedisServer from 'redis-server';
import { exec } from 'child_process';
import { OpenFeature } from '@openfeature/server-sdk';

import { getRedisSplitClient } from '../testUtils';
import { OpenFeatureSplitProvider } from '../../lib/js-split-provider';

const redisPort = '6385';

/**
* Initialize redis server and run a cli bash command to load redis with data to do the proper tests
*/
const startRedis = () => {
// Simply pass the port that you want a Redis server to listen on.
const server = new RedisServer(redisPort);

const promise = new Promise((resolve, reject) => {
server
.open()
.then(() => {
exec(`cat ./src/__tests__/mocks/redis-commands.txt | redis-cli -p ${redisPort}`, err => {
if (err) {
reject(server);
// Node.js couldn't execute the command
return;
}
resolve(server);
});
});
});

return promise;
};

let redisServer
let splitClient

beforeAll(async () => {
redisServer = await startRedis();
}, 30000);

afterAll(async () => {
await redisServer.close();
await splitClient.destroy();
});

describe('Regular usage - DEBUG strategy', () => {
splitClient = getRedisSplitClient(redisPort);
const provider = new OpenFeatureSplitProvider({ splitClient });

OpenFeature.setProviderAndWait(provider);
const client = OpenFeature.getClient();

test('Evaluate always on flag', async () => {
await client.getBooleanValue('always-on', false, {targetingKey: 'emma-ss'}).then(result => {
expect(result).toBe(true);
});
});

test('Evaluate user in segment', async () => {
await client.getBooleanValue('UT_IN_SEGMENT', false, {targetingKey: 'UT_Segment_member', properties: { /* empty properties are ignored */ }}).then(result => {
expect(result).toBe(true);
});

await client.getBooleanValue('UT_IN_SEGMENT', false, {targetingKey: 'UT_Segment_member', properties: { some: 'value1' } }).then(result => {
expect(result).toBe(true);
});

await client.getBooleanValue('UT_IN_SEGMENT', false, {targetingKey: 'other' }).then(result => {
expect(result).toBe(false);
});

await client.getBooleanValue('UT_NOT_IN_SEGMENT', true, {targetingKey: 'UT_Segment_member' }).then(result => {
expect(result).toBe(false);
});

await client.getBooleanValue('UT_NOT_IN_SEGMENT', true, {targetingKey: 'other' }).then(result => {
expect(result).toBe(true);
});

await client.getBooleanValue('UT_NOT_IN_SEGMENT', true, {targetingKey: 'other' }).then(result => {
expect(result).toBe(true);
});
});

test('Evaluate with attributes set matcher', async () => {
await client.getBooleanValue('UT_SET_MATCHER', false, {targetingKey: 'UT_Segment_member', permissions: ['admin'] }).then(result => {
expect(result).toBe(true);
});

await client.getBooleanValue('UT_SET_MATCHER', false, {targetingKey: 'UT_Segment_member', permissions: ['not_matching'] }).then(result => {
expect(result).toBe(false);
});

await client.getBooleanValue('UT_NOT_SET_MATCHER', true, {targetingKey: 'UT_Segment_member', permissions: ['create'] }).then(result => {
expect(result).toBe(false);
});

await client.getBooleanValue('UT_NOT_SET_MATCHER', true, {targetingKey: 'UT_Segment_member', permissions: ['not_matching'] }).then(result => {
expect(result).toBe(true);
});
})

test('Evaluate with dynamic config', async () => {
await client.getBooleanDetails('UT_NOT_SET_MATCHER', true, {targetingKey: 'UT_Segment_member', permissions: ['not_matching'] }).then(result => {
expect(result.value).toBe(true);
expect(result.flagMetadata).toEqual({'config': ''});
});

await client.getStringDetails('always-o.n-with-config', 'control', {targetingKey: 'other'}).then(result => {
expect(result.value).toBe('o.n');
expect(result.flagMetadata).toEqual({config: '{"color":"brown"}'});
});
})
});
4 changes: 2 additions & 2 deletions src/__tests__/nodeSuites/provider.spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable jest/no-conditional-expect */
import { getSplitClient } from '../testUtils';
import { getLocalHostSplitClient } from '../testUtils';
import { OpenFeatureSplitProvider } from '../../lib/js-split-provider';

describe('provider tests', () => {
Expand All @@ -8,7 +8,7 @@ describe('provider tests', () => {
let provider;

beforeEach(() => {
splitClient = getSplitClient();
splitClient = getLocalHostSplitClient();
provider = new OpenFeatureSplitProvider({ splitClient });
});

Expand Down
38 changes: 31 additions & 7 deletions src/__tests__/testUtils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,16 +70,40 @@ export function url(settings, target) {
return `${settings.urls.sdk}${target}`;
}

const getRedisConfig = (redisPort) => ({
core: {
authorizationKey: 'SOME SDK KEY' // in consumer mode, SDK key is only used to track and log warning regarding duplicated SDK instances
},
mode: 'consumer',
storage: {
type: 'REDIS',
prefix: 'REDIS_NODE_UT',
options: {
url: `redis://localhost:${redisPort}/0`
}
},
sync: {
impressionsMode: 'DEBUG'
},
startup: {
readyTimeout: 36000 // 10hs
}
});

/**
* get a Split client in localhost mode for testing purposes
*/
export function getSplitClient(apiKey = 'localhost') {
return SplitFactory({
const config = {
core: {
authorizationKey: apiKey
authorizationKey: 'localhost'
},
features: './split.yaml',
debug: 'DEBUG'
}).client();
}
/**
* get a Split client in localhost mode for testing purposes
*/
export function getLocalHostSplitClient() {
return SplitFactory(config).client();
}

export function getRedisSplitClient(redisPort) {
return SplitFactory(getRedisConfig(redisPort)).client();
}
38 changes: 26 additions & 12 deletions src/lib/js-split-provider.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import {
EvaluationContext,
Provider,
ResolutionDetails,
ParseError,
FlagNotFoundError,
InvalidContextError,
JsonValue,
TargetingKeyMissingError,
OpenFeatureEventEmitter,
ParseError,
Provider,
ProviderEvents,
ResolutionDetails,
StandardResolutionReasons,
TrackingEventDetails,
InvalidContextError,
TargetingKeyMissingError,
TrackingEventDetails
} from '@openfeature/server-sdk';
import { SplitFactory } from '@splitsoftware/splitio';
import type SplitIO from '@splitsoftware/splitio/types/splitio';

export interface SplitProviderOptions {
splitClient: SplitIO.IClient;
type SplitProviderOptions = {
splitClient: SplitIO.IClient | SplitIO.IAsyncClient;
}

type Consumer = {
Expand All @@ -29,10 +32,21 @@ export class OpenFeatureSplitProvider implements Provider {
name: 'split',
};
private initialized: Promise<void>;
private client: SplitIO.IClient;
private client: SplitIO.IClient | SplitIO.IAsyncClient;

public readonly events = new OpenFeatureEventEmitter();

constructor(options: SplitProviderOptions) {
this.client = options.splitClient;
constructor(options: SplitProviderOptions | string) {

if (typeof(options) === 'string') {
const splitFactory = SplitFactory({core: { authorizationKey: options } });
this.client = splitFactory.client();
} else {
this.client = options.splitClient;
}
this.client.on(this.client.Event.SDK_UPDATE, (payload) => {
this.events.emit(ProviderEvents.ConfigurationChanged, payload)
});
this.initialized = new Promise((resolve) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((this.client as any).__getStatus().isReady) {
Expand Down Expand Up @@ -121,7 +135,7 @@ export class OpenFeatureSplitProvider implements Provider {
}

await this.initialized;
const { treatment: value, config }: SplitIO.TreatmentWithConfig = this.client.getTreatmentWithConfig(
const { treatment: value, config }: SplitIO.TreatmentWithConfig = await this.client.getTreatmentWithConfig(
consumer.key,
flagKey,
consumer.attributes
Expand Down