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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
211 changes: 211 additions & 0 deletions src/sdkClient/__tests__/clientAttributesDecoration.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import { clientAttributesDecoration } from '../clientAttributesDecoration';
import { loggerMock } from '../../logger/__tests__/sdkLogger.mock';

// 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;
},
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 client = clientAttributesDecoration(loggerMock, clientMock);

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('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('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('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('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('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);

});

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('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('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('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('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('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);

});

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('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('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('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('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('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);

});

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('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('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('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('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('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();

});

});
18 changes: 13 additions & 5 deletions src/sdkClient/clientAttributesDecoration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TClient extends SplitIO.IClient | SplitIO.IAsyncClient>(log: ILogger, client: TClient): any {
export function clientAttributesDecoration<TClient extends SplitIO.IClient | SplitIO.IAsyncClient>(log: ILogger, client: TClient) {

const attributeStorage = new AttributesCacheInMemory();

Expand All @@ -19,25 +19,33 @@ export function clientAttributesDecoration<TClient extends SplitIO.IClient | Spl
const clientTrack = client.track;

@EmilianoSanchez EmilianoSanchez Jan 17, 2022

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in line 10, please remove the any as return type, so that TS automatically infers it for our UTs and the rest of the code.

function getTreatment(maybeKey: SplitIO.SplitKey, maybeSplit: string, maybeAttributes?: SplitIO.Attributes) {
return clientGetTreatment(maybeKey, maybeSplit, maybeAttributes);
return clientGetTreatment(maybeKey, maybeSplit, combineAttributes(maybeAttributes));
}

function getTreatmentWithConfig(maybeKey: SplitIO.SplitKey, maybeSplit: string, maybeAttributes?: SplitIO.Attributes) {
return clientGetTreatmentWithConfig(maybeKey, maybeSplit, maybeAttributes);
return clientGetTreatmentWithConfig(maybeKey, maybeSplit, combineAttributes(maybeAttributes));
}

function getTreatments(maybeKey: SplitIO.SplitKey, maybeSplits: string[], maybeAttributes?: SplitIO.Attributes) {
return clientGetTreatments(maybeKey, maybeSplits, maybeAttributes);
return clientGetTreatments(maybeKey, maybeSplits, combineAttributes(maybeAttributes));
}

function getTreatmentsWithConfig(maybeKey: SplitIO.SplitKey, maybeSplits: string[], maybeAttributes?: SplitIO.Attributes) {
return clientGetTreatmentsWithConfig(maybeKey, maybeSplits, maybeAttributes);
return clientGetTreatmentsWithConfig(maybeKey, maybeSplits, combineAttributes(maybeAttributes));
}

function track(maybeKey: SplitIO.SplitKey, maybeTT: string, maybeEvent: string, maybeEventValue?: number, maybeProperties?: SplitIO.Properties) {
return clientTrack(maybeKey, maybeTT, maybeEvent, maybeEventValue, maybeProperties);
}

function combineAttributes(maybeAttributes: SplitIO.Attributes | undefined): SplitIO.Attributes | undefined{
const storedAttributes = attributeStorage.getAll();
if (Object.keys(storedAttributes).length > 0) {
return objectAssign({}, storedAttributes, maybeAttributes);
}
return maybeAttributes;
}

return objectAssign(client, {
getTreatment: getTreatment,
getTreatmentWithConfig: getTreatmentWithConfig,
Expand Down