From 9292562d77c9446a6ff830243cce0c84070dcd26 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Fri, 7 Jan 2022 13:06:42 -0300 Subject: [PATCH 1/6] 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/6] 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/6] 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/6] 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 743fa8f4043e012111c11f51aa7cc352473d0243 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Thu, 13 Jan 2022 14:23:33 -0300 Subject: [PATCH 5/6] 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 6/6] 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.