diff --git a/.github/workflows/unstable.yml b/.github/workflows/unstable.yml index 4721d68..b35c309 100644 --- a/.github/workflows/unstable.yml +++ b/.github/workflows/unstable.yml @@ -22,9 +22,12 @@ jobs: - name: Create build version run: echo "BUILD_VERSION=$(cat package.json | grep version | head -1 | awk '{ print $2 }' | sed 's/[\",]//g' | tr -d '[[:space:]]')" >> $GITHUB_ENV + - name: Get short hash + run: echo "SHORT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_ENV + - name: Docker Build uses: docker/build-push-action@v5 with: context: . push: true - tags: splitio-docker-dev.jfrog.io/${{ github.event.repository.name }}:${{ env.BUILD_VERSION}} + tags: splitio-docker-dev.jfrog.io/${{ github.event.repository.name }}:${{ env.SHORT_SHA}} diff --git a/.jest/setEnvVars.js b/.jest/setEnvVars.js index 71c7cc2..e2fff9f 100644 --- a/.jest/setEnvVars.js +++ b/.jest/setEnvVars.js @@ -1,5 +1,12 @@ // Environments for testing -process.env.SPLIT_EVALUATOR_ENVIRONMENTS='[{"API_KEY":"localhost","AUTH_TOKEN":"test"},{"API_KEY":"apikey1","AUTH_TOKEN":"key_blue"},{"API_KEY":"apikey2","AUTH_TOKEN":"key_red"}]' +process.env.SPLIT_EVALUATOR_ENVIRONMENTS = `[ + {"API_KEY":"localhost","AUTH_TOKEN":"test"}, + {"API_KEY":"apikey1","AUTH_TOKEN":"key_blue"}, + {"API_KEY":"apikey2","AUTH_TOKEN":"key_red"}, + {"API_KEY":"apikey3","AUTH_TOKEN":"key_green","FLAG_SET_FILTER":"set_green"}, + {"API_KEY":"apikey4","AUTH_TOKEN":"key_purple","FLAG_SET_FILTER":"set_purple"}, + {"API_KEY":"apikey5","AUTH_TOKEN":"key_pink","FLAG_SET_FILTER":"set_green,set_purple"} +]`; // Before all tests, sdk module is mocked to create a wrapper where a different yaml file is assigned to each environment // sdk factory mock to set a different yaml for each apikey and localhost mode @@ -33,6 +40,13 @@ jest.mock('../sdk', () => ({ ...settings.core, authorizationKey: authorizationKey, }, + urls: { + sdk: 'https://sdk.test.io/api', + events: 'https://events.test.io/api', + auth: 'https://auth.test.io/api', + streaming: 'https://streaming.test.io', + telemetry: 'https://telemetry.test.io/api', + }, startup: { readyTimeout: 1, }, @@ -47,7 +61,7 @@ jest.mock('../sdk', () => ({ }; let sdk = jest.requireActual('../sdk'); - const { factory, telemetry, impressionsMode } = sdk.getSplitFactory(configForMock); + const { factory, impressionsMode } = sdk.getSplitFactory(configForMock); const mockedTelemetry = { splits: { diff --git a/admin/__tests__/stats.test.js b/admin/__tests__/stats.test.js index d77499e..5285708 100644 --- a/admin/__tests__/stats.test.js +++ b/admin/__tests__/stats.test.js @@ -78,6 +78,7 @@ describe('stats', () => { const authToken = environment.AUTH_TOKEN; const apiKey = environment.API_KEY; const mock = apiKeyMocksMap[apiKey]; + if (!mock) return; expect(stats.environments[utils.obfuscate(authToken)]).toEqual({ splitCount: mock.splitNames.length, segmentCount: mock.segments.length, diff --git a/client/__tests__/treatmentsByFlagSets.test.js b/client/__tests__/treatmentsByFlagSets.test.js new file mode 100644 index 0000000..bf218ea --- /dev/null +++ b/client/__tests__/treatmentsByFlagSets.test.js @@ -0,0 +1,301 @@ +const request = require('supertest'); +const app = require('../../app'); +const { expectError, expectErrorContaining, expectOkMultipleResults, getLongKey } = require('../../utils/testWrapper'); +const { NULL_FLAG_SETS, EMPTY_FLAG_SETS } = require('../../utils/constants'); +const { expectedGreenResults, expectedPurpleResults, expectedPinkResults } = require('../../utils/mocks'); + +jest.mock('node-fetch', () => { + return jest.fn().mockImplementation((url) => { + + const sdkUrl = 'https://sdk.test.io/api/splitChanges?since=-1'; + const splitChange2 = require('../../utils/mocks/splitchanges.since.-1.till.1602796638344.json'); + if (url.startsWith(sdkUrl)) return Promise.resolve({ status: 200, json: () => (splitChange2), ok: true }); + + return Promise.resolve({ status: 200, json: () => ({}), ok: true }); + }); +}); + +describe('get-treatments-by-sets', () => { + + beforeEach(() => { + jest.resetModules(); + jest.clearAllMocks(); + }); + + afterAll(() => { + // Unmock fetch + jest.unmock('node-fetch'); + }); + + // Testing authorization + test('should be 401 if auth is not passed', async (done) => { + const response = await request(app) + .get('/client/get-treatments-by-sets?key=test&flag-sets=my-experiment'); + expectError(response, 401, 'Unauthorized'); + done(); + }); + + test('should be 401 if auth does not match', async (done) => { + const response = await request(app) + .get('/client/get-treatments-by-sets?key=test&flag-sets=my-experiment') + .set('Authorization', 'invalid'); + expectError(response, 401, 'Unauthorized'); + done(); + }); + + // Testing Input Validation. + // The following tests are going to check null parameters, wrong types or lengths. + test('should be 400 if key is not passed', async (done) => { + const expected = [ + 'you passed a null or undefined key, key must be a non-empty string.' + ]; + const response = await request(app) + .get('/client/get-treatments-by-sets?flag-sets=test') + .set('Authorization', 'key_green'); + expectErrorContaining(response, 400, expected); + done(); + }); + + test('should be 400 if key is empty', async (done) => { + const expected = [ + 'you passed an empty string, key must be a non-empty string.' + ]; + const response = await request(app) + .get('/client/get-treatments-by-sets?key=&flag-sets=test') + .set('Authorization', 'key_green'); + expectErrorContaining(response, 400, expected); + done(); + }); + + test('should be 400 if key is empty trimmed', async (done) => { + const expected = [ + 'you passed an empty string, key must be a non-empty string.' + ]; + const response = await request(app) + .get('/client/get-treatments-by-sets?key= &flag-sets=test') + .set('Authorization', 'key_green'); + expectErrorContaining(response, 400, expected); + done(); + }); + + test('should be 400 if key is too long', async (done) => { + const expected = [ + 'key too long, key must be 250 characters or less.' + ]; + let key = ''; + for (let i = 0; i <=250; i++) { + key += 'a'; + } + const response = await request(app) + .get(`/client/get-treatments-by-sets?key=${key}&flag-sets=test`) + .set('Authorization', 'key_green'); + expectErrorContaining(response, 400, expected); + done(); + }); + + test('should be 400 if bucketing-key is empty', async (done) => { + const expected = [ + 'you passed an empty string, bucketing-key must be a non-empty string.' + ]; + const response = await request(app) + .get('/client/get-treatments-by-sets?key=key&bucketing-key=&flag-sets=test') + .set('Authorization', 'key_green'); + expectErrorContaining(response, 400, expected); + done(); + }); + + test('should be 400 if bucketing-key is empty trimmed', async (done) => { + const expected = [ + 'you passed an empty string, bucketing-key must be a non-empty string.' + ]; + const response = await request(app) + .get('/client/get-treatments-by-sets?key=key&bucketing-key= &flag-sets=test') + .set('Authorization', 'key_green'); + expectErrorContaining(response, 400, expected); + done(); + }); + + test('should be 400 if bucketing-key is too long', async (done) => { + const expected = [ + 'bucketing-key too long, bucketing-key must be 250 characters or less.' + ]; + let key = ''; + for (let i = 0; i <=250; i++) { + key += 'a'; + } + const response = await request(app) + .get(`/client/get-treatments-by-sets?key=key&bucketing-key=${key}&flag-sets=test`) + .set('Authorization', 'key_green'); + expectErrorContaining(response, 400, expected); + done(); + }); + + test('should be 400 if flag-sets is not passed', async (done) => { + const expected = [ + NULL_FLAG_SETS + ]; + const response = await request(app) + .get('/client/get-treatments-by-sets?key=test') + .set('Authorization', 'key_green'); + expectErrorContaining(response, 400, expected); + done(); + }); + + test('should be 400 if flag-sets is empty', async (done) => { + const expected = [ + EMPTY_FLAG_SETS + ]; + const response = await request(app) + .get('/client/get-treatments-by-sets?key=test&flag-sets=') + .set('Authorization', 'key_green'); + expectErrorContaining(response, 400, expected); + done(); + }); + + test('should be 400 if flag-sets is empty trimmed', async (done) => { + const expected = [ + EMPTY_FLAG_SETS + ]; + const response = await request(app) + .get('/client/get-treatments-by-sets?key=test&flag-sets= ') + .set('Authorization', 'key_green'); + expectErrorContaining(response, 400, expected); + done(); + }); + + test('should be 400 if there are errors in key and flag-sets', async (done) => { + const expected = [ + 'you passed an empty string, key must be a non-empty string.', + EMPTY_FLAG_SETS + ]; + const response = await request(app) + .get('/client/get-treatments-by-sets?key=&flag-sets= ') + .set('Authorization', 'key_green'); + expectErrorContaining(response, 400, expected); + done(); + }); + + test('should be 400 if attributes is invalid', async (done) => { + const expected = [ + 'attributes must be a plain object.' + ]; + const response = await request(app) + .get('/client/get-treatments-by-sets?key=test&flag-sets=my-experiment&attributes=lalala') + .set('Authorization', 'key_green'); + expectErrorContaining(response, 400, expected); + done(); + }); + + test('should be 400 if there are multiple errors', async (done) => { + const expected = [ + 'you passed an empty string, key must be a non-empty string.', + EMPTY_FLAG_SETS, + 'attributes must be a plain object.' + ]; + const response = await request(app) + .get('/client/get-treatments-by-sets?key= &flag-sets=&attributes="lalala"') + .set('Authorization', 'key_green'); + expectErrorContaining(response, 400, expected); + done(); + }); + + test('should be 400 if there are multiple errors in every input', async (done) => { + const expected = [ + 'you passed an empty string, key must be a non-empty string.', + EMPTY_FLAG_SETS, + 'attributes must be a plain object.', + 'bucketing-key too long, bucketing-key must be 250 characters or less.' + ]; + const key = getLongKey(); + const response = await request(app) + .get(`/client/get-treatments-by-sets?bucketing-key=${key}&key= &flag-sets=&attributes="lalala"`) + .set('Authorization', 'key_green'); + expectErrorContaining(response, 400, expected); + done(); + }); + + test('should be 400 if attributes is an invalid json (POST)', async (done) => { + const response = await request(app) + .post('/client/get-treatments-by-sets?key=test&flag-sets=my-experiment') + .set('Content-Type', 'application/json') + // eslint-disable-next-line no-useless-escape + .send('\|\\\"/regex/i') // Syntax error parsing the JSON. + .set('Authorization', 'key_green'); + expectError(response, 400); + expect(response.body.error.type).toBe('entity.parse.failed'); // validate the error + done(); + }); + + test('should be 200 if is valid attributes (GET)', async (done) => { + const response = await request(app) + .get('/client/get-treatments-by-sets?key=key_green&flag-sets=set_green&attributes={"test":"test"}') + .set('Authorization', 'key_green'); + expectOkMultipleResults(response, 200, expectedGreenResults, 3); + done(); + }); + + test('should be 200 when attributes is null (GET)', async (done) => { + const response = await request(app) + .get('/client/get-treatments-by-sets?key=key_green&flag-sets=set_green') + .set('Authorization', 'key_green'); + expectOkMultipleResults(response, 200, expectedGreenResults, 3); + done(); + }); + + test('should be 200 if is valid attributes (POST)', async (done) => { + const response = await request(app) + .post('/client/get-treatments-by-sets?key=key_green&flag-sets=set_green') + .set('Authorization', 'key_green') + .send({ + attributes: {test:'test'}, + }); + expectOkMultipleResults(response, 200, expectedGreenResults, 3); + done(); + }); + + test('should be 200 if is valid attributes as string (POST)', async (done) => { + const response = await request(app) + .post('/client/get-treatments-by-sets?key=key_green&flag-sets=set_green') + .send(JSON.stringify({ + attributes: {test:'test'}, + })) + .set('Authorization', 'key_green'); + expectOkMultipleResults(response, 200, expectedGreenResults, 3); + done(); + }); + + test('should be 200 if attributes is null (POST)', async (done) => { + const response = await request(app) + .post('/client/get-treatments-by-sets?key=key_green&flag-sets=set_green') + .send({ + attributes: null, + }) + .set('Authorization', 'key_green'); + expectOkMultipleResults(response, 200, expectedGreenResults, 3); + done(); + }); + + test('should be 200 with multiple evaluation but evualuate configured flag sets', async (done) => { + const response = await request(app) + .get('/client/get-treatments-by-sets?key=key_green&flag-sets=set_green,set_purple,nonexistant-experiment') + .set('Authorization', 'key_green'); + expectOkMultipleResults(response, 200, expectedGreenResults, 3); + done(); + }); + + test('should be 200 with multiple evaluation but evualuate configured flag sets', async (done) => { + const response = await request(app) + .get('/client/get-treatments-by-sets?key=key_purple&flag-sets=set_green,set_purple,nonexistant-experiment') + .set('Authorization', 'key_purple'); + expectOkMultipleResults(response, 200, expectedPurpleResults, 3); + done(); + }); + + test('should be 200 with multiple evaluation but evualuate configured flag sets', async (done) => { + const response = await request(app) + .get('/client/get-treatments-by-sets?key=key_purple&flag-sets=set_green,set_purple,nonexistant-experiment') + .set('Authorization', 'key_pink'); + expectOkMultipleResults(response, 200, expectedPinkResults, 5); + done(); + }); +}); diff --git a/client/__tests__/treatmentsWithConfigByFlagSets.test.js b/client/__tests__/treatmentsWithConfigByFlagSets.test.js new file mode 100644 index 0000000..9631c41 --- /dev/null +++ b/client/__tests__/treatmentsWithConfigByFlagSets.test.js @@ -0,0 +1,276 @@ +const request = require('supertest'); +const app = require('../../app'); +const { expectError, expectErrorContaining, expectOkMultipleResults, getLongKey } = require('../../utils/testWrapper'); +const { NULL_FLAG_SETS, EMPTY_FLAG_SETS } = require('../../utils/constants'); +const { expectedGreenResultsWithConfig, expectedPurpleResultsWithConfig, expectedPinkResultsWithConfig } = require('../../utils/mocks'); + +jest.mock('node-fetch', () => { + return jest.fn().mockImplementation((url) => { + const sdkUrl = 'https://sdk.test.io/api/splitChanges?since=-1'; + const splitChange2 = require('../../utils/mocks/splitchanges.since.-1.till.1602796638344.json'); + if (url.startsWith(sdkUrl)) return Promise.resolve({ status: 200, json: () => (splitChange2), ok: true}); + return Promise.resolve({ status: 200, json: () => ({}), ok: true }); + }); +}); + +describe('get-treatments-with-config-by-sets', () => { + + beforeEach(() => { + jest.resetModules(); + jest.clearAllMocks(); + }); + + afterAll(() => { + // Unmock fetch + jest.unmock('node-fetch'); + }); + + // Testing authorization + test('should be 401 if auth is not passed', async (done) => { + const response = await request(app) + .get('/client/get-treatments-with-config-by-sets?key=test&flag-sets=my-experiment'); + expectError(response, 401, 'Unauthorized'); + done(); + }); + + test('should be 401 if auth does not match', async (done) => { + const response = await request(app) + .get('/client/get-treatments-with-config-by-sets?key=test&flag-sets=my-experiment') + .set('Authorization', 'invalid'); + expectError(response, 401, 'Unauthorized'); + done(); + }); + + // Testing Input Validation + test('should be 400 if key is not passed', async (done) => { + const expected = [ + 'you passed a null or undefined key, key must be a non-empty string.' + ]; + const response = await request(app) + .get('/client/get-treatments-with-config-by-sets?flag-sets=test') + .set('Authorization', 'key_green'); + expectErrorContaining(response, 400, expected); + done(); + }); + + test('should be 400 if key is empty', async (done) => { + const expected = [ + 'you passed an empty string, key must be a non-empty string.' + ]; + const response = await request(app) + .get('/client/get-treatments-with-config-by-sets?key=&flag-sets=test') + .set('Authorization', 'key_green'); + expectErrorContaining(response, 400, expected); + done(); + }); + + test('should be 400 if key is empty trimmed', async (done) => { + const expected = [ + 'you passed an empty string, key must be a non-empty string.' + ]; + const response = await request(app) + .get('/client/get-treatments-with-config-by-sets?key= &flag-sets=test') + .set('Authorization', 'key_green'); + expectErrorContaining(response, 400, expected); + done(); + }); + + test('should be 400 if key is too long', async (done) => { + const expected = [ + 'key too long, key must be 250 characters or less.' + ]; + const key = getLongKey(); + const response = await request(app) + .get(`/client/get-treatments-with-config-by-sets?key=${key}&flag-sets=test`) + .set('Authorization', 'key_green'); + expectErrorContaining(response, 400, expected); + done(); + }); + + test('should be 400 if bucketing-key is empty', async (done) => { + const expected = [ + 'you passed an empty string, bucketing-key must be a non-empty string.' + ]; + const response = await request(app) + .get('/client/get-treatments-with-config-by-sets?key=key&bucketing-key=&flag-sets=test') + .set('Authorization', 'key_green'); + expectErrorContaining(response, 400, expected); + done(); + }); + + test('should be 400 if bucketing-key is empty trimmed', async (done) => { + const expected = [ + 'you passed an empty string, bucketing-key must be a non-empty string.' + ]; + const response = await request(app) + .get('/client/get-treatments-with-config-by-sets?key=key&bucketing-key= &flag-sets=test') + .set('Authorization', 'key_green'); + expectErrorContaining(response, 400, expected); + done(); + }); + + test('should be 400 if bucketing-key is too long', async (done) => { + const expected = [ + 'bucketing-key too long, bucketing-key must be 250 characters or less.' + ]; + const key = getLongKey(); + const response = await request(app) + .get(`/client/get-treatments-with-config-by-sets?key=key&bucketing-key=${key}&flag-sets=test`) + .set('Authorization', 'key_green'); + expectErrorContaining(response, 400, expected); + done(); + }); + + test('should be 400 if flag-sets is not passed', async (done) => { + const expected = [ + NULL_FLAG_SETS + ]; + const response = await request(app) + .get('/client/get-treatments-with-config-by-sets?key=test') + .set('Authorization', 'key_green'); + expectErrorContaining(response, 400, expected); + done(); + }); + + test('should be 400 if flag-sets is empty', async (done) => { + const expected = [ + EMPTY_FLAG_SETS + ]; + const response = await request(app) + .get('/client/get-treatments-with-config-by-sets?key=test&flag-sets=') + .set('Authorization', 'key_green'); + expectErrorContaining(response, 400, expected); + done(); + }); + + test('should be 400 if flag-sets is empty trimmed', async (done) => { + const expected = [ + EMPTY_FLAG_SETS + ]; + const response = await request(app) + .get('/client/get-treatments-with-config-by-sets?key=test&flag-sets= ') + .set('Authorization', 'key_green'); + expectErrorContaining(response, 400, expected); + done(); + }); + + test('should be 400 if there are errors in key and flag-sets', async (done) => { + const expected = [ + 'you passed an empty string, key must be a non-empty string.', + EMPTY_FLAG_SETS + ]; + const response = await request(app) + .get('/client/get-treatments-with-config-by-sets?key=&flag-sets= ') + .set('Authorization', 'key_green'); + expectErrorContaining(response, 400, expected); + done(); + }); + + test('should be 400 if attributes is invalid', async (done) => { + const expected = [ + 'attributes must be a plain object.' + ]; + const response = await request(app) + .get('/client/get-treatments-with-config-by-sets?key=test&flag-sets=my-experiment&attributes=lalala') + .set('Authorization', 'key_green'); + expectErrorContaining(response, 400, expected); + done(); + }); + + test('should be 400 if there are multiple errors', async (done) => { + const expected = [ + 'you passed an empty string, key must be a non-empty string.', + EMPTY_FLAG_SETS, + 'attributes must be a plain object.' + ]; + const response = await request(app) + .get('/client/get-treatments-with-config-by-sets?key= &flag-sets=&attributes="lalala"') + .set('Authorization', 'key_green'); + expectErrorContaining(response, 400, expected); + done(); + }); + + test('should be 400 if there are multiple errors in every input', async (done) => { + const expected = [ + 'you passed an empty string, key must be a non-empty string.', + EMPTY_FLAG_SETS, + 'attributes must be a plain object.', + 'bucketing-key too long, bucketing-key must be 250 characters or less.' + ]; + const key = getLongKey(); + const response = await request(app) + .get(`/client/get-treatments-with-config-by-sets?bucketing-key=${key}&key= &flag-sets=&attributes="lalala"`) + .set('Authorization', 'key_green'); + expectErrorContaining(response, 400, expected); + done(); + }); + + test('should be 200 if is valid attributes (GET)', async (done) => { + const response = await request(app) + .get('/client/get-treatments-with-config-by-sets?key=key_green&flag-sets=set_green&attributes={"test":"test"}') + .set('Authorization', 'key_green'); + expectOkMultipleResults(response, 200, expectedGreenResultsWithConfig, 3); + done(); + }); + + test('should be 200 when attributes is null (GET)', async (done) => { + const response = await request(app) + .get('/client/get-treatments-with-config-by-sets?key=key_green&flag-sets=set_green') + .set('Authorization', 'key_green'); + expectOkMultipleResults(response, 200, expectedGreenResultsWithConfig, 3); + done(); + }); + + test('should be 200 if is valid attributes (POST)', async (done) => { + const response = await request(app) + .post('/client/get-treatments-with-config-by-sets?key=key_green&flag-sets=set_green') + .send({attributes: { test:'test' }}) + .set('Authorization', 'key_green'); + expectOkMultipleResults(response, 200, expectedGreenResultsWithConfig, 3); + done(); + }); + + test('should be 200 if is valid attributes stringified (POST)', async (done) => { + const response = await request(app) + .get('/client/get-treatments-with-config-by-sets?key=key_green&flag-sets=set_green&attributes={"test":"test"}') + .send(JSON.stringify({attributes: { test:'test' }})) + .set('Authorization', 'key_green'); + expectOkMultipleResults(response, 200, expectedGreenResultsWithConfig, 3); + done(); + }); + + test('should be 200 when attributes is null (POST)', async (done) => { + const response = await request(app) + .get('/client/get-treatments-with-config-by-sets?key=key_green&flag-sets=set_green') + .send({ + attributes: null, + }) + .set('Authorization', 'key_green'); + expectOkMultipleResults(response, 200, expectedGreenResultsWithConfig, 3); + done(); + }); + + test('should be 200 with multiple evaluation but evualuate configured flag sets', async (done) => { + const response = await request(app) + .get('/client/get-treatments-with-config-by-sets?key=key_green&flag-sets=set_green,set_purple,nonexistant-experiment') + .set('Authorization', 'key_green'); + expectOkMultipleResults(response, 200, expectedGreenResultsWithConfig, 3); + done(); + }); + + test('should be 200 with multiple evaluation but evualuate configured flag sets', async (done) => { + const response = await request(app) + .get('/client/get-treatments-with-config-by-sets?key=key_purple&flag-sets=set_green,set_purple,nonexistant-experiment') + .set('Authorization', 'key_purple'); + expectOkMultipleResults(response, 200, expectedPurpleResultsWithConfig, 3); + done(); + }); + + test('should be 200 with multiple evaluation but evualuate configured flag sets', async (done) => { + const response = await request(app) + .get('/client/get-treatments-with-config-by-sets?key=key_purple&flag-sets=set_green,set_purple,nonexistant-experiment') + .set('Authorization', 'key_pink'); + expectOkMultipleResults(response, 200, expectedPinkResultsWithConfig, 5); + done(); + }); +}); diff --git a/client/client.controller.js b/client/client.controller.js index ff9eba4..ea3b223 100644 --- a/client/client.controller.js +++ b/client/client.controller.js @@ -100,6 +100,57 @@ const getTreatmentsWithConfig = async (req, res) => { } }; +/** + * getTreatmentsByFlagSets evaluates an array of flag sets and returns configs also + * @param {*} req + * @param {*} res + */ +const getTreatmentsByFlagSets = async (req, res) => { + const client = environmentManager.getClient(req.headers.authorization); + const key = parseKey(req.splitio.matchingKey, req.splitio.bucketingKey); + const flagSets = req.splitio.flagSetNames; + const attributes = req.splitio.attributes; + + try { + const evaluationResults = await client.getTreatmentsByFlagSets(key, flagSets, attributes); + environmentManager.updateLastEvaluation(req.headers.authorization); + + const result = {}; + Object.keys(evaluationResults).forEach(featureFlag => { + result[featureFlag] = { + treatment: evaluationResults[featureFlag], + }; + }); + + res.send(result); + } catch (error) { + res.status(500).send({error}); + } +}; + +/** + * getTreatmentsWithConfigByFlagSets evaluates an array of flag sets + * @param {*} req + * @param {*} res + */ +const getTreatmentsWithConfigByFlagSets = async (req, res) => { + const client = environmentManager.getClient(req.headers.authorization); + const key = parseKey(req.splitio.matchingKey, req.splitio.bucketingKey); + const flagSets = req.splitio.flagSetNames; + const attributes = req.splitio.attributes; + + try { + const evaluationResults = await client.getTreatmentsWithConfigByFlagSets(key, flagSets, attributes); + environmentManager.updateLastEvaluation(req.headers.authorization); + + const result = evaluationResults; + + res.send(result); + } catch (error) { + res.status(500).send({error}); + } +}; + /** * track events tracking * @param {*} req @@ -205,6 +256,8 @@ module.exports = { getTreatments, getTreatmentWithConfig, getTreatmentsWithConfig, + getTreatmentsByFlagSets, + getTreatmentsWithConfigByFlagSets, getAllTreatments, getAllTreatmentsWithConfig, track, diff --git a/client/client.router.js b/client/client.router.js index a018d4b..2f66630 100644 --- a/client/client.router.js +++ b/client/client.router.js @@ -2,6 +2,7 @@ const express = require('express'); const router = express.Router(); const keyValidator = require('../utils/inputValidation/key'); const splitValidator = require('../utils/inputValidation/split'); +const flagSetsValidator = require('../utils/inputValidation/flagSets'); const splitsValidator = require('../utils/inputValidation/splits'); const attributesValidator = require('../utils/inputValidation/attributes'); const trafficTypeValidator = require('../utils/inputValidation/trafficType'); @@ -76,6 +77,38 @@ const treatmentsValidation = (req, res, next) => { next(); }; +/** + * flagSetsValidation performs input validation for flag sets call. + * @param {object} req + * @param {object} res + * @param {function} next + */ +const flagSetsValidation = (req, res, next) => { + const matchingKeyValidation = keyValidator(req.query.key, 'key'); + const bucketingKeyValidation = req.query['bucketing-key'] !== undefined ? keyValidator(req.query['bucketing-key'], 'bucketing-key') : null; + const flagSetNameValidation = flagSetsValidator(req.query['flag-sets']); + const attributesValidation = attributesValidator(req.query.attributes); + + const error = parseValidators([matchingKeyValidation, bucketingKeyValidation, flagSetNameValidation, attributesValidation]); + if (error.length) { + return res + .status(400) + .send({ + error, + }); + } else { + req.splitio = { + matchingKey: matchingKeyValidation.value, + flagSetNames: flagSetNameValidation.value, + attributes: attributesValidation.value, + }; + + if (bucketingKeyValidation && bucketingKeyValidation.valid) req.splitio.bucketingKey = bucketingKeyValidation.value; + } + + next(); +}; + /** * trackValidation performs input validation for event tracking calls. * @param {object} req @@ -161,6 +194,8 @@ router.get('/get-treatment', treatmentValidation, clientController.getTreatment) router.get('/get-treatment-with-config', treatmentValidation, clientController.getTreatmentWithConfig); router.get('/get-treatments', treatmentsValidation, clientController.getTreatments); router.get('/get-treatments-with-config', treatmentsValidation, clientController.getTreatmentsWithConfig); +router.get('/get-treatments-by-sets', flagSetsValidation, clientController.getTreatmentsByFlagSets); +router.get('/get-treatments-with-config-by-sets', flagSetsValidation, clientController.getTreatmentsWithConfigByFlagSets); router.get('/get-all-treatments', allTreatmentValidation, clientController.getAllTreatments); router.get('/get-all-treatments-with-config', allTreatmentValidation, clientController.getAllTreatmentsWithConfig); @@ -170,6 +205,8 @@ router.post('/get-treatment',express.json(JSON_PARSE_OPTS), fwdAttributesFromPos router.post('/get-treatment-with-config', express.json(JSON_PARSE_OPTS), fwdAttributesFromPost, handleBodyParserErr, treatmentValidation, clientController.getTreatmentWithConfig); router.post('/get-treatments', express.json(JSON_PARSE_OPTS), fwdAttributesFromPost, handleBodyParserErr, treatmentsValidation, clientController.getTreatments); router.post('/get-treatments-with-config', express.json(JSON_PARSE_OPTS), fwdAttributesFromPost, handleBodyParserErr, treatmentsValidation, clientController.getTreatmentsWithConfig); +router.post('/get-treatments-by-sets', express.json(JSON_PARSE_OPTS), fwdAttributesFromPost, handleBodyParserErr, flagSetsValidation, clientController.getTreatmentsByFlagSets); +router.post('/get-treatments-with-config-by-sets', express.json(JSON_PARSE_OPTS), fwdAttributesFromPost, handleBodyParserErr, flagSetsValidation, clientController.getTreatmentsWithConfigByFlagSets); router.post('/get-all-treatments', express.json(JSON_PARSE_OPTS), fwdAttributesFromPost, handleBodyParserErr, allTreatmentValidation, clientController.getAllTreatments); router.post('/get-all-treatments-with-config', express.json(JSON_PARSE_OPTS), fwdAttributesFromPost, handleBodyParserErr, allTreatmentValidation, clientController.getAllTreatmentsWithConfig); diff --git a/environmentManager/__tests__/globalConfig.test.js b/environmentManager/__tests__/globalConfig.test.js index a944273..d387b62 100644 --- a/environmentManager/__tests__/globalConfig.test.js +++ b/environmentManager/__tests__/globalConfig.test.js @@ -64,6 +64,46 @@ describe('environmentManager', () => { // impressionsMode should be NONE as configured in global config expect(factorySettings.sync.impressionsMode).toBe('NONE'); }); + await environmentManagerFactory.destroy(); + }); + }); + + describe('flag sets', () => { + test('Environment manager should initialize for legacy configuration without filters', async () => { + delete process.env.SPLIT_EVALUATOR_ENVIRONMENTS; + process.env.SPLIT_EVALUATOR_AUTH_TOKEN = 'test'; + process.env.SPLIT_EVALUATOR_API_KEY = 'test'; + const environmentManagerFactory = require('../'); + expect(() => environmentManagerFactory.getInstance()).not.toThrow(); + expect(environmentManagerFactory.hasInstance()).toBe(true); + await environmentManagerFactory.destroy(); + }); + + test('Environment manager should throw an error if is initialized with environments and filters on global config', async () => { + process.env.SPLIT_EVALUATOR_GLOBAL_CONFIG = JSON.stringify({ + urls: urls, + sync: { + splitFilters: [{type: 'bySet', values: ['set_a', 'set_b']}], + }, + }); + process.env.SPLIT_EVALUATOR_ENVIRONMENTS = JSON.stringify(environmentsConfig); + expect(() => require('../')).toThrow(); + }); + + test('Environment manager should initialize for legacy configuration with filters', async () => { + delete process.env.SPLIT_EVALUATOR_ENVIRONMENTS; + process.env.SPLIT_EVALUATOR_GLOBAL_CONFIG = JSON.stringify({ + urls: urls, + sync: { + splitFilters: [{type: 'bySet', values: ['set_a', 'set_b']}], + }, + }); + + const environmentManagerFactory = require('../'); + const environmentManager = environmentManagerFactory.getInstance(); + const factorySettings = environmentManager.getFactory(process.env.SPLIT_EVALUATOR_AUTH_TOKEN).settings; + expect(factorySettings.sync.splitFilters).toEqual([{type: 'bySet', values: ['set_a', 'set_b']}]); + await environmentManagerFactory.destroy(); }); }); }); diff --git a/environmentManager/__tests__/manager.test.js b/environmentManager/__tests__/manager.test.js index 0a7a374..e2bc381 100644 --- a/environmentManager/__tests__/manager.test.js +++ b/environmentManager/__tests__/manager.test.js @@ -1,9 +1,30 @@ const request = require('supertest'); const app = require('../../app'); +jest.mock('node-fetch', () => { + return jest.fn().mockImplementation((url) => { + + const sdkUrl = 'https://sdk.test.io/api/splitChanges?since=-1'; + const splitChange2 = require('../../utils/mocks/splitchanges.since.-1.till.1602796638344.json'); + if (url.startsWith(sdkUrl)) return Promise.resolve({ status: 200, json: () => (splitChange2), ok: true }); + + return Promise.resolve({ status: 200, json: () => ({}), ok: true }); + }); +}); + // Multiple environment - manager endpoints describe('environmentManager - manager endpoints', () => { + beforeEach(() => { + jest.resetModules(); + jest.clearAllMocks(); + }); + + afterAll(() => { + // Unmock fetch + jest.unmock('node-fetch'); + }); + // splits test('[/splits] should be 200 if is valid authToken and return feature flags on split2 yaml file for key_red', async () => { const response = await request(app) @@ -35,7 +56,41 @@ describe('environmentManager - manager endpoints', () => { expect(response.body).toEqual({'error':'Unauthorized'}); }); + test('[/splits] should be 200 if is valid authToken and return feature flags on set set_green for key_green', async (done) => { + const response = await request(app) + .get('/manager/splits') + .set('Authorization', 'key_green'); + expect(response.statusCode).toBe(200); + expect(response.body.splits.map(flag => {return flag.name;})) + .toEqual( + ['test_green', 'test_color', 'test_green_config'] + ); + done(); + }); + test('[/splits] should be 200 if is valid authToken and return feature flags on set set_purple for key_purple', async (done) => { + const response = await request(app) + .get('/manager/splits') + .set('Authorization', 'key_purple'); + expect(response.statusCode).toBe(200); + expect(response.body.splits.map(flag => {return flag.name;})) + .toEqual( + ['test_color', 'test_purple', 'test_purple_config'] + ); + done(); + }); + + test('[/splits] should be 200 if is valid authToken and return feature flags on sets set_green & set_purple file for key_pink', async (done) => { + const response = await request(app) + .get('/manager/splits') + .set('Authorization', 'key_pink'); + expect(response.statusCode).toBe(200); + expect(response.body.splits.map(flag => {return flag.name;})) + .toEqual( + ['test_green', 'test_color', 'test_green_config', 'test_purple', 'test_purple_config'] + ); + done(); + }); // split test('[/split] should be 200 if is valid authToken and return feature flag testing_split_red for key_red', async () => { @@ -62,6 +117,49 @@ describe('environmentManager - manager endpoints', () => { expect(response.body).toEqual({'error':'Unauthorized'}); }); + test('[/split] should be 200 if is valid authToken and return feature flag test_green for key_green', async () => { + const response = await request(app) + .get('/manager/split?split-name=test_green') + .set('Authorization', 'key_green'); + expect(response.statusCode).toBe(200); + expect(response.body.name).toEqual('test_green'); + expect(response.body.sets).toEqual(['set_green']); + }); + + test('[/split] should be 200 if is valid authToken and return feature flag test_purple for key_purple', async () => { + const response = await request(app) + .get('/manager/split?split-name=test_purple') + .set('Authorization', 'key_purple'); + expect(response.statusCode).toBe(200); + expect(response.body.name).toEqual('test_purple'); + expect(response.body.sets).toEqual(['set_purple']); + }); + + test('[/split] should be 404 if is valid authToken and return 404 for test_green using key_purple', async () => { + const response = await request(app) + .get('/manager/split?split-name=test_green') + .set('Authorization', 'key_purple'); + expect(response.statusCode).toBe(404); + }); + + test('[/split] should be 200 if is valid authToken and return feature flag test_green for key_pink', async () => { + const response = await request(app) + .get('/manager/split?split-name=test_green') + .set('Authorization', 'key_pink'); + expect(response.statusCode).toBe(200); + expect(response.body.name).toEqual('test_green'); + expect(response.body.sets).toEqual(['set_green']); + }); + + test('[/split] should be 200 if is valid authToken and return feature flag test_purple for key_pink', async () => { + const response = await request(app) + .get('/manager/split?split-name=test_purple') + .set('Authorization', 'key_pink'); + expect(response.statusCode).toBe(200); + expect(response.body.name).toEqual('test_purple'); + expect(response.body.sets).toEqual(['set_purple']); + }); + // names @@ -95,4 +193,40 @@ describe('environmentManager - manager endpoints', () => { expect(response.body).toEqual({'error':'Unauthorized'}); }); -}); \ No newline at end of file + test('[/names] should be 200 if is valid authToken and return feature flags on set set_green for key_green', async (done) => { + const response = await request(app) + .get('/manager/names') + .set('Authorization', 'key_green'); + expect(response.statusCode).toBe(200); + expect(response.body.splits) + .toEqual( + ['test_green', 'test_color', 'test_green_config'] + ); + done(); + }); + + test('[/names] should be 200 if is valid authToken and return feature flags on set set_purple for key_purple', async (done) => { + const response = await request(app) + .get('/manager/names') + .set('Authorization', 'key_purple'); + expect(response.statusCode).toBe(200); + expect(response.body.splits) + .toEqual( + ['test_color', 'test_purple', 'test_purple_config'] + ); + done(); + }); + + test('[/names] should be 200 if is valid authToken and return feature flags on sets set_green & set_purple file for key_pink', async (done) => { + const response = await request(app) + .get('/manager/names') + .set('Authorization', 'key_pink'); + expect(response.statusCode).toBe(200); + expect(response.body.splits) + .toEqual( + ['test_green', 'test_color', 'test_green_config', 'test_purple', 'test_purple_config'] + ); + done(); + }); + +}); diff --git a/environmentManager/__tests__/validation.test.js b/environmentManager/__tests__/validation.test.js index b862a0b..c54a133 100644 --- a/environmentManager/__tests__/validation.test.js +++ b/environmentManager/__tests__/validation.test.js @@ -1,5 +1,4 @@ /* eslint-disable no-useless-escape */ -const environmentManagerFactory = require('../index.js'); const validEnvironment = '[{"API_KEY":"localhost","AUTH_TOKEN":"test"},{"API_KEY":"apikey1","AUTH_TOKEN":"key_blue"},{"API_KEY":"apikey2","AUTH_TOKEN":"key_red"}]'; const environmentNotString = [{'API_KEY':'localhost','AUTH_TOKEN':'test'},{'API_KEY':'apikey1','AUTH_TOKEN':'key_blue'},{'API_KEY':'apikey2','AUTH_TOKEN':'key_red'}]; @@ -14,10 +13,17 @@ const environmentWithAuthTokenNotString1 = '[{"API_KEY":"localhost","AUTH_TOKEN" const environmentWithAuthTokenNotString2 = '[{"API_KEY":"localhost","AUTH_TOKEN":"key_red"},{"API_KEY":"apikey1","AUTH_TOKEN":{"key":"value"}}]'; const environmentWithAuthTokenNotString3 = '[{"API_KEY":"localhost","AUTH_TOKEN":"key_red"},{"API_KEY":"apikey1","AUTH_TOKEN":true}]'; const environmentWithAuthTokenEmpty = '[{"API_KEY":"localhost","AUTH_TOKEN":""},{"API_KEY":"apikey1","AUTH_TOKEN":"key_red"}]'; +const environmentWithValidFlagSets = '[{"API_KEY":"key_green","AUTH_TOKEN":"key_green","FLAG_SET_FILTER":"set_a,set_b"},{"API_KEY":"key_red","AUTH_TOKEN":"key_red"}]'; +const environmentsWithValidFlagSets1 = '[{"API_KEY":"key_green","AUTH_TOKEN":"key_green","FLAG_SET_FILTER":"set_a"},{"API_KEY":"key_red","AUTH_TOKEN":"key_red","FLAG_SET_FILTER":"set_x"}]'; +const environmentsWithValidFlagSets2 = '[{"API_KEY":"key_green","AUTH_TOKEN":"key_green","FLAG_SET_FILTER":"set_1"},{"API_KEY":"key_red","AUTH_TOKEN":"key_red","FLAG_SET_FILTER":"set_c"}]'; +const environmentWithInvalidFlagSets1 = '[{"API_KEY":"key_green","AUTH_TOKEN":"key_green","FLAG_SET_FILTER":"Set_3,_set_4"},{"API_KEY":"key_red","AUTH_TOKEN":"key_red"}]'; +const environmentWithInvalidFlagSets2 = '[{"API_KEY":"key_green","AUTH_TOKEN":"key_green","FLAG_SET_FILTER":["set_y","set_z"]},{"API_KEY":"key_red","AUTH_TOKEN":"key_red"}]'; +const environmentWithInvalidFlagSets3 = '[{"API_KEY":"key_green","AUTH_TOKEN":"key_green","FLAG_SET_FILTER":set_t},{"API_KEY":"key_red","AUTH_TOKEN":"key_red"}]'; // Multiple environment - client endpoints -describe('environmentManager - input validations', () => { - test('SPLIT_EVALUATOR_ENVIRONMENTS ', () => { +describe('environmentManager - input validations', () => { + test('SPLIT_EVALUATOR_ENVIRONMENTS ',async () => { + const environmentManagerFactory = require('../'); // Testing environment not string process.env.SPLIT_EVALUATOR_ENVIRONMENTS=environmentNotString; @@ -70,6 +76,49 @@ describe('environmentManager - input validations', () => { // Testing environment With AuthToken Empty process.env.SPLIT_EVALUATOR_ENVIRONMENTS= validEnvironment; expect(() => environmentManagerFactory.getInstance()).not.toThrow(); + await environmentManagerFactory.destroy(); + + }); + + test('Flag sets validation', async () => { + + const evaluateFlagSets = async (greenQuery, redQuery) => { + const environmentManagerFactory = require('../'); + expect(environmentManagerFactory.hasInstance()).toBe(false); + let environmentManager = environmentManagerFactory.getInstance(); + + let environmentSettings = environmentManager.getFactory('key_green').settings; + let queryString = environmentSettings.sync.__splitFiltersValidation.queryString; + expect(queryString).toStrictEqual(greenQuery); + + environmentSettings = environmentManager.getFactory('key_red').settings; + queryString = environmentSettings.sync.__splitFiltersValidation.queryString; + expect(queryString).toStrictEqual(redQuery); + await environmentManager.destroy(); + await environmentManagerFactory.destroy(); + }; + + process.env.SPLIT_EVALUATOR_ENVIRONMENTS = environmentWithValidFlagSets; + await evaluateFlagSets('&sets=set_a,set_b',null); + + process.env.SPLIT_EVALUATOR_ENVIRONMENTS = environmentsWithValidFlagSets1; + await evaluateFlagSets('&sets=set_a','&sets=set_x'); + + process.env.SPLIT_EVALUATOR_ENVIRONMENTS = environmentsWithValidFlagSets2; + await evaluateFlagSets('&sets=set_1','&sets=set_c'); + + process.env.SPLIT_EVALUATOR_ENVIRONMENTS = environmentWithInvalidFlagSets1; + await evaluateFlagSets('&sets=set_3',null); + + const environmentManagerFactory = require('../'); + + process.env.SPLIT_EVALUATOR_ENVIRONMENTS = environmentWithInvalidFlagSets2; + expect(() => environmentManagerFactory.getInstance()).toThrow(); + expect(environmentManagerFactory.hasInstance()).toBe(false); + + process.env.SPLIT_EVALUATOR_ENVIRONMENTS = environmentWithInvalidFlagSets3; + expect(() => environmentManagerFactory.getInstance()).toThrow(); + expect(environmentManagerFactory.hasInstance()).toBe(false); }); }); \ No newline at end of file diff --git a/environmentManager/index.js b/environmentManager/index.js index 5723551..333a010 100644 --- a/environmentManager/index.js +++ b/environmentManager/index.js @@ -1,10 +1,11 @@ const settings = require('../utils/parserConfigs')(); -const { validEnvironment, validEnvironmentConfig, isString, throwError } = require('../utils/parserConfigs/validators'); +const { validEnvironment, validEnvironmentConfig, isString, throwError, validFlagSets } = require('../utils/parserConfigs/validators'); const { getSplitFactory } = require('../sdk'); const { obfuscate } = require('../utils/utils'); const SPLIT_EVALUATOR_ENVIRONMENTS = 'SPLIT_EVALUATOR_ENVIRONMENTS'; const SPLIT_EVALUATOR_AUTH_TOKEN = 'SPLIT_EVALUATOR_AUTH_TOKEN'; const SPLIT_EVALUATOR_API_KEY = 'SPLIT_EVALUATOR_API_KEY'; +const DEFAULT_AUTH_TOKEN = 'DEFAULT_AUTH_TOKEN'; const EnvironmentManagerFactory = (function(){ /** @@ -29,13 +30,16 @@ const EnvironmentManagerFactory = (function(){ } _initializeEnvironments(){ + + let defaultEnvironment = false; // If environments envVar is not defined, it creates an environment with auth_token and api_key envVars if (!process.env.SPLIT_EVALUATOR_ENVIRONMENTS) { + defaultEnvironment = true; const AUTH_TOKEN = process.env.SPLIT_EVALUATOR_AUTH_TOKEN; // If auth_token envVar is not defined, means that openapi security tag should not be added if (!AUTH_TOKEN) { this.requireAuth = false; - process.env.SPLIT_EVALUATOR_AUTH_TOKEN = 'splitToken'; + process.env.SPLIT_EVALUATOR_AUTH_TOKEN = DEFAULT_AUTH_TOKEN; } process.env.SPLIT_EVALUATOR_ENVIRONMENTS = `[{ "AUTH_TOKEN": "${process.env[SPLIT_EVALUATOR_AUTH_TOKEN]}", @@ -44,7 +48,6 @@ const EnvironmentManagerFactory = (function(){ } const environmentConfigs = validEnvironmentConfig(SPLIT_EVALUATOR_ENVIRONMENTS); - environmentConfigs.forEach(environment => { validEnvironment(environment); @@ -60,6 +63,14 @@ const EnvironmentManagerFactory = (function(){ throwError(`There are two or more environments with the same authToken '${authToken}' `); } + if (!defaultEnvironment) { + const flagSets = validFlagSets(environment['FLAG_SET_FILTER']); + settings.sync = { + ...settings.sync, + splitFilters: flagSets, + }; + } + const { factory, telemetry, impressionsMode} = getSplitFactory(settings); // Creates an environment for authToken @@ -105,7 +116,7 @@ const EnvironmentManagerFactory = (function(){ } getFactory(authToken) { - if (!this.requireAuth) authToken = 'splitToken'; + if (!this.requireAuth) authToken = DEFAULT_AUTH_TOKEN; return this._environments[authToken].factory; } @@ -122,7 +133,7 @@ const EnvironmentManagerFactory = (function(){ } getTelemetry(authToken) { - if (!this.requireAuth) authToken = 'splitToken'; + if (!this.requireAuth) authToken = DEFAULT_AUTH_TOKEN; const environment = this.getEnvironment(authToken); const telemetry = environment.telemetry; const stats = { @@ -152,7 +163,7 @@ const EnvironmentManagerFactory = (function(){ } updateLastEvaluation(authToken) { - if (!this.requireAuth) authToken = 'splitToken'; + if (!this.requireAuth) authToken = DEFAULT_AUTH_TOKEN; this._environments[authToken].lastEvaluation = new Date().toJSON(); } diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index d894b25..303bd6c 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -40,6 +40,14 @@ components: type: string description: The name of the feature flags you want to include in the evaluation. example: SPLIT_NAME1,SPLIT_NAME2,SPLIT_NAME3 + flag-sets: + in: query + name: flag-sets + required: true + schema: + type: string + description: The name of the flag sets you want to include in the evaluation. + example: set_a,set_b,set_c bucketing-key: in: query name: bucketing-key @@ -114,6 +122,14 @@ components: items: type: string example: ["you passed a null or undefined split-name, split-name must be a non-empty string."] + InputValidationSets: + type: object + properties: + error: + type: array + items: + type: string + example: ["you passed a null or undefined flag-sets, flag-sets must be a non-empty string."] Unauthorized: type: object properties: @@ -479,6 +495,158 @@ paths: $ref: '#/components/schemas/Unauthorized' '500': description: Internal server error + /client/get-treatments-by-sets: + get: + tags: + - client + summary: performs multiple evaluation at once by sets + description: Calls getTreatmentsByFlagSets method from the SDK. + parameters: + - $ref: '#/components/parameters/key' + - $ref: '#/components/parameters/flag-sets' + - $ref: '#/components/parameters/bucketing-key' + - $ref: '#/components/parameters/attributes' + responses: + '200': + description: Evaluation result + content: + application/json: + schema: + type: object + example: + 'split-name': + treatment: on + 'split-name-2': + treatment: off + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/InputValidationSets' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Unauthorized' + '500': + description: Internal server error + post: + tags: + - client + summary: performs multiple evaluation at once by sets, but receiving the attributes on the request body. + description: Calls getTreatmentsByFlagSets method from the SDK. + parameters: + - $ref: '#/components/parameters/key' + - $ref: '#/components/parameters/flag-sets' + - $ref: '#/components/parameters/bucketing-key' + requestBody: + $ref: '#/components/requestBodies/EvaluationRequestBody' + responses: + '200': + description: Evaluation result + content: + application/json: + schema: + type: object + example: + 'split-name': + treatment: on + 'split-name-2': + treatment: off + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/InputValidationSets' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Unauthorized' + '500': + description: Internal server error + /client/get-treatments-with-config-by-sets: + get: + tags: + - client + summary: performs multiple evaluation by sets at once and attachs config + description: Calls getTreatmentsWithConfigBySets method from the SDK. + parameters: + - $ref: '#/components/parameters/key' + - $ref: '#/components/parameters/flag-sets' + - $ref: '#/components/parameters/bucketing-key' + - $ref: '#/components/parameters/attributes' + responses: + '200': + description: Evaluation result + content: + application/json: + schema: + type: object + example: + 'split-name': + treatment: on + config: "{\"color\": \"on\" }" + 'split-name-2': + treatment: off + config: "{\"color\": \"off\" }" + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/InputValidationSets' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Unauthorized' + '500': + description: Internal server error + post: + tags: + - client + summary: performs multiple evaluation by sets at once and attachs config, but receiving the attributes on the request body. + description: Calls getTreatmentsWithConfigBySets method from the SDK. + parameters: + - $ref: '#/components/parameters/key' + - $ref: '#/components/parameters/flag-sets' + - $ref: '#/components/parameters/bucketing-key' + requestBody: + $ref: '#/components/requestBodies/EvaluationRequestBody' + responses: + '200': + description: Evaluation result + content: + application/json: + schema: + type: object + example: + 'my-split': + treatment: on + config: null + 'my-split2': + treatment: off + config: "{\"color\": \"blue\" }" + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/InputValidationSets' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Unauthorized' + '500': + description: Internal server error /client/get-all-treatments: get: tags: diff --git a/package-lock.json b/package-lock.json index 7f487b8..cc30029 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "split-evaluator", - "version": "2.4.0", + "version": "2.5.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "split-evaluator", - "version": "2.4.0", + "version": "2.5.0", "license": "Apache-2.0", "dependencies": { - "@splitsoftware/splitio": "10.23.0", + "@splitsoftware/splitio": "10.23.2-rc.1", "config": "^3.3.9", "express": "^4.17.1", "morgan": "^1.9.1", @@ -2545,11 +2545,11 @@ } }, "node_modules/@splitsoftware/splitio": { - "version": "10.23.0", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-10.23.0.tgz", - "integrity": "sha512-b9mn2B8U1DfpDETsaWH4T1jhkn8XWwlAVsHwhgIRhCgBs0B9wm4SsXx+OWHZ5bl5uvEwtFFIAtCU58j/irnqpw==", + "version": "10.23.2-rc.1", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-10.23.2-rc.1.tgz", + "integrity": "sha512-lLc6S98cKS5CmT2fr0THgcQAyrl1PUfDakgvwdmBNUw/Y1Ovou05kmSleu9gKR9Xh1oUzo/loFuFgCjtgpf7vQ==", "dependencies": { - "@splitsoftware/splitio-commons": "1.9.0", + "@splitsoftware/splitio-commons": "1.10.1-rc.0", "@types/google.analytics": "0.0.40", "@types/ioredis": "^4.28.0", "bloom-filters": "^3.0.0", @@ -2567,9 +2567,9 @@ } }, "node_modules/@splitsoftware/splitio-commons": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.9.0.tgz", - "integrity": "sha512-2QoWvGOk/LB+q2TglqGD0w/hcUKG4DZwBSt5NtmT1ODGiLyCf2wbcfG/eBR9QlUnLisJ62dj6vOQsVUB2kiHOw==", + "version": "1.10.1-rc.0", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.10.1-rc.0.tgz", + "integrity": "sha512-bot9NEg0u/suJ4gSqIWfkiRLhTc3NL6Veq1GQiD5J+dh0x/STUwUfV3pnJ+cUuPPclC1teJxAqxMdmyFKSx+NA==", "dependencies": { "tslib": "^2.3.1" }, @@ -8322,6 +8322,52 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi": { "version": "7.1.0", "inBundle": true, @@ -8336,6 +8382,94 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/npm/node_modules/@isaacs/string-locale-compare": { "version": "1.1.0", "inBundle": true, @@ -10668,20 +10802,6 @@ "node": ">=8" } }, - "node_modules/npm/node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "inBundle": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/npm/node_modules/strip-ansi": { "version": "6.0.1", "inBundle": true, @@ -10693,18 +10813,6 @@ "node": ">=8" } }, - "node_modules/npm/node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/npm/node_modules/supports-color": { "version": "9.3.1", "inBundle": true, @@ -10883,23 +10991,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/npm/node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/npm/node_modules/wrap-ansi/node_modules/ansi-regex": { "version": "6.0.1", "inBundle": true, @@ -13278,9 +13369,9 @@ } }, "node_modules/tslib": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", - "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/type-check": { "version": "0.4.0", @@ -15676,11 +15767,11 @@ } }, "@splitsoftware/splitio": { - "version": "10.23.0", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-10.23.0.tgz", - "integrity": "sha512-b9mn2B8U1DfpDETsaWH4T1jhkn8XWwlAVsHwhgIRhCgBs0B9wm4SsXx+OWHZ5bl5uvEwtFFIAtCU58j/irnqpw==", + "version": "10.23.2-rc.1", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-10.23.2-rc.1.tgz", + "integrity": "sha512-lLc6S98cKS5CmT2fr0THgcQAyrl1PUfDakgvwdmBNUw/Y1Ovou05kmSleu9gKR9Xh1oUzo/loFuFgCjtgpf7vQ==", "requires": { - "@splitsoftware/splitio-commons": "1.9.0", + "@splitsoftware/splitio-commons": "1.10.1-rc.0", "@types/google.analytics": "0.0.40", "@types/ioredis": "^4.28.0", "bloom-filters": "^3.0.0", @@ -15692,9 +15783,9 @@ } }, "@splitsoftware/splitio-commons": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.9.0.tgz", - "integrity": "sha512-2QoWvGOk/LB+q2TglqGD0w/hcUKG4DZwBSt5NtmT1ODGiLyCf2wbcfG/eBR9QlUnLisJ62dj6vOQsVUB2kiHOw==", + "version": "1.10.1-rc.0", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.10.1-rc.0.tgz", + "integrity": "sha512-bot9NEg0u/suJ4gSqIWfkiRLhTc3NL6Veq1GQiD5J+dh0x/STUwUfV3pnJ+cUuPPclC1teJxAqxMdmyFKSx+NA==", "requires": { "tslib": "^2.3.1" } @@ -20050,12 +20141,108 @@ "strip-ansi": "^7.0.1" } }, + "string-width-cjs": { + "version": "npm:string-width@4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "bundled": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "bundled": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "bundled": true + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "bundled": true, + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, "strip-ansi": { "version": "7.1.0", "bundled": true, "requires": { "ansi-regex": "^6.0.1" } + }, + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "bundled": true, + "requires": { + "ansi-regex": "^5.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "bundled": true + } + } + }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "bundled": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "bundled": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "bundled": true + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "bundled": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "bundled": true, + "requires": { + "ansi-regex": "^5.0.1" + } + } + } } } }, @@ -21565,15 +21752,6 @@ "strip-ansi": "^6.0.1" } }, - "string-width-cjs": { - "version": "npm:string-width@4.2.3", - "bundled": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, "strip-ansi": { "version": "6.0.1", "bundled": true, @@ -21581,13 +21759,6 @@ "ansi-regex": "^5.0.1" } }, - "strip-ansi-cjs": { - "version": "npm:strip-ansi@6.0.1", - "bundled": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, "supports-color": { "version": "9.3.1", "bundled": true @@ -21740,15 +21911,6 @@ } } }, - "wrap-ansi-cjs": { - "version": "npm:wrap-ansi@7.0.0", - "bundled": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, "wrappy": { "version": "1.0.2", "bundled": true @@ -23564,9 +23726,9 @@ } }, "tslib": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", - "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "type-check": { "version": "0.4.0", diff --git a/package.json b/package.json index 169f26b..9594b6f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "split-evaluator", - "version": "2.4.0", + "version": "2.5.0", "description": "Split-Evaluator", "repository": "splitio/split-evaluator", "homepage": "https://github.com/splitio/split-evaluator#readme", @@ -40,7 +40,7 @@ "test": "NODE_ENV=test jest" }, "dependencies": { - "@splitsoftware/splitio": "10.23.0", + "@splitsoftware/splitio": "10.23.2-rc.1", "config": "^3.3.9", "express": "^4.17.1", "morgan": "^1.9.1", diff --git a/utils/constants.js b/utils/constants.js new file mode 100644 index 0000000..31d0948 --- /dev/null +++ b/utils/constants.js @@ -0,0 +1,10 @@ +const TRIMMABLE_SPACES_REGEX = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/; + +const EMPTY_FLAG_SETS = 'you passed an empty flag-sets, flag-sets must be a non-empty array.'; +const NULL_FLAG_SETS = 'you passed a null or undefined flag-sets, flag-sets must be a non-empty array.'; + +module.exports = { + TRIMMABLE_SPACES_REGEX, + EMPTY_FLAG_SETS, + NULL_FLAG_SETS, +}; \ No newline at end of file diff --git a/utils/inputValidation/flagSet.js b/utils/inputValidation/flagSet.js new file mode 100644 index 0000000..3eb7e24 --- /dev/null +++ b/utils/inputValidation/flagSet.js @@ -0,0 +1,18 @@ +const errorWrapper = require('./wrapper/error'); +const okWrapper = require('./wrapper/ok'); +const { NULL_FLAG_SET, EMPTY_FLAG_SET, TRIMMABLE_SPACES_REGEX } = require('../constants'); + +const validateFlagSet = (maybeFlagSet) => { + // eslint-disable-next-line + if (maybeFlagSet == undefined) return errorWrapper(NULL_FLAG_SET); + + if (TRIMMABLE_SPACES_REGEX.test(maybeFlagSet)) { + console.log(`flag-sets "${maybeFlagSet}" has extra whitespace, trimming.`); + maybeFlagSet = maybeFlagSet.trim(); + } + if (maybeFlagSet.length === 0) return errorWrapper(EMPTY_FLAG_SET); + + return okWrapper(maybeFlagSet); +}; + +module.exports = validateFlagSet; \ No newline at end of file diff --git a/utils/inputValidation/flagSets.js b/utils/inputValidation/flagSets.js new file mode 100644 index 0000000..2b33de1 --- /dev/null +++ b/utils/inputValidation/flagSets.js @@ -0,0 +1,28 @@ +const errorWrapper = require('./wrapper/error'); +const okWrapper = require('./wrapper/ok'); +const lang = require('../lang'); +const validateFlagSet = require('./flagSet'); +const { EMPTY_FLAG_SETS, NULL_FLAG_SETS } = require('../constants'); + +const validateFlagSets = (maybeFlagSets) => { + // eslint-disable-next-line eqeqeq + if (maybeFlagSets == undefined) return errorWrapper(NULL_FLAG_SETS); + + maybeFlagSets = maybeFlagSets.split(','); + + if (maybeFlagSets.length > 0) { + let validatedArray = []; + // Remove invalid values + maybeFlagSets.forEach(maybeFlagSet => { + const flagSetValidation = validateFlagSet(maybeFlagSet); + if (flagSetValidation.valid) validatedArray.push(flagSetValidation.value); + }); + + // Strip off duplicated values if we have valid flag sets then return + if (validatedArray.length) return okWrapper(lang.uniq(validatedArray)); + } + + return errorWrapper(EMPTY_FLAG_SETS); +}; + +module.exports = validateFlagSets; \ No newline at end of file diff --git a/utils/inputValidation/split.js b/utils/inputValidation/split.js index 1fbefa6..3955df3 100644 --- a/utils/inputValidation/split.js +++ b/utils/inputValidation/split.js @@ -1,6 +1,6 @@ const errorWrapper = require('./wrapper/error'); const okWrapper = require('./wrapper/ok'); -const TRIMMABLE_SPACES_REGEX = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/; +const { TRIMMABLE_SPACES_REGEX } = require('../constants'); const validateSplit = (maybeSplit) => { // eslint-disable-next-line eqeqeq diff --git a/utils/mocks/index.js b/utils/mocks/index.js index f422881..efb60f5 100644 --- a/utils/mocks/index.js +++ b/utils/mocks/index.js @@ -74,10 +74,6 @@ const storage = { }; const sync = { - splitFilters: [{ - type: 'byName', - values: ['split_name_1', 'split_name_2'], - }], impressionsMode: 'NONE', enabled: false, }; @@ -86,4 +82,70 @@ const integrations = [{ type: 'GOOGLE_ANALYTICS_TO_SPLIT', }]; -module.exports = { core, scheduler, urls, startup, storage, sync, integrations, apiKeyMocksMap }; \ No newline at end of file +const expectedGreenResults = { + 'test_green': { + treatment: 'on', + }, + 'test_color': { + treatment: 'on', + }, + 'test_green_config': { + treatment: 'on', + }, +}; +const expectedPurpleResults = { + 'test_purple': { + treatment: 'on', + }, + 'test_color': { + treatment: 'on', + }, + 'test_purple_config': { + treatment: 'on', + }, +}; +const expectedPinkResults = { + ...expectedGreenResults, + ...expectedPurpleResults, +}; + +const expectedGreenResultsWithConfig = { + 'test_green': { + treatment: 'on', + }, + 'test_color': { + treatment: 'on', + }, + 'test_green_config': { + treatment: 'on', + config: '{"color":"green"}', + }, +}; + +const expectedPurpleResultsWithConfig = { + 'test_purple': { + treatment: 'on', + }, + 'test_color': { + treatment: 'on', + }, + 'test_purple_config': { + treatment: 'on', + config: '{"color":"purple"}', + }, +}; + +const expectedPinkResultsWithConfig = { + ...expectedGreenResultsWithConfig, + ...expectedPurpleResultsWithConfig, +}; + +module.exports = { + core, scheduler, urls, startup, storage, sync, integrations, apiKeyMocksMap, + expectedGreenResults, + expectedPurpleResults, + expectedPinkResults, + expectedGreenResultsWithConfig, + expectedPurpleResultsWithConfig, + expectedPinkResultsWithConfig, +}; \ No newline at end of file diff --git a/utils/mocks/splitchanges.since.-1.till.1602796638344.json b/utils/mocks/splitchanges.since.-1.till.1602796638344.json new file mode 100644 index 0000000..8a75190 --- /dev/null +++ b/utils/mocks/splitchanges.since.-1.till.1602796638344.json @@ -0,0 +1,225 @@ +{ + "splits": [ + { + "trafficTypeName": "client", + "name": "test_green", + "trafficAllocation": 100, + "trafficAllocationSeed": 147392224, + "seed": 524417105, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "on", + "changeNumber": 1602796638344, + "algo": 2, + "configurations": {}, + "sets": ["set_green"], + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { "trafficType": "client", "attribute": null }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { "treatment": "on", "size": 100 }, + { "treatment": "off", "size": 0 }, + { "treatment": "free", "size": 0 }, + { "treatment": "conta", "size": 0 } + ], + "label": "default rule" + } + ] + }, + { + "trafficTypeName": "client", + "name": "test_color", + "trafficAllocation": 100, + "trafficAllocationSeed": 147392224, + "seed": 524417105, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "on", + "changeNumber": 1602796638344, + "algo": 2, + "configurations": {}, + "sets": ["set_green", "set_purple"], + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { "trafficType": "client", "attribute": null }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { "treatment": "on", "size": 100 }, + { "treatment": "off", "size": 0 }, + { "treatment": "free", "size": 0 }, + { "treatment": "conta", "size": 0 } + ], + "label": "default rule" + } + ] + }, + { + "trafficTypeName": "client", + "name": "test_green_config", + "trafficAllocation": 100, + "trafficAllocationSeed": 147392224, + "seed": 524417105, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "on", + "changeNumber": 1602796638344, + "algo": 2, + "configurations": { + "on": "{\"color\":\"green\"}" + }, + "sets": ["set_green"], + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { "trafficType": "client", "attribute": null }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { "treatment": "on", "size": 100 }, + { "treatment": "off", "size": 0 }, + { "treatment": "free", "size": 0 }, + { "treatment": "conta", "size": 0 } + ], + "label": "default rule" + } + ] + }, + { + "trafficTypeName": "client", + "name": "test_purple", + "trafficAllocation": 100, + "trafficAllocationSeed": 147392224, + "seed": 524417105, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "on", + "changeNumber": 1602796638344, + "algo": 2, + "configurations": {}, + "sets": ["set_purple"], + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { "trafficType": "client", "attribute": null }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { "treatment": "on", "size": 100 }, + { "treatment": "off", "size": 0 }, + { "treatment": "free", "size": 0 }, + { "treatment": "conta", "size": 0 } + ], + "label": "default rule" + } + ] + }, + { + "trafficTypeName": "client", + "name": "test_purple_config", + "trafficAllocation": 100, + "trafficAllocationSeed": 147392224, + "seed": 524417105, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "on", + "changeNumber": 1602796638344, + "algo": 2, + "configurations": { + "on": "{\"color\":\"purple\"}" + }, + "sets": ["set_purple"], + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { "trafficType": "client", "attribute": null }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { "treatment": "on", "size": 100 }, + { "treatment": "off", "size": 0 }, + { "treatment": "free", "size": 0 }, + { "treatment": "conta", "size": 0 } + ], + "label": "default rule" + } + ] + } + ], + "since": -1, + "till": 1602796638344 +} diff --git a/utils/parserConfigs/index.js b/utils/parserConfigs/index.js index 73caec6..42fd574 100644 --- a/utils/parserConfigs/index.js +++ b/utils/parserConfigs/index.js @@ -1,5 +1,5 @@ const path = require('path'); -const { parseNumber, validUrl, validLogLevel, validGlobalConfig } = require('./validators'); +const { parseNumber, validUrl, validLogLevel, validGlobalConfig, throwError } = require('./validators'); const getConfigs = () => { let configs = { @@ -18,6 +18,12 @@ const getConfigs = () => { console.info('Setting global config'); const globalConfig = validGlobalConfig('SPLIT_EVALUATOR_GLOBAL_CONFIG'); + if (process.env.SPLIT_EVALUATOR_ENVIRONMENTS){ + if (globalConfig.sync && globalConfig.sync.splitFilters) { + throwError('Flag sets must be defined in SPLIT_EVALUATOR_ENVIRONMENTS, initialization aborted'); + } + } + configs = Object.assign(globalConfig, nulleableConfigs, configs); if (configs.sync) Object.assign(configs.sync, {enabled: undefined} ); diff --git a/utils/parserConfigs/validators.js b/utils/parserConfigs/validators.js index 3edff3a..7cdc880 100644 --- a/utils/parserConfigs/validators.js +++ b/utils/parserConfigs/validators.js @@ -86,6 +86,15 @@ const validGlobalConfig = (globalParam) => { } }; +const validFlagSets = (maybeFlagSets) => { + if (!maybeFlagSets) return; + if (!isString(maybeFlagSets)) { + throwError('you passed an invalid flag set, flag sets must be comma separated a string list'); + return; + } + return [{type: 'bySet', values: maybeFlagSets.split(',')}]; +}; + module.exports = { throwError, validUrl, @@ -96,4 +105,5 @@ module.exports = { validEnvironment, validEnvironmentConfig, validGlobalConfig, + validFlagSets, }; \ No newline at end of file diff --git a/utils/split3.yml b/utils/split3.yml new file mode 100644 index 0000000..107940d --- /dev/null +++ b/utils/split3.yml @@ -0,0 +1,9 @@ +# Always green +- testing_split_green: + treatment: "green" +- testing_split_color: + treatment: "green" +# All keys with config +- testing_split_with_config: + treatment: "green" + config: "{\"color\": \"green\"}" diff --git a/utils/split4.yml b/utils/split4.yml new file mode 100644 index 0000000..161970d --- /dev/null +++ b/utils/split4.yml @@ -0,0 +1,9 @@ +# Always purple +- testing_split_purple: + treatment: "purple" +- testing_split_color: + treatment: "purple" +# All keys with config +- testing_split_with_config: + treatment: "purple" + config: "{\"color\": \"purple\"}"