From 9292562d77c9446a6ff830243cce0c84070dcd26 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Fri, 7 Jan 2022 13:06:42 -0300 Subject: [PATCH 1/9] 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 2/9] 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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 7/9] 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 8/9] 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 9/9] 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();