From 9292562d77c9446a6ff830243cce0c84070dcd26 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Fri, 7 Jan 2022 13:06:42 -0300 Subject: [PATCH 01/12] SDKS-5410. Create attributes cache in memory and add it to browser client --- .../__tests__/sdkClientMethodCS.spec.ts | 4 +- src/sdkClient/clientAttributesDecoration.ts | 119 ++++++++++++++++++ src/sdkClient/clientCS.ts | 26 ++-- src/sdkClient/sdkClientMethodCS.ts | 2 + src/sdkClient/sdkClientMethodCSWithTT.ts | 2 + .../inMemory/AttributesCacheInMemory.ts | 72 +++++++++++ .../__tests__/AttributesCacheInMemory.spec.ts | 36 ++++++ src/types.ts | 39 ++++++ .../__tests__/attribute.spec.ts | 18 +++ .../__tests__/attributes.spec.ts | 39 +++++- src/utils/inputValidation/attribute.ts | 21 ++++ src/utils/inputValidation/attributes.ts | 14 +++ 12 files changed, 382 insertions(+), 10 deletions(-) create mode 100644 src/sdkClient/clientAttributesDecoration.ts create mode 100644 src/storages/inMemory/AttributesCacheInMemory.ts create mode 100644 src/storages/inMemory/__tests__/AttributesCacheInMemory.spec.ts create mode 100644 src/utils/inputValidation/__tests__/attribute.spec.ts create mode 100644 src/utils/inputValidation/attribute.ts diff --git a/src/sdkClient/__tests__/sdkClientMethodCS.spec.ts b/src/sdkClient/__tests__/sdkClientMethodCS.spec.ts index 0a5c8e59..89490ef4 100644 --- a/src/sdkClient/__tests__/sdkClientMethodCS.spec.ts +++ b/src/sdkClient/__tests__/sdkClientMethodCS.spec.ts @@ -227,8 +227,8 @@ describe('sdkClientMethodCSFactory', () => { assertClientApi(client, params.sdkReadinessManager.sdkStatus); // but with false as binded key and TT - if (ignoresTT) expect(clientCSDecoratorSpy).toHaveBeenCalledWith(expect.anything(), false); - else expect(clientCSDecoratorSpy).toHaveBeenCalledWith(expect.anything(), false, false); + if (ignoresTT) expect(clientCSDecoratorSpy).toHaveBeenCalledWith(expect.anything(), expect.anything(), false); + else expect(clientCSDecoratorSpy).toHaveBeenCalledWith(expect.anything(), expect.anything(), false, false); }); }); diff --git a/src/sdkClient/clientAttributesDecoration.ts b/src/sdkClient/clientAttributesDecoration.ts new file mode 100644 index 00000000..f0a5e305 --- /dev/null +++ b/src/sdkClient/clientAttributesDecoration.ts @@ -0,0 +1,119 @@ +import { AttributesCacheInMemory } from '../storages/inMemory/AttributesCacheInMemory'; +import { validateAttributesDeep } from '../../src/utils/inputValidation/attributes'; +import { SplitIO } from '../types'; +import { ILogger } from '../logger/types'; +import { objectAssign } from '../utils/lang/objectAssign'; + +/** + * Add in memory attributes storage methods and combine them with any attribute received from the getTreatment/s call + */ +export function ClientAttributesDecoration(log: ILogger, client: TClient): any { + + const attributeStorage = new AttributesCacheInMemory(); + + // Keep a reference to the original methods + const clientGetTreatment = client.getTreatment; + const clientGetTreatmentWithConfig = client.getTreatmentWithConfig; + const clientGetTreatments = client.getTreatments; + const clientGetTreatmentsWithConfig = client.getTreatmentsWithConfig; + const clientTrack = client.track; + + /** + * Add an attribute to client's in memory attributes storage + * + * @param {string} attributeName Attrinute name + * @param {string, number, boolean, list} attributeValue Attribute value + * @returns {boolean} true if the attribute was stored and false otherways + */ + function setAttribute(attributeName: string, attributeValue: Object): boolean { + const attribute: Record = {}; + attribute[attributeName] = attributeValue; + if (!validateAttributesDeep(log, attribute, 'setAttribute')) return false; + log.debug(`stored ${attributeValue} for attribute ${attributeName}`); + return attributeStorage.setAttribute(attributeName, attributeValue); + } + + /** + * Returns the attribute with the given key + * + * @param {string} attributeName Attribute name + * @returns {Object} Attribute with the given key + */ + function getAttribute(attributeName: string): Object { + log.debug(`retrieved attribute ${attributeName}`); + return attributeStorage.getAttribute(attributeName + ''); + } + + /** + * Add to client's in memory attributes storage the attributes in 'attributes' + * + * @param {Object} attributes Object with attributes to store + * @returns true if attributes were stored an false otherways + */ + function setAttributes(attributes: Record): boolean { + if (!validateAttributesDeep(log, attributes, 'setAttributes')) return false; + return attributeStorage.setAttributes(attributes); + } + + /** + * Return all the attributes stored in client's in memory attributes storage + * + * @returns {Object} returns all the stored attributes + */ + function getAttributes(): Record { + return attributeStorage.getAll(); + } + + /** + * Removes from client's in memory attributes storage the attribute with the given key + * + * @param {string} attributeName + * @returns {boolean} true if attribute was removed and false otherways + */ + function removeAttribute(attributeName: string): boolean { + log.debug(`removed attribute ${attributeName}`); + return attributeStorage.removeAttribute(attributeName + ''); + } + + /** + * Remove all the stored attributes in the client's in memory attribute storage + */ + function clearAttributes() { + return attributeStorage.clear(); + } + + function getTreatment(maybeKey: SplitIO.SplitKey, maybeSplit: string, maybeAttributes?: SplitIO.Attributes) { + return clientGetTreatment(maybeKey, maybeSplit, maybeAttributes); + } + + function getTreatmentWithConfig(maybeKey: SplitIO.SplitKey, maybeSplit: string, maybeAttributes?: SplitIO.Attributes) { + return clientGetTreatmentWithConfig(maybeKey, maybeSplit, maybeAttributes); + } + + function getTreatments(maybeKey: SplitIO.SplitKey, maybeSplits: string[], maybeAttributes?: SplitIO.Attributes) { + return clientGetTreatments(maybeKey, maybeSplits, maybeAttributes); + } + + function getTreatmentsWithConfig(maybeKey: SplitIO.SplitKey, maybeSplits: string[], maybeAttributes?: SplitIO.Attributes) { + return clientGetTreatmentsWithConfig(maybeKey, maybeSplits, maybeAttributes); + } + + function track(maybeKey: SplitIO.SplitKey, maybeTT: string, maybeEvent: string, maybeEventValue?: number, maybeProperties?: SplitIO.Properties) { + return clientTrack(maybeKey, maybeTT, maybeEvent, maybeEventValue, maybeProperties); + } + + return objectAssign(client, { + getTreatment: getTreatment, + getTreatmentWithConfig: getTreatmentWithConfig, + getTreatments: getTreatments, + getTreatmentsWithConfig: getTreatmentsWithConfig, + track: track, + setAttribute: setAttribute, + getAttribute: getAttribute, + setAttributes: setAttributes, + getAttributes: getAttributes, + removeAttribute: removeAttribute, + clearAttributes: clearAttributes + }); + +} diff --git a/src/sdkClient/clientCS.ts b/src/sdkClient/clientCS.ts index dee6a6af..9c5ec97a 100644 --- a/src/sdkClient/clientCS.ts +++ b/src/sdkClient/clientCS.ts @@ -1,5 +1,7 @@ import { objectAssign } from '../utils/lang/objectAssign'; +import { ILogger } from '../logger/types'; import { SplitIO } from '../types'; +import { ClientAttributesDecoration } from './clientAttributesDecoration'; /** @@ -9,15 +11,25 @@ import { SplitIO } from '../types'; * @param key validated split key * @param trafficType validated traffic type */ -export function clientCSDecorator(client: SplitIO.IClient, key: SplitIO.SplitKey, trafficType?: string): SplitIO.ICsClient { - return objectAssign(client, { +export function clientCSDecorator(log: ILogger, client: SplitIO.IClient, key: SplitIO.SplitKey, trafficType?: string): SplitIO.ICsClient { + + let clientCS = ClientAttributesDecoration(log, client); + + return objectAssign(clientCS, { // In the client-side API, we bind a key to the client `getTreatment*` methods - getTreatment: client.getTreatment.bind(client, key), - getTreatmentWithConfig: client.getTreatmentWithConfig.bind(client, key), - getTreatments: client.getTreatments.bind(client, key), - getTreatmentsWithConfig: client.getTreatmentsWithConfig.bind(client, key), + getTreatment: clientCS.getTreatment.bind(clientCS, key), + getTreatmentWithConfig: clientCS.getTreatmentWithConfig.bind(clientCS, key), + getTreatments: clientCS.getTreatments.bind(clientCS, key), + getTreatmentsWithConfig: clientCS.getTreatmentsWithConfig.bind(clientCS, key), // Key is bound to the `track` method. Same thing happens with trafficType but only if provided - track: trafficType ? client.track.bind(client, key, trafficType) : client.track.bind(client, key) + track: trafficType ? clientCS.track.bind(clientCS, key, trafficType) : clientCS.track.bind(clientCS, key), + + setAttribute: clientCS.setAttribute, + getAttribute: clientCS.getAttribute, + setAttributes: clientCS.setAttributes, + getAttributes: clientCS.getAttributes, + removeAttribute: clientCS.removeAttribute, + clearAttributes: clientCS.clearAttributes }) as SplitIO.ICsClient; } diff --git a/src/sdkClient/sdkClientMethodCS.ts b/src/sdkClient/sdkClientMethodCS.ts index c0d28f60..2abd7a20 100644 --- a/src/sdkClient/sdkClientMethodCS.ts +++ b/src/sdkClient/sdkClientMethodCS.ts @@ -29,6 +29,7 @@ export function sdkClientMethodCSFactory(params: ISdkClientFactoryParams): (key? const validKey = validateKey(log, key, method); const mainClientInstance = clientCSDecorator( + log, sdkClientFactory(params) as SplitIO.IClient, // @ts-ignore validKey ); @@ -74,6 +75,7 @@ export function sdkClientMethodCSFactory(params: ISdkClientFactoryParams): (key? // As shared clients reuse all the storage information, we don't need to check here if we // will use offline or online mode. We should stick with the original decision. clientInstances[instanceId] = clientCSDecorator( + log, sdkClientFactory(objectAssign({}, params, { sdkReadinessManager: sharedSdkReadiness, storage: sharedStorage || storage, diff --git a/src/sdkClient/sdkClientMethodCSWithTT.ts b/src/sdkClient/sdkClientMethodCSWithTT.ts index 1c844296..7c5427e2 100644 --- a/src/sdkClient/sdkClientMethodCSWithTT.ts +++ b/src/sdkClient/sdkClientMethodCSWithTT.ts @@ -35,6 +35,7 @@ export function sdkClientMethodCSFactory(params: ISdkClientFactoryParams): (key? } const mainClientInstance = clientCSDecorator( + log, sdkClientFactory(params) as SplitIO.IClient, // @ts-ignore validKey, validTrafficType @@ -88,6 +89,7 @@ export function sdkClientMethodCSFactory(params: ISdkClientFactoryParams): (key? // As shared clients reuse all the storage information, we don't need to check here if we // will use offline or online mode. We should stick with the original decision. clientInstances[instanceId] = clientCSDecorator( + log, sdkClientFactory(objectAssign({}, params, { sdkReadinessManager: sharedSdkReadiness, storage: sharedStorage || storage, diff --git a/src/storages/inMemory/AttributesCacheInMemory.ts b/src/storages/inMemory/AttributesCacheInMemory.ts new file mode 100644 index 00000000..b10fc4ae --- /dev/null +++ b/src/storages/inMemory/AttributesCacheInMemory.ts @@ -0,0 +1,72 @@ +import { objectAssign } from '../../utils/lang/objectAssign'; + +export class AttributesCacheInMemory { + + private attributesCache: Record = {}; + + + /** + * Create or update the value for the given attribute + * + * @param {string} attributeName attribute name + * @param {Object} attributeValue attribute value + * @returns {boolean} the attribute was stored + */ + setAttribute(attributeName: string, attributeValue: Object): boolean { + this.attributesCache[attributeName] = attributeValue; + return true; + } + + /** + * Retrieves the value of a given attribute + * + * @param {string} attributeName attribute name + * @returns {Object?} stored attribute value + */ + getAttribute(attributeName: string): Object { + return this.attributesCache[attributeName]; + } + + /** + * Create or update all the given attributes + * + * @param {[string, Object]} attributes attributes to create or update + * @returns {boolean} attributes were stored + */ + setAttributes(attributes: Record): boolean { + this.attributesCache = objectAssign(this.attributesCache, attributes); + return true; + } + + /** + * Retrieve the full attributes map + * + * @returns {Map} stored attributes + */ + getAll(): Record { + return this.attributesCache; + } + + /** + * Removes a given attribute from the map + * + * @param {string} attributeName attribute to remove + * @returns {boolean} attribute removed + */ + removeAttribute(attributeName: string): boolean { + if (Object.keys(this.attributesCache).indexOf(attributeName) >= 0) { + delete this.attributesCache[attributeName]; + return true; + } + return false; + } + + /** + * Clears all attributes stored in the SDK + * + */ + clear() { + this.attributesCache = {}; + } + +} diff --git a/src/storages/inMemory/__tests__/AttributesCacheInMemory.spec.ts b/src/storages/inMemory/__tests__/AttributesCacheInMemory.spec.ts new file mode 100644 index 00000000..690fdccb --- /dev/null +++ b/src/storages/inMemory/__tests__/AttributesCacheInMemory.spec.ts @@ -0,0 +1,36 @@ +import { AttributesCacheInMemory } from '../AttributesCacheInMemory'; + + +describe('ATTRIBUTES CACHE', () => { + + test('ATTRIBUTES CACHE / Should ..', () => { + + const cache = new AttributesCacheInMemory(); + + expect(cache.setAttribute('attributeName1', 'attributeValue1')).toEqual(true); + expect(cache.setAttribute('attributeName2', 'attributeValue2')).toEqual(true); + + expect(cache.getAll()).toEqual({ attributeName1: 'attributeValue1', attributeName2: 'attributeValue2' }); + + expect(cache.removeAttribute('attributeName1')).toEqual(true); + + expect(cache.getAttribute('attributeName1')).toEqual(undefined); + expect(cache.getAttribute('attributeName2')).toEqual('attributeValue2'); + + expect(cache.setAttributes({ + 'attributeName3': 'attributeValue3', + 'attributeName4': 'attributeValue4' + })).toEqual(true); + + expect(cache.getAttribute('attributeName2')).toEqual('attributeValue2'); + expect(cache.getAttribute('attributeName3')).toEqual('attributeValue3'); + expect(cache.getAttribute('attributeName4')).toEqual('attributeValue4'); + + cache.clear(); + + expect(cache.getAll()).toEqual({}); + + }); + +}); + diff --git a/src/types.ts b/src/types.ts index d4351696..91a20df0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1108,6 +1108,45 @@ export namespace SplitIO { * @returns {boolean} Whether the event was added to the queue succesfully or not. */ track(...args: [trafficType: string, eventType: string, value?: number, properties?: Properties] | [eventType: string, value?: number, properties?: Properties]): boolean, + /** + * Add an attribute to client's in memory attributes storage + * @function setAttribute + * @param {string} attributeName Attrinute name + * @param {string, number, boolean, list} attributeValue Attribute value + * @returns {boolean} true if the attribute was stored and false otherways + */ + setAttribute(attributeName: string, attributeValue: Object): boolean, + /** + * Returns the attribute with the given key + * @function getAttribute + * @param {string} attributeName Attribute name + * @returns {Object} Attribute with the given key + */ + getAttribute(attributeName: string): Object, + /** + * Add to client's in memory attributes storage the attributes in 'attributes' + * @function setAttributes + * @param {Object} attributes Object with attributes to store + * @returns true if attributes were stored an false otherways + */ + setAttributes(attributes: Record): boolean, + /** + * Return all the attributes stored in client's in memory attributes storage + * @function getAttributes + * @returns {Object} returns all the stored attributes + */ + getAttributes(): Record, + /** + * Removes from client's in memory attributes storage the attribute with the given key + * @function removeAttribute + * @param {string} attributeName + * @returns {boolean} true if attribute was removed and false otherways + */ + removeAttribute(attributeName: string): boolean, + /** + * Remove all the stored attributes in the client's in memory attribute storage + */ + clearAttributes(): any } /** * Representation of a manager instance with synchronous storage of the SDK. diff --git a/src/utils/inputValidation/__tests__/attribute.spec.ts b/src/utils/inputValidation/__tests__/attribute.spec.ts new file mode 100644 index 00000000..9c3e2356 --- /dev/null +++ b/src/utils/inputValidation/__tests__/attribute.spec.ts @@ -0,0 +1,18 @@ +import { validateAttribute } from '../attribute'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; + + +test('INPUT VALIDATION for Attribute', () => { + expect(validateAttribute(loggerMock, '', 'empty', 'some_method_attrs')).toEqual(false); // It should be invalid if the attribute key is not a string + + expect(validateAttribute(loggerMock, 'attributeKey', new Date(), 'some_method_attrs')).toEqual(false); // It should be invalid if the attribute value is not a String, Number, Boolean or Lists. + expect(validateAttribute(loggerMock, 'attributeKey', { 'some': 'object' }, 'some_method_attrs')).toEqual(false); // It should be invalid if the attribute value is not a String, Number, Boolean or Lists. + expect(validateAttribute(loggerMock, 'attributeKey', Infinity, 'some_method_attrs')).toEqual(false); // It should be invalid if the attribute value is not a String, Number, Boolean or Lists. + + expect(validateAttribute(loggerMock, 'attributeKey', 'attributeValue', 'some_method_attrs')).toEqual(true); // It should be valid if the attribute value is a String, Number, Boolean or Lists. + expect(validateAttribute(loggerMock, 'attributeKey', ['attribute', 'value'], 'some_method_attrs')).toEqual(true); // It should be valid if the attribute value is a String, Number, Boolean or Lists. + expect(validateAttribute(loggerMock, 'attributeKey', 25, 'some_method_attrs')).toEqual(true); // It should be valid if the attribute value is a String, Number, Boolean or Lists. + expect(validateAttribute(loggerMock, 'attributeKey', false, 'some_method_attrs')).toEqual(true); // It should be valid if the attribute value is a String, Number, Boolean or Lists. + expect(validateAttribute(loggerMock, 'attributeKey', Date.now(), 'some_method_attrs')).toEqual(true); // It should be valid if the attribute value is a String, Number, Boolean or Lists. + +}); diff --git a/src/utils/inputValidation/__tests__/attributes.spec.ts b/src/utils/inputValidation/__tests__/attributes.spec.ts index e414b4ea..1671122b 100644 --- a/src/utils/inputValidation/__tests__/attributes.spec.ts +++ b/src/utils/inputValidation/__tests__/attributes.spec.ts @@ -1,7 +1,7 @@ import { ERROR_NOT_PLAIN_OBJECT } from '../../../logger/constants'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -import { validateAttributes } from '../attributes'; +import { validateAttributes, validateAttributesDeep } from '../attributes'; const invalidAttributes = [ [], @@ -49,3 +49,40 @@ describe('INPUT VALIDATION for Attributes', () => { expect(loggerMock.warn).not.toBeCalled(); // It should have not logged any warnings. }); }); + +describe('DEEP INPUT VALIDATION for Attributes', () => { + + afterEach(() => { loggerMock.mockClear(); }); + + test('Should return true if it is a valid attributes map without logging any errors', () => { + const validAttributes = { amIvalid: 'yes', 'are_you_sure': true, howMuch: 10, 'spell': ['1', '0'] }; + + expect(validateAttributesDeep(loggerMock, validAttributes, 'some_method_attrs')).toEqual(true); // It should return true if it is valid. + expect(loggerMock.error).not.toBeCalled(); // Should not log any errors. + expect(loggerMock.warn).not.toBeCalled(); // It should have not logged any warnings. + + }); + + test('Should return false and log error if attributes map is invalid', () => { + + expect(validateAttributesDeep(loggerMock, { '': 'empty' }, 'some_method_attrs')).toEqual(false); // It should be invalid if the attribute key is not a string + expect(validateAttributesDeep(loggerMock, { 'attributeKey': new Date() }, 'some_method_attrs')).toEqual(false); // It should be invalid if the attribute value is not a String, Number, Boolean or Lists. + expect(validateAttributesDeep(loggerMock, { 'attributeKey': { 'some': 'object' } }, 'some_method_attrs')).toEqual(false); // It should be invalid if the attribute value is not a String, Number, Boolean or Lists. + expect(validateAttributesDeep(loggerMock, { 'attributeKey': Infinity }, 'some_method_attrs')).toEqual(false); // It should be invalid if the attribute value is not a String, Number, Boolean or Lists. + + expect(loggerMock.error).not.toBeCalled(); // Should not log any errors. + + }); + + test('Should return true if attributes map is valid', () => { + expect(validateAttributesDeep(loggerMock, { 'attributeKey': 'attributeValue' }, 'some_method_args')).toEqual(true); // It should be valid if the attribute value is a String, Number, Boolean or Lists. + expect(validateAttributesDeep(loggerMock, { 'attributeKey': ['attribute', 'value'] }, 'some_method_args')).toEqual(true); // It should be valid if the attribute value is a String, Number, Boolean or Lists. + expect(validateAttributesDeep(loggerMock, { 'attributeKey': 25 }, 'some_method_args')).toEqual(true); // It should be valid if the attribute value is a String, Number, Boolean or Lists. + expect(validateAttributesDeep(loggerMock, { 'attributeKey': false }, 'some_method_args')).toEqual(true); // It should be valid if the attribute value is a String, Number, Boolean or Lists. + expect(validateAttributesDeep(loggerMock, { 'attributeKey': Date.now() }, 'some_method_args')).toEqual(true); // It should be valid if the attribute value is a String, Number, Boolean or Lists. + + expect(loggerMock.error).not.toBeCalled(); // Should not log any errors. + expect(loggerMock.warn).not.toBeCalled(); // It should have not logged any warnings. + + }); +}); diff --git a/src/utils/inputValidation/attribute.ts b/src/utils/inputValidation/attribute.ts new file mode 100644 index 00000000..8fc6ef1e --- /dev/null +++ b/src/utils/inputValidation/attribute.ts @@ -0,0 +1,21 @@ +import { isString, isFiniteNumber, isBoolean } from '../../../src/utils/lang'; +import { ILogger } from '../../logger/types'; + +export function validateAttribute(log: ILogger, attributeKey: string, attributeValue: Object, method: string): boolean { + if (!isString(attributeKey) || attributeKey.length === 0) { + log.warn(`${method}: you passed an invalid attribute name, attribute name must be a non-empty string.`); + return false; + } + + const isStringVal = isString(attributeValue); + const isFiniteVal = isFiniteNumber(attributeValue); + const isBoolVal = isBoolean(attributeValue); + const isArrayVal = Array.isArray(attributeValue); + + if (!(isStringVal || isFiniteVal || isBoolVal || isArrayVal)) { // If it's not of valid type. + log.warn(`${method}: you passed an invalid attribute value for ${attributeKey}. Acceptable types are: string, number, boolean and array of strings.`); + return false; + } + + return true; +} diff --git a/src/utils/inputValidation/attributes.ts b/src/utils/inputValidation/attributes.ts index e45f67c0..7a06ee90 100644 --- a/src/utils/inputValidation/attributes.ts +++ b/src/utils/inputValidation/attributes.ts @@ -1,6 +1,7 @@ import { isObject } from '../lang'; import { SplitIO } from '../../types'; import { ILogger } from '../../logger/types'; +import { validateAttribute } from './attribute'; import { ERROR_NOT_PLAIN_OBJECT } from '../../logger/constants'; export function validateAttributes(log: ILogger, maybeAttrs: any, method: string): SplitIO.Attributes | undefined | false { @@ -11,3 +12,16 @@ export function validateAttributes(log: ILogger, maybeAttrs: any, method: string log.error(ERROR_NOT_PLAIN_OBJECT, [method, 'attributes']); return false; } + +export function validateAttributesDeep(log: ILogger, maybeAttributes: Record, method: string): boolean { + if (!validateAttributes(log, maybeAttributes, method)) return false; + + let result = true; + Object.keys(maybeAttributes).forEach(attributeKey => { + if (!validateAttribute(log, attributeKey, maybeAttributes[attributeKey], method)) + result = false; + }); + + return result; + +} From 53abb7130087a7cc54d7b5558fb3a6b79fd2c75a Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Fri, 7 Jan 2022 14:41:22 -0300 Subject: [PATCH 02/12] add client attributes binding unit tests --- .../__tests__/sdkClientMethodCS.spec.ts | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/src/sdkClient/__tests__/sdkClientMethodCS.spec.ts b/src/sdkClient/__tests__/sdkClientMethodCS.spec.ts index 89490ef4..8d4f2410 100644 --- a/src/sdkClient/__tests__/sdkClientMethodCS.spec.ts +++ b/src/sdkClient/__tests__/sdkClientMethodCS.spec.ts @@ -51,6 +51,20 @@ const params = { settings: settingsWithKey }; +const invalidAttributes = [ + new Date(), + {some: 'object'}, + Infinity +]; + +const validAttributes = [ + 25, // number + Date.now(), // number + 'string', // string + ['string', 'list'], // list + false // boolean +] + /** End mocks */ describe('sdkClientMethodCSFactory', () => { @@ -231,4 +245,55 @@ describe('sdkClientMethodCSFactory', () => { else expect(clientCSDecoratorSpy).toHaveBeenCalledWith(expect.anything(), expect.anything(), false, false); }); + test.each(testTargets)('main client', (sdkClientMethodCSFactory) => { + // @ts-expect-error + const sdkClientMethod = sdkClientMethodCSFactory(params); + + // should return a function + expect(typeof sdkClientMethod).toBe('function'); + + // calling the function should return a client instance + const client = sdkClientMethod(); + assertClientApi(client, params.sdkReadinessManager.sdkStatus); + + expect(client.setAttribute('attributeName1', 'attributeValue1')).toEqual(true); + expect(client.setAttribute('attributeName2', 'attributeValue2')).toEqual(true); + + expect(client.setAttribute('', 'empty')).toEqual(false); // Attribute name should not be an empty string + expect(client.setAttribute({'': 'empty'})).toEqual(false); // Attribute name should not be an empty string + + invalidAttributes.forEach(invalidValue => { + expect(client.setAttribute('attributeName', invalidValue)).toEqual(false); + expect(client.setAttributes({attributeName: invalidValue})).toEqual(false); + }) + + validAttributes.forEach(validValue => { + expect(client.setAttribute('attributeName', validValue)).toEqual(true); + }); + + validAttributes.forEach(validValue => { + expect(client.setAttributes({attributeName: validValue})).toEqual(true); + }) + + expect(client.getAttributes()).toEqual({ attributeName: false, attributeName1: 'attributeValue1', attributeName2: 'attributeValue2' }); + + expect(client.removeAttribute('attributeName1')).toEqual(true); + + expect(client.getAttribute('attributeName1')).toEqual(undefined); + expect(client.getAttribute('attributeName2')).toEqual('attributeValue2'); + + expect(client.setAttributes({ + 'attributeName3': 'attributeValue3', + 'attributeName4': 'attributeValue4' + })).toEqual(true); + + expect(client.getAttribute('attributeName2')).toEqual('attributeValue2'); + expect(client.getAttribute('attributeName3')).toEqual('attributeValue3'); + expect(client.getAttribute('attributeName4')).toEqual('attributeValue4'); + + client.clearAttributes(); + + expect(client.getAttributes()).toEqual({}); + }); + }); From 51ee091ff67ace6d52fc9ab2b6d3f1ce0e500840 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Fri, 7 Jan 2022 15:05:21 -0300 Subject: [PATCH 03/12] Add unit test for shared clients --- .../__tests__/sdkClientMethodCS.spec.ts | 53 +++++++++++++++---- .../__tests__/AttributesCacheInMemory.spec.ts | 2 +- 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/src/sdkClient/__tests__/sdkClientMethodCS.spec.ts b/src/sdkClient/__tests__/sdkClientMethodCS.spec.ts index 8d4f2410..a52fe26f 100644 --- a/src/sdkClient/__tests__/sdkClientMethodCS.spec.ts +++ b/src/sdkClient/__tests__/sdkClientMethodCS.spec.ts @@ -53,7 +53,7 @@ const params = { const invalidAttributes = [ new Date(), - {some: 'object'}, + { some: 'object' }, Infinity ]; @@ -63,7 +63,7 @@ const validAttributes = [ 'string', // string ['string', 'list'], // list false // boolean -] +]; /** End mocks */ @@ -245,7 +245,7 @@ describe('sdkClientMethodCSFactory', () => { else expect(clientCSDecoratorSpy).toHaveBeenCalledWith(expect.anything(), expect.anything(), false, false); }); - test.each(testTargets)('main client', (sdkClientMethodCSFactory) => { + test.each(testTargets)('attributes binding - main client', (sdkClientMethodCSFactory) => { // @ts-expect-error const sdkClientMethod = sdkClientMethodCSFactory(params); @@ -255,25 +255,27 @@ describe('sdkClientMethodCSFactory', () => { // calling the function should return a client instance const client = sdkClientMethod(); assertClientApi(client, params.sdkReadinessManager.sdkStatus); - + expect(client.setAttribute('attributeName1', 'attributeValue1')).toEqual(true); expect(client.setAttribute('attributeName2', 'attributeValue2')).toEqual(true); expect(client.setAttribute('', 'empty')).toEqual(false); // Attribute name should not be an empty string - expect(client.setAttribute({'': 'empty'})).toEqual(false); // Attribute name should not be an empty string + expect(client.setAttribute({ '': 'empty' })).toEqual(false); // Attribute name should not be an empty string invalidAttributes.forEach(invalidValue => { expect(client.setAttribute('attributeName', invalidValue)).toEqual(false); - expect(client.setAttributes({attributeName: invalidValue})).toEqual(false); - }) + expect(client.setAttributes({ attributeName: invalidValue })).toEqual(false); + }); + + expect(client.getAttributes()).toEqual({ attributeName1: 'attributeValue1', attributeName2: 'attributeValue2' }); validAttributes.forEach(validValue => { expect(client.setAttribute('attributeName', validValue)).toEqual(true); }); validAttributes.forEach(validValue => { - expect(client.setAttributes({attributeName: validValue})).toEqual(true); - }) + expect(client.setAttributes({ attributeName: validValue })).toEqual(true); + }); expect(client.getAttributes()).toEqual({ attributeName: false, attributeName1: 'attributeValue1', attributeName2: 'attributeValue2' }); @@ -296,4 +298,37 @@ describe('sdkClientMethodCSFactory', () => { expect(client.getAttributes()).toEqual({}); }); + + test.each(testTargets)('attributes binding - shared clients', (sdkClientMethodCSFactory) => { + // @ts-expect-error + const sdkClientMethod = sdkClientMethodCSFactory(params); + + // should return a function + expect(typeof sdkClientMethod).toBe('function'); + + // calling the function should return a client instance + const emmanuelClient = sdkClientMethod('emmanuel@split.io'); + const emilianoClient = sdkClientMethod('emiliano@split.io'); + assertClientApi(emmanuelClient); + assertClientApi(emilianoClient); + + expect(emmanuelClient.setAttribute('name', 'Emmanuel')).toEqual(true); + expect(emilianoClient.setAttribute('name', 'Emiliano')).toEqual(true); + + expect(emmanuelClient.getAttribute('name')).toEqual('Emmanuel'); + expect(emilianoClient.getAttribute('name')).toEqual('Emiliano'); + + expect(emmanuelClient.setAttributes({ email: 'emmanuel@split.io' })).toEqual(true); + expect(emilianoClient.setAttributes({ email: 'emiliano@split.io' })).toEqual(true); + + expect(emmanuelClient.getAttributes()).toEqual({ name: 'Emmanuel', email: 'emmanuel@split.io' }); + expect(emilianoClient.getAttributes()).toEqual({ name: 'Emiliano', email: 'emiliano@split.io' }); + + emmanuelClient.clearAttributes(); + + expect(emmanuelClient.getAttributes()).toEqual({}); + expect(emilianoClient.getAttributes()).toEqual({ name: 'Emiliano', email: 'emiliano@split.io' }); + + }); + }); diff --git a/src/storages/inMemory/__tests__/AttributesCacheInMemory.spec.ts b/src/storages/inMemory/__tests__/AttributesCacheInMemory.spec.ts index 690fdccb..665ec94b 100644 --- a/src/storages/inMemory/__tests__/AttributesCacheInMemory.spec.ts +++ b/src/storages/inMemory/__tests__/AttributesCacheInMemory.spec.ts @@ -3,7 +3,7 @@ import { AttributesCacheInMemory } from '../AttributesCacheInMemory'; describe('ATTRIBUTES CACHE', () => { - test('ATTRIBUTES CACHE / Should ..', () => { + test('ATTRIBUTES CACHE / Attributes in memory storage test basic usage ', () => { const cache = new AttributesCacheInMemory(); From 24720ca14d2388faa615e7b42f03662d231f7a34 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Tue, 11 Jan 2022 17:15:58 -0300 Subject: [PATCH 04/12] fix imports path --- src/sdkClient/clientAttributesDecoration.ts | 2 +- src/utils/inputValidation/attribute.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sdkClient/clientAttributesDecoration.ts b/src/sdkClient/clientAttributesDecoration.ts index f0a5e305..b42ddbfa 100644 --- a/src/sdkClient/clientAttributesDecoration.ts +++ b/src/sdkClient/clientAttributesDecoration.ts @@ -1,5 +1,5 @@ import { AttributesCacheInMemory } from '../storages/inMemory/AttributesCacheInMemory'; -import { validateAttributesDeep } from '../../src/utils/inputValidation/attributes'; +import { validateAttributesDeep } from '../utils/inputValidation/attributes'; import { SplitIO } from '../types'; import { ILogger } from '../logger/types'; import { objectAssign } from '../utils/lang/objectAssign'; diff --git a/src/utils/inputValidation/attribute.ts b/src/utils/inputValidation/attribute.ts index 8fc6ef1e..e30d9473 100644 --- a/src/utils/inputValidation/attribute.ts +++ b/src/utils/inputValidation/attribute.ts @@ -1,4 +1,4 @@ -import { isString, isFiniteNumber, isBoolean } from '../../../src/utils/lang'; +import { isString, isFiniteNumber, isBoolean } from '../../utils/lang'; import { ILogger } from '../../logger/types'; export function validateAttribute(log: ILogger, attributeKey: string, attributeValue: Object, method: string): boolean { From d7f908ac0199d90784801265b0a56b3698f42c75 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Tue, 11 Jan 2022 17:18:01 -0300 Subject: [PATCH 05/12] SDKS-5411. Add evaluation logic --- src/sdkClient/clientAttributesDecoration.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/sdkClient/clientAttributesDecoration.ts b/src/sdkClient/clientAttributesDecoration.ts index b42ddbfa..1274fc00 100644 --- a/src/sdkClient/clientAttributesDecoration.ts +++ b/src/sdkClient/clientAttributesDecoration.ts @@ -83,25 +83,33 @@ export function ClientAttributesDecoration 0) { + return objectAssign({}, storedAttributes, maybeAttributes); + } + return maybeAttributes; + } + return objectAssign(client, { getTreatment: getTreatment, getTreatmentWithConfig: getTreatmentWithConfig, From 743fa8f4043e012111c11f51aa7cc352473d0243 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Thu, 13 Jan 2022 14:23:33 -0300 Subject: [PATCH 06/12] fix return type for clearAttribute method to boolean --- src/storages/inMemory/AttributesCacheInMemory.ts | 1 + src/types.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/storages/inMemory/AttributesCacheInMemory.ts b/src/storages/inMemory/AttributesCacheInMemory.ts index b10fc4ae..455d35e1 100644 --- a/src/storages/inMemory/AttributesCacheInMemory.ts +++ b/src/storages/inMemory/AttributesCacheInMemory.ts @@ -67,6 +67,7 @@ export class AttributesCacheInMemory { */ clear() { this.attributesCache = {}; + return true; } } diff --git a/src/types.ts b/src/types.ts index 91a20df0..9a94dd45 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1146,7 +1146,7 @@ export namespace SplitIO { /** * Remove all the stored attributes in the client's in memory attribute storage */ - clearAttributes(): any + clearAttributes(): boolean } /** * Representation of a manager instance with synchronous storage of the SDK. From d18c64138de3d597cd8c6a6157c34b229b5ec852 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Fri, 14 Jan 2022 17:19:25 -0300 Subject: [PATCH 07/12] implemented PR suggested changes --- .../__tests__/sdkClientMethodCS.spec.ts | 4 +- src/sdkClient/clientAttributesDecoration.ts | 137 +++++++++--------- src/sdkClient/clientCS.ts | 13 +- .../__tests__/AttributesCacheInMemory.spec.ts | 40 +++-- .../__tests__/attribute.spec.ts | 9 ++ .../__tests__/attributes.spec.ts | 2 + 6 files changed, 99 insertions(+), 106 deletions(-) diff --git a/src/sdkClient/__tests__/sdkClientMethodCS.spec.ts b/src/sdkClient/__tests__/sdkClientMethodCS.spec.ts index a52fe26f..b5b6b21e 100644 --- a/src/sdkClient/__tests__/sdkClientMethodCS.spec.ts +++ b/src/sdkClient/__tests__/sdkClientMethodCS.spec.ts @@ -293,7 +293,7 @@ describe('sdkClientMethodCSFactory', () => { expect(client.getAttribute('attributeName3')).toEqual('attributeValue3'); expect(client.getAttribute('attributeName4')).toEqual('attributeValue4'); - client.clearAttributes(); + expect(client.clearAttributes()).toEqual(true); expect(client.getAttributes()).toEqual({}); }); @@ -324,7 +324,7 @@ describe('sdkClientMethodCSFactory', () => { expect(emmanuelClient.getAttributes()).toEqual({ name: 'Emmanuel', email: 'emmanuel@split.io' }); expect(emilianoClient.getAttributes()).toEqual({ name: 'Emiliano', email: 'emiliano@split.io' }); - emmanuelClient.clearAttributes(); + expect(emmanuelClient.clearAttributes()).toEqual(true); expect(emmanuelClient.getAttributes()).toEqual({}); expect(emilianoClient.getAttributes()).toEqual({ name: 'Emiliano', email: 'emiliano@split.io' }); diff --git a/src/sdkClient/clientAttributesDecoration.ts b/src/sdkClient/clientAttributesDecoration.ts index b42ddbfa..e4ff1270 100644 --- a/src/sdkClient/clientAttributesDecoration.ts +++ b/src/sdkClient/clientAttributesDecoration.ts @@ -7,7 +7,7 @@ import { objectAssign } from '../utils/lang/objectAssign'; /** * Add in memory attributes storage methods and combine them with any attribute received from the getTreatment/s call */ -export function ClientAttributesDecoration(log: ILogger, client: TClient): any { +export function clientAttributesDecoration(log: ILogger, client: TClient): any { const attributeStorage = new AttributesCacheInMemory(); @@ -18,70 +18,6 @@ export function ClientAttributesDecoration = {}; - attribute[attributeName] = attributeValue; - if (!validateAttributesDeep(log, attribute, 'setAttribute')) return false; - log.debug(`stored ${attributeValue} for attribute ${attributeName}`); - return attributeStorage.setAttribute(attributeName, attributeValue); - } - - /** - * Returns the attribute with the given key - * - * @param {string} attributeName Attribute name - * @returns {Object} Attribute with the given key - */ - function getAttribute(attributeName: string): Object { - log.debug(`retrieved attribute ${attributeName}`); - return attributeStorage.getAttribute(attributeName + ''); - } - - /** - * Add to client's in memory attributes storage the attributes in 'attributes' - * - * @param {Object} attributes Object with attributes to store - * @returns true if attributes were stored an false otherways - */ - function setAttributes(attributes: Record): boolean { - if (!validateAttributesDeep(log, attributes, 'setAttributes')) return false; - return attributeStorage.setAttributes(attributes); - } - - /** - * Return all the attributes stored in client's in memory attributes storage - * - * @returns {Object} returns all the stored attributes - */ - function getAttributes(): Record { - return attributeStorage.getAll(); - } - - /** - * Removes from client's in memory attributes storage the attribute with the given key - * - * @param {string} attributeName - * @returns {boolean} true if attribute was removed and false otherways - */ - function removeAttribute(attributeName: string): boolean { - log.debug(`removed attribute ${attributeName}`); - return attributeStorage.removeAttribute(attributeName + ''); - } - - /** - * Remove all the stored attributes in the client's in memory attribute storage - */ - function clearAttributes() { - return attributeStorage.clear(); - } - function getTreatment(maybeKey: SplitIO.SplitKey, maybeSplit: string, maybeAttributes?: SplitIO.Attributes) { return clientGetTreatment(maybeKey, maybeSplit, maybeAttributes); } @@ -108,12 +44,71 @@ export function ClientAttributesDecoration = {}; + attribute[attributeName] = attributeValue; + if (!validateAttributesDeep(log, attribute, 'setAttribute')) return false; + log.debug(`stored ${attributeValue} for attribute ${attributeName}`); + return attributeStorage.setAttribute(attributeName, attributeValue); + }, + + /** + * Returns the attribute with the given key + * + * @param {string} attributeName Attribute name + * @returns {Object} Attribute with the given key + */ + getAttribute(attributeName: string) { + log.debug(`retrieved attribute ${attributeName}`); + return attributeStorage.getAttribute(attributeName + ''); + }, + + /** + * Add to client's in memory attributes storage the attributes in 'attributes' + * + * @param {Object} attributes Object with attributes to store + * @returns true if attributes were stored an false otherways + */ + setAttributes(attributes: Record) { + if (!validateAttributesDeep(log, attributes, 'setAttributes')) return false; + return attributeStorage.setAttributes(attributes); + }, + + /** + * Return all the attributes stored in client's in memory attributes storage + * + * @returns {Object} returns all the stored attributes + */ + getAttributes(): Record { + return attributeStorage.getAll(); + }, + + /** + * Removes from client's in memory attributes storage the attribute with the given key + * + * @param {string} attributeName + * @returns {boolean} true if attribute was removed and false otherways + */ + removeAttribute(attributeName: string) { + log.debug(`removed attribute ${attributeName}`); + return attributeStorage.removeAttribute(attributeName + ''); + }, + + /** + * Remove all the stored attributes in the client's in memory attribute storage + */ + clearAttributes() { + return attributeStorage.clear(); + } + }); } diff --git a/src/sdkClient/clientCS.ts b/src/sdkClient/clientCS.ts index 9c5ec97a..8cf0d6a2 100644 --- a/src/sdkClient/clientCS.ts +++ b/src/sdkClient/clientCS.ts @@ -1,7 +1,7 @@ import { objectAssign } from '../utils/lang/objectAssign'; import { ILogger } from '../logger/types'; import { SplitIO } from '../types'; -import { ClientAttributesDecoration } from './clientAttributesDecoration'; +import { clientAttributesDecoration } from './clientAttributesDecoration'; /** @@ -13,7 +13,7 @@ import { ClientAttributesDecoration } from './clientAttributesDecoration'; */ export function clientCSDecorator(log: ILogger, client: SplitIO.IClient, key: SplitIO.SplitKey, trafficType?: string): SplitIO.ICsClient { - let clientCS = ClientAttributesDecoration(log, client); + let clientCS = clientAttributesDecoration(log, client); return objectAssign(clientCS, { // In the client-side API, we bind a key to the client `getTreatment*` methods @@ -23,13 +23,6 @@ export function clientCSDecorator(log: ILogger, client: SplitIO.IClient, key: Sp getTreatmentsWithConfig: clientCS.getTreatmentsWithConfig.bind(clientCS, key), // Key is bound to the `track` method. Same thing happens with trafficType but only if provided - track: trafficType ? clientCS.track.bind(clientCS, key, trafficType) : clientCS.track.bind(clientCS, key), - - setAttribute: clientCS.setAttribute, - getAttribute: clientCS.getAttribute, - setAttributes: clientCS.setAttributes, - getAttributes: clientCS.getAttributes, - removeAttribute: clientCS.removeAttribute, - clearAttributes: clientCS.clearAttributes + track: trafficType ? clientCS.track.bind(clientCS, key, trafficType) : clientCS.track.bind(clientCS, key) }) as SplitIO.ICsClient; } diff --git a/src/storages/inMemory/__tests__/AttributesCacheInMemory.spec.ts b/src/storages/inMemory/__tests__/AttributesCacheInMemory.spec.ts index 665ec94b..12863369 100644 --- a/src/storages/inMemory/__tests__/AttributesCacheInMemory.spec.ts +++ b/src/storages/inMemory/__tests__/AttributesCacheInMemory.spec.ts @@ -1,36 +1,30 @@ import { AttributesCacheInMemory } from '../AttributesCacheInMemory'; +test('ATTRIBUTES CACHE / Attributes in memory storage test basic usage ', () => { -describe('ATTRIBUTES CACHE', () => { + const cache = new AttributesCacheInMemory(); - test('ATTRIBUTES CACHE / Attributes in memory storage test basic usage ', () => { + expect(cache.setAttribute('attributeName1', 'attributeValue1')).toEqual(true); + expect(cache.setAttribute('attributeName2', 'attributeValue2')).toEqual(true); - const cache = new AttributesCacheInMemory(); + expect(cache.getAll()).toEqual({ attributeName1: 'attributeValue1', attributeName2: 'attributeValue2' }); - expect(cache.setAttribute('attributeName1', 'attributeValue1')).toEqual(true); - expect(cache.setAttribute('attributeName2', 'attributeValue2')).toEqual(true); + expect(cache.removeAttribute('attributeName1')).toEqual(true); - expect(cache.getAll()).toEqual({ attributeName1: 'attributeValue1', attributeName2: 'attributeValue2' }); + expect(cache.getAttribute('attributeName1')).toEqual(undefined); + expect(cache.getAttribute('attributeName2')).toEqual('attributeValue2'); - expect(cache.removeAttribute('attributeName1')).toEqual(true); + expect(cache.setAttributes({ + 'attributeName3': 'attributeValue3', + 'attributeName4': 'attributeValue4' + })).toEqual(true); - expect(cache.getAttribute('attributeName1')).toEqual(undefined); - expect(cache.getAttribute('attributeName2')).toEqual('attributeValue2'); + expect(cache.getAttribute('attributeName2')).toEqual('attributeValue2'); + expect(cache.getAttribute('attributeName3')).toEqual('attributeValue3'); + expect(cache.getAttribute('attributeName4')).toEqual('attributeValue4'); - expect(cache.setAttributes({ - 'attributeName3': 'attributeValue3', - 'attributeName4': 'attributeValue4' - })).toEqual(true); + expect(cache.clear()).toEqual(true); - expect(cache.getAttribute('attributeName2')).toEqual('attributeValue2'); - expect(cache.getAttribute('attributeName3')).toEqual('attributeValue3'); - expect(cache.getAttribute('attributeName4')).toEqual('attributeValue4'); - - cache.clear(); - - expect(cache.getAll()).toEqual({}); - - }); + expect(cache.getAll()).toEqual({}); }); - diff --git a/src/utils/inputValidation/__tests__/attribute.spec.ts b/src/utils/inputValidation/__tests__/attribute.spec.ts index 9c3e2356..a9d7f6e4 100644 --- a/src/utils/inputValidation/__tests__/attribute.spec.ts +++ b/src/utils/inputValidation/__tests__/attribute.spec.ts @@ -3,7 +3,16 @@ import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; test('INPUT VALIDATION for Attribute', () => { + + // @ts-ignore + expect(validateAttribute(loggerMock, 2, 'dos', 'some_method_attrs')).toEqual(false); // It should be invalid if the attribute key is not a string expect(validateAttribute(loggerMock, '', 'empty', 'some_method_attrs')).toEqual(false); // It should be invalid if the attribute key is not a string + // @ts-ignore + expect(validateAttribute(loggerMock, null, 'null', 'some_method_attrs')).toEqual(false); // It should be invalid if the attribute key is not a string + // @ts-ignore + expect(validateAttribute(loggerMock, true, 'boolean', 'some_method_attrs')).toEqual(false); // It should be invalid if the attribute key is not a string + // @ts-ignore + expect(validateAttribute(loggerMock, {'some':'object'}, 'object', 'some_method_attrs')).toEqual(false); // It should be invalid if the attribute key is not a string expect(validateAttribute(loggerMock, 'attributeKey', new Date(), 'some_method_attrs')).toEqual(false); // It should be invalid if the attribute value is not a String, Number, Boolean or Lists. expect(validateAttribute(loggerMock, 'attributeKey', { 'some': 'object' }, 'some_method_attrs')).toEqual(false); // It should be invalid if the attribute value is not a String, Number, Boolean or Lists. diff --git a/src/utils/inputValidation/__tests__/attributes.spec.ts b/src/utils/inputValidation/__tests__/attributes.spec.ts index 1671122b..cd6e7052 100644 --- a/src/utils/inputValidation/__tests__/attributes.spec.ts +++ b/src/utils/inputValidation/__tests__/attributes.spec.ts @@ -66,6 +66,8 @@ describe('DEEP INPUT VALIDATION for Attributes', () => { test('Should return false and log error if attributes map is invalid', () => { expect(validateAttributesDeep(loggerMock, { '': 'empty' }, 'some_method_attrs')).toEqual(false); // It should be invalid if the attribute key is not a string + // @ts-ignore + expect(validateAttributesDeep(loggerMock, { 'attrKey': null }, 'some_method_attrs')).toEqual(false); // It should be invalid if the attribute value is not a String, Number, Boolean or Lists. expect(validateAttributesDeep(loggerMock, { 'attributeKey': new Date() }, 'some_method_attrs')).toEqual(false); // It should be invalid if the attribute value is not a String, Number, Boolean or Lists. expect(validateAttributesDeep(loggerMock, { 'attributeKey': { 'some': 'object' } }, 'some_method_attrs')).toEqual(false); // It should be invalid if the attribute value is not a String, Number, Boolean or Lists. expect(validateAttributesDeep(loggerMock, { 'attributeKey': Infinity }, 'some_method_attrs')).toEqual(false); // It should be invalid if the attribute value is not a String, Number, Boolean or Lists. From 18c02adbc7d54bc9b30d870a46b78243a4e14b2e Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Fri, 14 Jan 2022 21:11:47 -0300 Subject: [PATCH 08/12] Add UT for clientAttributesDecoration --- .../clientAttributesDecoration.spec.ts | 270 ++++++++++++++++++ 1 file changed, 270 insertions(+) create mode 100644 src/sdkClient/__tests__/clientAttributesDecoration.spec.ts diff --git a/src/sdkClient/__tests__/clientAttributesDecoration.spec.ts b/src/sdkClient/__tests__/clientAttributesDecoration.spec.ts new file mode 100644 index 00000000..da56e992 --- /dev/null +++ b/src/sdkClient/__tests__/clientAttributesDecoration.spec.ts @@ -0,0 +1,270 @@ +import { sdkClientMethodCSFactory } from '../sdkClientMethodCS'; + +import { settingsWithKey } from '../../utils/settingsValidation/__tests__/settings.mocks'; + +const partialStorages: { destroy: jest.Mock }[] = []; + +const storageMock = { + destroy: jest.fn(), + shared: jest.fn(() => { + partialStorages.push({ destroy: jest.fn() }); + return partialStorages[partialStorages.length - 1]; + }) +}; + +const partialSdkReadinessManagers: { sdkStatus: jest.Mock, readinessManager: { destroy: jest.Mock } }[] = []; + +const sdkReadinessManagerMock = { + sdkStatus: jest.fn(), + readinessManager: { + destroy: jest.fn(), + isDestroyed: jest.fn(() => { return false; }), + isReady: jest.fn(() => { return true; }) + }, + shared: jest.fn(() => { + partialSdkReadinessManagers.push({ + sdkStatus: jest.fn(), + readinessManager: { destroy: jest.fn() }, + }); + return partialSdkReadinessManagers[partialSdkReadinessManagers.length - 1]; + }) +}; + +const partialSyncManagers: { start: jest.Mock, stop: jest.Mock, flush: jest.Mock }[] = []; + +const syncManagerMock = { + stop: jest.fn(), + flush: jest.fn(() => Promise.resolve()), + shared: jest.fn(() => { + partialSyncManagers.push({ start: jest.fn(), stop: jest.fn(), flush: jest.fn(() => Promise.resolve()) }); + return partialSyncManagers[partialSyncManagers.length - 1]; + }) +}; + +const params = { + storage: storageMock, + sdkReadinessManager: sdkReadinessManagerMock, + syncManager: syncManagerMock, + signalListener: { stop: jest.fn() }, + settings: settingsWithKey +}; + +// We are testing attributes binding feature and we just need to know how the attributes are combined before evaluation +// So input validation decorator is mocked to return the combined attributes instead of evaluation so we can verify them +jest.mock('../clientInputValidation', () => { + return { + clientInputValidationDecorator() { + return { + getTreatment(maybeKey: any, maybeSplit: string, maybeAttributes?: any) { + return maybeAttributes; + }, + getTreatmentWithConfig(maybeKey: any, maybeSplit: string, maybeAttributes?: any) { + return maybeAttributes; + }, + getTreatments(maybeKey: any, maybeSplits: string[], maybeAttributes?: any) { + return maybeAttributes; + }, + getTreatmentsWithConfig(maybeKey: any, maybeSplits: string[], maybeAttributes?: any) { + return maybeAttributes; + } + }; + } + }; +}); + +// @ts-expect-error +const sdkClientMethod = sdkClientMethodCSFactory(params); +const client = sdkClientMethod(); + +//expect(client).toBe(AttributesDecorationMockedClient); + +test('ATTRIBUTES DECORATION / storage', () => { + + client.setAttribute('attributeName1', 'attributeValue1'); + client.setAttribute('attributeName2', 'attributeValue2'); + + expect(client.getAttributes()).toEqual({ attributeName1: 'attributeValue1', attributeName2: 'attributeValue2' }); // It should be equal + + client.removeAttribute('attributeName1'); + client.setAttribute('attributeName2', 'newAttributeValue2'); + + expect(client.getAttribute('attributeName1')).toEqual(undefined); // It should throw undefined + expect(client.getAttribute('attributeName2')).toEqual('newAttributeValue2'); // It should be equal + + client.setAttributes({ + 'attributeName3': 'attributeValue3', + 'attributeName4': 'attributeValue4' + }); + + expect(client.getAttributes()).toEqual({ attributeName2: 'newAttributeValue2', attributeName3: 'attributeValue3', attributeName4: 'attributeValue4' }); // It should be equal + + expect(client.clearAttributes()).toEqual(true); + + expect(Object.keys(client.getAttributes()).length).toEqual(0); // It should be zero after clearing attributes + +}); + + +describe('ATTRIBUTES DECORATION / validation', () => { + + test('Should return true if it is a valid attributes map without logging any errors', () => { + const validAttributes = { amIvalid: 'yes', 'are_you_sure': true, howMuch: 10, 'spell': ['1', '0'] }; + + expect(client.setAttributes(validAttributes)).toEqual(true); // It should return true if it is valid. + expect(client.getAttributes()).toEqual(validAttributes); // It should be the same. + expect(client.setAttribute('attrKey', 'attrValue')).toEqual(true); // It should return true. + expect(client.getAttribute('attrKey')).toEqual('attrValue'); // It should return true. + + expect(client.removeAttribute('attrKey')).toEqual(true); // It should return true. + expect(client.getAttributes()).toEqual(validAttributes); // It should be equal to the first set. + + expect(client.clearAttributes()).toEqual(true); + + expect(Object.keys(client.getAttributes()).length).toEqual(0); // It should be zero after clearing attributes + + }); + + test('Should return false if it is an invalid attributes map', () => { + expect(client.setAttribute('', 'attributeValue')).toEqual(false); // It should be invalid if the attribute key is not a string + expect(client.setAttribute('attributeKey1', new Date())).toEqual(false); // It should be invalid if the attribute value is not a String, Number, Boolean or Lists. + expect(client.setAttribute('attributeKey2', { 'some': 'object' })).toEqual(false); // It should be invalid if the attribute value is not a String, Number, Boolean or Lists. + expect(client.setAttribute('attributeKey3', Infinity)).toEqual(false); // It should be invalid if the attribute value is not a String, Number, Boolean or Lists. + + expect(client.clearAttributes()).toEqual(true); + + let values = client.getAttributes(); + + expect(Object.keys(values).length).toEqual(0); // It should be zero after clearing attributes + + let attributes = { + 'attributeKey': 'attributeValue', + '': 'attributeValue' + }; + + expect(client.setAttributes(attributes)).toEqual(false); // It should be invalid if the attribute key is not a string + + expect(Object.keys(client.getAttributes()).length).toEqual(0); // It should be zero after trying to add an invalid attribute + + expect(client.clearAttributes()).toEqual(true); + + }); + + test('Should return true if attributes map is valid', () => { + const validAttributes = { + 'attributeKey1': 'attributeValue', + 'attributeKey2': ['attribute', 'value'], + 'attributeKey3': 25, + 'attributeKey4': false + }; + + expect(client.setAttribute('attributeKey1', 'attributeValue')).toEqual(true); // It should be valid if the attribute value is a String, Number, Boolean or Lists. + expect(client.setAttribute('attributeKey2', ['attribute', 'value'])).toEqual(true); // It should be valid if the attribute value is a String, Number, Boolean or Lists. + expect(client.setAttribute('attributeKey3', 25)).toEqual(true); // It should be valid if the attribute value is a String, Number, Boolean or Lists. + expect(client.setAttribute('attributeKey4', false)).toEqual(true); // It should be valid if the attribute value is a String, Number, Boolean or Lists. + expect(client.setAttribute('attributeKey5', Date.now())).toEqual(true); // It should be valid if the attribute value is a String, Number, Boolean or Lists. + + expect(client.removeAttribute('attributeKey5')).toEqual(true); // It should be capable of remove the attribute with that name + expect(client.getAttributes()).toEqual(validAttributes); // It should had stored every valid attributes. + + expect(client.clearAttributes()).toEqual(true); + + expect(client.setAttributes(validAttributes)).toEqual(true); // It should add them all because they are valid attributes. + expect(client.getAttributes()).toEqual(validAttributes); // It should had stored every valid attributes. + + expect(client.clearAttributes()).toEqual(true); + + }); + +}); + +describe('ATTRIBUTES DECORATION / evaluation', () => { + + test('Evaluation attributes logic and precedence / getTreatment', () => { + + // If the same attribute is “cached” and provided on the function, the value received on the function call takes precedence. + expect(client.getTreatment('split')).toEqual(undefined); // Nothing changes if no attributes were provided using the new api + expect(client.getTreatment('split', { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Nothing changes if no attributes were provided using the new api + expect(client.getAttributes()).toEqual({}); // Attributes in memory storage must be empty + client.setAttribute('func_attr_bool', false); + expect(client.getAttributes()).toEqual({ 'func_attr_bool': false }); // In memory attribute storage must have the unique stored attribute + expect(client.getTreatment('split', { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Function attributes has precedence against api ones + // @ts-ignore + expect(client.getTreatment('split', null)).toEqual({ func_attr_bool: false }); // API attributes should be kept in memory and use for evaluations + expect(client.getTreatment('split', { func_attr_str: 'true' })).toEqual({ func_attr_bool: false, func_attr_str: 'true' }); // API attributes should be kept in memory and use for evaluations + client.setAttributes({ func_attr_str: 'false' }); + expect(client.getAttributes()).toEqual({ 'func_attr_bool': false, 'func_attr_str': 'false' }); // In memory attribute storage must have two stored attributes + expect(client.getTreatment('split', { func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 })).toEqual({ func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 }); // Function attributes has precedence against api ones + // @ts-ignore + expect(client.getTreatment('split', null)).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. + expect(client.getTreatment('split')).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. + expect(client.clearAttributes()).toEqual(true); + + }); + + test('Evaluation attributes logic and precedence / getTreatments', () => { + + // If the same attribute is “cached” and provided on the function, the value received on the function call takes precedence. + expect(client.getTreatments(['split'])).toEqual(undefined); // Nothing changes if no attributes were provided using the new api + expect(client.getTreatments(['split'], { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Nothing changes if no attributes were provided using the new api + expect(client.getAttributes()).toEqual({}); // Attributes in memory storage must be empty + client.setAttribute('func_attr_bool', false); + expect(client.getAttributes()).toEqual({ 'func_attr_bool': false }); // In memory attribute storage must have the unique stored attribute + expect(client.getTreatments(['split'], { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Function attributes has precedence against api ones + // @ts-ignore + expect(client.getTreatments(['split'], null)).toEqual({ func_attr_bool: false }); // API attributes should be kept in memory and use for evaluations + expect(client.getTreatments(['split'], { func_attr_str: 'true' })).toEqual({ func_attr_bool: false, func_attr_str: 'true' }); // API attributes should be kept in memory and use for evaluations + client.setAttributes({ func_attr_str: 'false' }); + expect(client.getAttributes()).toEqual({ 'func_attr_bool': false, 'func_attr_str': 'false' }); // In memory attribute storage must have two stored attributes + expect(client.getTreatments(['split'], { func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 })).toEqual({ func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 }); // Function attributes has precedence against api ones + // @ts-ignore + expect(client.getTreatments(['split'], null)).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. + expect(client.getTreatments(['split'])).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. + expect(client.clearAttributes()).toEqual(true); + + }); + + test('Evaluation attributes logic and precedence / getTreatmentWithConfig', () => { + + // If the same attribute is “cached” and provided on the function, the value received on the function call takes precedence. + expect(client.getTreatmentWithConfig('split')).toEqual(undefined); // Nothing changes if no attributes were provided using the new api + expect(client.getTreatmentWithConfig('split', { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Nothing changes if no attributes were provided using the new api + expect(client.getAttributes()).toEqual({}); // Attributes in memory storage must be empty + client.setAttribute('func_attr_bool', false); + expect(client.getAttributes()).toEqual({ 'func_attr_bool': false }); // In memory attribute storage must have the unique stored attribute + expect(client.getTreatmentWithConfig('split', { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Function attributes has precedence against api ones + // @ts-ignore + expect(client.getTreatmentWithConfig('split', null)).toEqual({ func_attr_bool: false }); // API attributes should be kept in memory and use for evaluations + expect(client.getTreatmentWithConfig('split', { func_attr_str: 'true' })).toEqual({ func_attr_bool: false, func_attr_str: 'true' }); // API attributes should be kept in memory and use for evaluations + client.setAttributes({ func_attr_str: 'false' }); + expect(client.getAttributes()).toEqual({ 'func_attr_bool': false, 'func_attr_str': 'false' }); // In memory attribute storage must have two stored attributes + expect(client.getTreatmentWithConfig('split', { func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 })).toEqual({ func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 }); // Function attributes has precedence against api ones + // @ts-ignore + expect(client.getTreatmentWithConfig('split', null)).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. + expect(client.getTreatmentWithConfig('split')).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. + expect(client.clearAttributes()).toEqual(true); + + }); + + test('Evaluation attributes logic and precedence / getTreatmentsWithConfig', () => { + + // If the same attribute is “cached” and provided on the function, the value received on the function call takes precedence. + expect(client.getTreatmentsWithConfig(['split'])).toEqual(undefined); // Nothing changes if no attributes were provided using the new api + expect(client.getTreatmentsWithConfig(['split'], { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Nothing changes if no attributes were provided using the new api + expect(client.getAttributes()).toEqual({}); // Attributes in memory storage must be empty + client.setAttribute('func_attr_bool', false); + expect(client.getAttributes()).toEqual({ 'func_attr_bool': false }); // In memory attribute storage must have the unique stored attribute + expect(client.getTreatmentsWithConfig(['split'], { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Function attributes has precedence against api ones + // @ts-ignore + expect(client.getTreatmentsWithConfig(['split'], null)).toEqual({ func_attr_bool: false }); // API attributes should be kept in memory and use for evaluations + expect(client.getTreatmentsWithConfig(['split'], { func_attr_str: 'true' })).toEqual({ func_attr_bool: false, func_attr_str: 'true' }); // API attributes should be kept in memory and use for evaluations + client.setAttributes({ func_attr_str: 'false' }); + expect(client.getAttributes()).toEqual({ 'func_attr_bool': false, 'func_attr_str': 'false' }); // In memory attribute storage must have two stored attributes + expect(client.getTreatmentsWithConfig(['split'], { func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 })).toEqual({ func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 }); // Function attributes has precedence against api ones + // @ts-ignore + expect(client.getTreatmentsWithConfig(['split'], null)).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. + expect(client.getTreatmentsWithConfig(['split'])).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. + client.clearAttributes(); + + }); + +}); From fde847909054ad63b71f945a550697f3104c2714 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Mon, 17 Jan 2022 14:34:01 -0300 Subject: [PATCH 09/12] call clientAttributesDecoration function directly on clientAttributesDecoration UT --- .../clientAttributesDecoration.spec.ts | 155 ++++++------------ src/sdkClient/clientAttributesDecoration.ts | 2 +- 2 files changed, 49 insertions(+), 108 deletions(-) diff --git a/src/sdkClient/__tests__/clientAttributesDecoration.spec.ts b/src/sdkClient/__tests__/clientAttributesDecoration.spec.ts index da56e992..bb6c0df7 100644 --- a/src/sdkClient/__tests__/clientAttributesDecoration.spec.ts +++ b/src/sdkClient/__tests__/clientAttributesDecoration.spec.ts @@ -1,82 +1,23 @@ -import { sdkClientMethodCSFactory } from '../sdkClientMethodCS'; +import { clientAttributesDecoration } from '../clientAttributesDecoration'; +import { loggerMock } from '../../logger/__tests__/sdkLogger.mock'; -import { settingsWithKey } from '../../utils/settingsValidation/__tests__/settings.mocks'; - -const partialStorages: { destroy: jest.Mock }[] = []; - -const storageMock = { - destroy: jest.fn(), - shared: jest.fn(() => { - partialStorages.push({ destroy: jest.fn() }); - return partialStorages[partialStorages.length - 1]; - }) -}; - -const partialSdkReadinessManagers: { sdkStatus: jest.Mock, readinessManager: { destroy: jest.Mock } }[] = []; - -const sdkReadinessManagerMock = { - sdkStatus: jest.fn(), - readinessManager: { - destroy: jest.fn(), - isDestroyed: jest.fn(() => { return false; }), - isReady: jest.fn(() => { return true; }) +// mocked methods return the provided attributes object (2nd argument), to assert that it was properly passed +const clientMock = { + getTreatment(maybeKey: any, maybeSplit: string, maybeAttributes?: any) { + return maybeAttributes; }, - shared: jest.fn(() => { - partialSdkReadinessManagers.push({ - sdkStatus: jest.fn(), - readinessManager: { destroy: jest.fn() }, - }); - return partialSdkReadinessManagers[partialSdkReadinessManagers.length - 1]; - }) -}; - -const partialSyncManagers: { start: jest.Mock, stop: jest.Mock, flush: jest.Mock }[] = []; - -const syncManagerMock = { - stop: jest.fn(), - flush: jest.fn(() => Promise.resolve()), - shared: jest.fn(() => { - partialSyncManagers.push({ start: jest.fn(), stop: jest.fn(), flush: jest.fn(() => Promise.resolve()) }); - return partialSyncManagers[partialSyncManagers.length - 1]; - }) -}; - -const params = { - storage: storageMock, - sdkReadinessManager: sdkReadinessManagerMock, - syncManager: syncManagerMock, - signalListener: { stop: jest.fn() }, - settings: settingsWithKey + getTreatmentWithConfig(maybeKey: any, maybeSplit: string, maybeAttributes?: any) { + return maybeAttributes; + }, + getTreatments(maybeKey: any, maybeSplits: string[], maybeAttributes?: any) { + return maybeAttributes; + }, + getTreatmentsWithConfig(maybeKey: any, maybeSplits: string[], maybeAttributes?: any) { + return maybeAttributes; + } }; - -// We are testing attributes binding feature and we just need to know how the attributes are combined before evaluation -// So input validation decorator is mocked to return the combined attributes instead of evaluation so we can verify them -jest.mock('../clientInputValidation', () => { - return { - clientInputValidationDecorator() { - return { - getTreatment(maybeKey: any, maybeSplit: string, maybeAttributes?: any) { - return maybeAttributes; - }, - getTreatmentWithConfig(maybeKey: any, maybeSplit: string, maybeAttributes?: any) { - return maybeAttributes; - }, - getTreatments(maybeKey: any, maybeSplits: string[], maybeAttributes?: any) { - return maybeAttributes; - }, - getTreatmentsWithConfig(maybeKey: any, maybeSplits: string[], maybeAttributes?: any) { - return maybeAttributes; - } - }; - } - }; -}); - // @ts-expect-error -const sdkClientMethod = sdkClientMethodCSFactory(params); -const client = sdkClientMethod(); - -//expect(client).toBe(AttributesDecorationMockedClient); +const client = clientAttributesDecoration(loggerMock, clientMock); test('ATTRIBUTES DECORATION / storage', () => { @@ -182,21 +123,21 @@ describe('ATTRIBUTES DECORATION / evaluation', () => { test('Evaluation attributes logic and precedence / getTreatment', () => { // If the same attribute is “cached” and provided on the function, the value received on the function call takes precedence. - expect(client.getTreatment('split')).toEqual(undefined); // Nothing changes if no attributes were provided using the new api - expect(client.getTreatment('split', { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Nothing changes if no attributes were provided using the new api + expect(client.getTreatment('key', 'split')).toEqual(undefined); // Nothing changes if no attributes were provided using the new api + expect(client.getTreatment('key', 'split', { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Nothing changes if no attributes were provided using the new api expect(client.getAttributes()).toEqual({}); // Attributes in memory storage must be empty client.setAttribute('func_attr_bool', false); expect(client.getAttributes()).toEqual({ 'func_attr_bool': false }); // In memory attribute storage must have the unique stored attribute - expect(client.getTreatment('split', { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Function attributes has precedence against api ones + expect(client.getTreatment('key', 'split', { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Function attributes has precedence against api ones // @ts-ignore - expect(client.getTreatment('split', null)).toEqual({ func_attr_bool: false }); // API attributes should be kept in memory and use for evaluations - expect(client.getTreatment('split', { func_attr_str: 'true' })).toEqual({ func_attr_bool: false, func_attr_str: 'true' }); // API attributes should be kept in memory and use for evaluations + expect(client.getTreatment('key', 'split', null)).toEqual({ func_attr_bool: false }); // API attributes should be kept in memory and use for evaluations + expect(client.getTreatment('key', 'split', { func_attr_str: 'true' })).toEqual({ func_attr_bool: false, func_attr_str: 'true' }); // API attributes should be kept in memory and use for evaluations client.setAttributes({ func_attr_str: 'false' }); expect(client.getAttributes()).toEqual({ 'func_attr_bool': false, 'func_attr_str': 'false' }); // In memory attribute storage must have two stored attributes - expect(client.getTreatment('split', { func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 })).toEqual({ func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 }); // Function attributes has precedence against api ones + expect(client.getTreatment('key', 'split', { func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 })).toEqual({ func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 }); // Function attributes has precedence against api ones // @ts-ignore - expect(client.getTreatment('split', null)).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. - expect(client.getTreatment('split')).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. + expect(client.getTreatment('key', 'split', null)).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. + expect(client.getTreatment('key', 'split')).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. expect(client.clearAttributes()).toEqual(true); }); @@ -204,21 +145,21 @@ describe('ATTRIBUTES DECORATION / evaluation', () => { test('Evaluation attributes logic and precedence / getTreatments', () => { // If the same attribute is “cached” and provided on the function, the value received on the function call takes precedence. - expect(client.getTreatments(['split'])).toEqual(undefined); // Nothing changes if no attributes were provided using the new api - expect(client.getTreatments(['split'], { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Nothing changes if no attributes were provided using the new api + expect(client.getTreatments('key', ['split'])).toEqual(undefined); // Nothing changes if no attributes were provided using the new api + expect(client.getTreatments('key', ['split'], { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Nothing changes if no attributes were provided using the new api expect(client.getAttributes()).toEqual({}); // Attributes in memory storage must be empty client.setAttribute('func_attr_bool', false); expect(client.getAttributes()).toEqual({ 'func_attr_bool': false }); // In memory attribute storage must have the unique stored attribute - expect(client.getTreatments(['split'], { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Function attributes has precedence against api ones + expect(client.getTreatments('key', ['split'], { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Function attributes has precedence against api ones // @ts-ignore - expect(client.getTreatments(['split'], null)).toEqual({ func_attr_bool: false }); // API attributes should be kept in memory and use for evaluations - expect(client.getTreatments(['split'], { func_attr_str: 'true' })).toEqual({ func_attr_bool: false, func_attr_str: 'true' }); // API attributes should be kept in memory and use for evaluations + expect(client.getTreatments('key', ['split'], null)).toEqual({ func_attr_bool: false }); // API attributes should be kept in memory and use for evaluations + expect(client.getTreatments('key', ['split'], { func_attr_str: 'true' })).toEqual({ func_attr_bool: false, func_attr_str: 'true' }); // API attributes should be kept in memory and use for evaluations client.setAttributes({ func_attr_str: 'false' }); expect(client.getAttributes()).toEqual({ 'func_attr_bool': false, 'func_attr_str': 'false' }); // In memory attribute storage must have two stored attributes - expect(client.getTreatments(['split'], { func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 })).toEqual({ func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 }); // Function attributes has precedence against api ones + expect(client.getTreatments('key', ['split'], { func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 })).toEqual({ func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 }); // Function attributes has precedence against api ones // @ts-ignore - expect(client.getTreatments(['split'], null)).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. - expect(client.getTreatments(['split'])).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. + expect(client.getTreatments('key', ['split'], null)).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. + expect(client.getTreatments('key', ['split'])).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. expect(client.clearAttributes()).toEqual(true); }); @@ -226,21 +167,21 @@ describe('ATTRIBUTES DECORATION / evaluation', () => { test('Evaluation attributes logic and precedence / getTreatmentWithConfig', () => { // If the same attribute is “cached” and provided on the function, the value received on the function call takes precedence. - expect(client.getTreatmentWithConfig('split')).toEqual(undefined); // Nothing changes if no attributes were provided using the new api - expect(client.getTreatmentWithConfig('split', { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Nothing changes if no attributes were provided using the new api + expect(client.getTreatmentWithConfig('key', 'split')).toEqual(undefined); // Nothing changes if no attributes were provided using the new api + expect(client.getTreatmentWithConfig('key', 'split', { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Nothing changes if no attributes were provided using the new api expect(client.getAttributes()).toEqual({}); // Attributes in memory storage must be empty client.setAttribute('func_attr_bool', false); expect(client.getAttributes()).toEqual({ 'func_attr_bool': false }); // In memory attribute storage must have the unique stored attribute - expect(client.getTreatmentWithConfig('split', { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Function attributes has precedence against api ones + expect(client.getTreatmentWithConfig('key', 'split', { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Function attributes has precedence against api ones // @ts-ignore - expect(client.getTreatmentWithConfig('split', null)).toEqual({ func_attr_bool: false }); // API attributes should be kept in memory and use for evaluations - expect(client.getTreatmentWithConfig('split', { func_attr_str: 'true' })).toEqual({ func_attr_bool: false, func_attr_str: 'true' }); // API attributes should be kept in memory and use for evaluations + expect(client.getTreatmentWithConfig('key', 'split', null)).toEqual({ func_attr_bool: false }); // API attributes should be kept in memory and use for evaluations + expect(client.getTreatmentWithConfig('key', 'split', { func_attr_str: 'true' })).toEqual({ func_attr_bool: false, func_attr_str: 'true' }); // API attributes should be kept in memory and use for evaluations client.setAttributes({ func_attr_str: 'false' }); expect(client.getAttributes()).toEqual({ 'func_attr_bool': false, 'func_attr_str': 'false' }); // In memory attribute storage must have two stored attributes - expect(client.getTreatmentWithConfig('split', { func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 })).toEqual({ func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 }); // Function attributes has precedence against api ones + expect(client.getTreatmentWithConfig('key', 'split', { func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 })).toEqual({ func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 }); // Function attributes has precedence against api ones // @ts-ignore - expect(client.getTreatmentWithConfig('split', null)).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. - expect(client.getTreatmentWithConfig('split')).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. + expect(client.getTreatmentWithConfig('key', 'split', null)).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. + expect(client.getTreatmentWithConfig('key', 'split')).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. expect(client.clearAttributes()).toEqual(true); }); @@ -248,21 +189,21 @@ describe('ATTRIBUTES DECORATION / evaluation', () => { test('Evaluation attributes logic and precedence / getTreatmentsWithConfig', () => { // If the same attribute is “cached” and provided on the function, the value received on the function call takes precedence. - expect(client.getTreatmentsWithConfig(['split'])).toEqual(undefined); // Nothing changes if no attributes were provided using the new api - expect(client.getTreatmentsWithConfig(['split'], { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Nothing changes if no attributes were provided using the new api + expect(client.getTreatmentsWithConfig('key', ['split'])).toEqual(undefined); // Nothing changes if no attributes were provided using the new api + expect(client.getTreatmentsWithConfig('key', ['split'], { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Nothing changes if no attributes were provided using the new api expect(client.getAttributes()).toEqual({}); // Attributes in memory storage must be empty client.setAttribute('func_attr_bool', false); expect(client.getAttributes()).toEqual({ 'func_attr_bool': false }); // In memory attribute storage must have the unique stored attribute - expect(client.getTreatmentsWithConfig(['split'], { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Function attributes has precedence against api ones + expect(client.getTreatmentsWithConfig('key', ['split'], { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Function attributes has precedence against api ones // @ts-ignore - expect(client.getTreatmentsWithConfig(['split'], null)).toEqual({ func_attr_bool: false }); // API attributes should be kept in memory and use for evaluations - expect(client.getTreatmentsWithConfig(['split'], { func_attr_str: 'true' })).toEqual({ func_attr_bool: false, func_attr_str: 'true' }); // API attributes should be kept in memory and use for evaluations + expect(client.getTreatmentsWithConfig('key', ['split'], null)).toEqual({ func_attr_bool: false }); // API attributes should be kept in memory and use for evaluations + expect(client.getTreatmentsWithConfig('key', ['split'], { func_attr_str: 'true' })).toEqual({ func_attr_bool: false, func_attr_str: 'true' }); // API attributes should be kept in memory and use for evaluations client.setAttributes({ func_attr_str: 'false' }); expect(client.getAttributes()).toEqual({ 'func_attr_bool': false, 'func_attr_str': 'false' }); // In memory attribute storage must have two stored attributes - expect(client.getTreatmentsWithConfig(['split'], { func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 })).toEqual({ func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 }); // Function attributes has precedence against api ones + expect(client.getTreatmentsWithConfig('key', ['split'], { func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 })).toEqual({ func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 }); // Function attributes has precedence against api ones // @ts-ignore - expect(client.getTreatmentsWithConfig(['split'], null)).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. - expect(client.getTreatmentsWithConfig(['split'])).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. + expect(client.getTreatmentsWithConfig('key', ['split'], null)).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. + expect(client.getTreatmentsWithConfig('key', ['split'])).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate. client.clearAttributes(); }); diff --git a/src/sdkClient/clientAttributesDecoration.ts b/src/sdkClient/clientAttributesDecoration.ts index 67e649c5..5d55c7df 100644 --- a/src/sdkClient/clientAttributesDecoration.ts +++ b/src/sdkClient/clientAttributesDecoration.ts @@ -7,7 +7,7 @@ import { objectAssign } from '../utils/lang/objectAssign'; /** * Add in memory attributes storage methods and combine them with any attribute received from the getTreatment/s call */ -export function clientAttributesDecoration(log: ILogger, client: TClient): any { +export function clientAttributesDecoration(log: ILogger, client: TClient) { const attributeStorage = new AttributesCacheInMemory(); From 83b19b63ba8a626185024190fa3d95b516e836a8 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Mon, 17 Jan 2022 16:12:20 -0300 Subject: [PATCH 10/12] release v1.1.1 --- CHANGES.txt | 3 +++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index b6952027..d7f107b1 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,6 @@ +1.1.1 (XXX) + - Added support to SDK clients on browser to optionally bind attributes to the client, keeping these loaded within the SDK along with the user ID, for easier usage when requesting flag. + 1.1.0 (January 11, 2022) - Added support for the SDK to run in "consumer" and "partial consumer" modes, with a pluggable implementation of it's internal storage, enabling customers to implement this caching with any storage technology of choice and connect it to the SDK instance to be used instead of its default in-memory storage. diff --git a/package-lock.json b/package-lock.json index 0b72381e..216370a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.1.0", + "version": "1.1.1-rc.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index dc09153e..030c18d6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.1.0", + "version": "1.1.1-rc.0", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", From f1d33da59133e8332fa69e66812b3071683f461a Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Wed, 19 Jan 2022 13:43:17 -0300 Subject: [PATCH 11/12] prepare release v1.1.1 --- CHANGES.txt | 2 +- package-lock.json | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index d7f107b1..f7a51e8a 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,4 @@ -1.1.1 (XXX) +1.1.1 (January 19, 2022) - Added support to SDK clients on browser to optionally bind attributes to the client, keeping these loaded within the SDK along with the user ID, for easier usage when requesting flag. 1.1.0 (January 11, 2022) diff --git a/package-lock.json b/package-lock.json index 216370a0..4bcf3753 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.1.1-rc.0", + "version": "1.1.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 030c18d6..84bc19c1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.1.1-rc.0", + "version": "1.1.1", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", From d68dd168491b5dfe31e296f6692bf7528376625b Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Wed, 19 Jan 2022 16:18:35 -0300 Subject: [PATCH 12/12] fix version to 1.2.0 --- CHANGES.txt | 2 +- package-lock.json | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index f7a51e8a..892a6b9d 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,4 @@ -1.1.1 (January 19, 2022) +1.2.0 (January 19, 2022) - Added support to SDK clients on browser to optionally bind attributes to the client, keeping these loaded within the SDK along with the user ID, for easier usage when requesting flag. 1.1.0 (January 11, 2022) diff --git a/package-lock.json b/package-lock.json index 4bcf3753..7be002a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.1.1", + "version": "1.2.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 84bc19c1..9310f51f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.1.1", + "version": "1.2.0", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js",