diff --git a/spec/src/constructorio.js b/spec/src/constructorio.js index 13802aba..de776485 100644 --- a/spec/src/constructorio.js +++ b/spec/src/constructorio.js @@ -280,6 +280,49 @@ describe(`ConstructorIO${bundledDescriptionSuffix}`, () => { expect(instance.options).to.have.property('testCells').to.deep.equal(newTestCells); }); + it('Should filter out non-string testCell values in constructor', () => { + const testCells = { + valid: 'bar', + nullVal: null, + numVal: 123, + objVal: { nested: 'value' }, + emptyStr: '', + boolean: true, + }; + const instance = new ConstructorIO({ + apiKey: validApiKey, + testCells, + }); + + expect(instance.options.testCells).to.deep.equal({ valid: 'bar' }); + }); + + it('Should filter out non-string testCell values in setClientOptions', () => { + const instance = new ConstructorIO({ + apiKey: validApiKey, + testCells: { initial: 'value' }, + }); + + instance.setClientOptions({ + testCells: { + valid: 'baz', + nullVal: null, + numVal: 42, + }, + }); + + expect(instance.options.testCells).to.deep.equal({ valid: 'baz' }); + }); + + it('Should handle null testCells in constructor without error', () => { + const instance = new ConstructorIO({ + apiKey: validApiKey, + testCells: null, + }); + + expect(instance.options.testCells).to.deep.equal({}); + }); + it('Should update the client options with new sendTrackingEvents value', () => { const instance = new ConstructorIO({ apiKey: validApiKey, @@ -507,22 +550,22 @@ describe(`ConstructorIO${bundledDescriptionSuffix}`, () => { }); expect(instance.options).to.have.property('testCells').to.deep.equal(oldTestCells); - expect(instance.search.options).to.have.property('testCells').to.equal(oldTestCells); - expect(instance.autocomplete.options).to.have.property('testCells').to.equal(oldTestCells); - expect(instance.browse.options).to.have.property('testCells').to.equal(oldTestCells); - expect(instance.recommendations.options).to.have.property('testCells').to.equal(oldTestCells); - expect(instance.tracker.options).to.have.property('testCells').to.equal(oldTestCells); + expect(instance.search.options).to.have.property('testCells').to.deep.equal(oldTestCells); + expect(instance.autocomplete.options).to.have.property('testCells').to.deep.equal(oldTestCells); + expect(instance.browse.options).to.have.property('testCells').to.deep.equal(oldTestCells); + expect(instance.recommendations.options).to.have.property('testCells').to.deep.equal(oldTestCells); + expect(instance.tracker.options).to.have.property('testCells').to.deep.equal(oldTestCells); instance.setClientOptions({ testCells: newTestCells, }); expect(instance.options).to.have.property('testCells').to.deep.equal(newTestCells); - expect(instance.search.options).to.have.property('testCells').to.equal(newTestCells); - expect(instance.autocomplete.options).to.have.property('testCells').to.equal(newTestCells); - expect(instance.browse.options).to.have.property('testCells').to.equal(newTestCells); - expect(instance.recommendations.options).to.have.property('testCells').to.equal(newTestCells); - expect(instance.tracker.options).to.have.property('testCells').to.equal(newTestCells); + expect(instance.search.options).to.have.property('testCells').to.deep.equal(newTestCells); + expect(instance.autocomplete.options).to.have.property('testCells').to.deep.equal(newTestCells); + expect(instance.browse.options).to.have.property('testCells').to.deep.equal(newTestCells); + expect(instance.recommendations.options).to.have.property('testCells').to.deep.equal(newTestCells); + expect(instance.tracker.options).to.have.property('testCells').to.deep.equal(newTestCells); }); it('Should update the client options with a new user id', () => { diff --git a/spec/src/modules/autocomplete.js b/spec/src/modules/autocomplete.js index 4098f45a..093ef557 100644 --- a/spec/src/modules/autocomplete.js +++ b/spec/src/modules/autocomplete.js @@ -112,6 +112,24 @@ describe(`ConstructorIO - Autocomplete${bundledDescriptionSuffix}`, () => { }); }); + it('Should only include valid string testCells in request params', (done) => { + const testCells = { valid: 'bar', invalid: null, numVal: 123 }; + const { autocomplete } = new ConstructorIO({ + apiKey: testApiKey, + testCells, + fetch: fetchSpy, + }); + + autocomplete.getAutocompleteResults(query).then(() => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(requestedUrlParams).to.have.property('ef-valid').to.equal('bar'); + expect(requestedUrlParams).to.not.have.property('ef-invalid'); + expect(requestedUrlParams).to.not.have.property('ef-numVal'); + done(); + }); + }); + it('Should return a response with a valid query, and segments', (done) => { const segments = ['foo', 'bar']; const { autocomplete } = new ConstructorIO({ diff --git a/spec/src/modules/browse.js b/spec/src/modules/browse.js index 7273977e..d3db86ef 100644 --- a/spec/src/modules/browse.js +++ b/spec/src/modules/browse.js @@ -97,6 +97,24 @@ describe(`ConstructorIO - Browse${bundledDescriptionSuffix}`, () => { }); }); + it('Should only include valid string testCells in request params', (done) => { + const testCells = { valid: 'bar', invalid: null, numVal: 123 }; + const { browse } = new ConstructorIO({ + apiKey: testApiKey, + testCells, + fetch: fetchSpy, + }); + + browse.getBrowseResults(filterName, filterValue).then(() => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(requestedUrlParams).to.have.property('ef-valid').to.equal('bar'); + expect(requestedUrlParams).to.not.have.property('ef-invalid'); + expect(requestedUrlParams).to.not.have.property('ef-numVal'); + done(); + }); + }); + it('Should return a response with a valid filterName, filterValue and segments', (done) => { const segments = ['foo', 'bar']; const { browse } = new ConstructorIO({ diff --git a/spec/src/modules/search.js b/spec/src/modules/search.js index d30db3f5..aaa65890 100644 --- a/spec/src/modules/search.js +++ b/spec/src/modules/search.js @@ -93,6 +93,24 @@ describe(`ConstructorIO - Search${bundledDescriptionSuffix}`, () => { }); }); + it('Should only include valid string testCells in request params', (done) => { + const testCells = { valid: 'bar', invalid: null, numVal: 123 }; + const { search } = new ConstructorIO({ + apiKey: testApiKey, + testCells, + fetch: fetchSpy, + }); + + search.getSearchResults(query, { section }).then(() => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(requestedUrlParams).to.have.property('ef-valid').to.equal('bar'); + expect(requestedUrlParams).to.not.have.property('ef-invalid'); + expect(requestedUrlParams).to.not.have.property('ef-numVal'); + done(); + }); + }); + it('Should return a response with a valid query, section and segments', (done) => { const segments = ['foo', 'bar']; const { search } = new ConstructorIO({ diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index 746e3ed5..8659f0b5 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -349,6 +349,29 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { expect(tracker.trackSessionStart()).to.equal(true); }); + it('Should only include valid string testCells in request params', (done) => { + const testCells = { valid: 'bar', invalid: null, numVal: 123 }; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + testCells, + ...requestQueueOptions, + }); + + tracker.on('success', () => { + const requestParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('ef-valid').to.equal('bar'); + expect(requestParams).to.not.have.property('ef-invalid'); + expect(requestParams).to.not.have.property('ef-numVal'); + + done(); + }); + + expect(tracker.trackSessionStart()).to.equal(true); + }); + if (!skipNetworkTimeoutTests) { it('Should be rejected when network request timeout is provided and reached', (done) => { const { tracker } = new ConstructorIO({ diff --git a/spec/src/utils/helpers.js b/spec/src/utils/helpers.js index 2da73912..7a80e6d5 100644 --- a/spec/src/utils/helpers.js +++ b/spec/src/utils/helpers.js @@ -26,6 +26,7 @@ const { addHTTPSToString, trimUrl, cleanAndValidateUrl, + toValidTestCells, } = require('../../../test/utils/helpers'); // eslint-disable-line import/extensions const jsdom = require('./jsdom-global'); const store = require('../../../test/utils/store'); // eslint-disable-line import/extensions @@ -699,4 +700,71 @@ describe('ConstructorIO - Utils - Helpers', () => { }); }); } + + describe('toValidTestCells', () => { + it('Should return an empty object for null input', () => { + expect(toValidTestCells(null)).to.deep.equal({}); + }); + + it('Should return an empty object for undefined input', () => { + expect(toValidTestCells(undefined)).to.deep.equal({}); + }); + + it('Should return an empty object for an empty object input', () => { + expect(toValidTestCells({})).to.deep.equal({}); + }); + + it('Should return an empty object for an array input', () => { + expect(toValidTestCells(['foo'])).to.deep.equal({}); + }); + + it('Should return an empty object for a string input', () => { + expect(toValidTestCells('foo')).to.deep.equal({}); + }); + + it('Should return an empty object for a number input', () => { + expect(toValidTestCells(123)).to.deep.equal({}); + }); + + it('Should filter out entries with null values', () => { + expect(toValidTestCells({ key: null })).to.deep.equal({}); + }); + + it('Should filter out entries with undefined values', () => { + expect(toValidTestCells({ key: undefined })).to.deep.equal({}); + }); + + it('Should filter out entries with numeric values', () => { + expect(toValidTestCells({ key: 123 })).to.deep.equal({}); + }); + + it('Should filter out entries with object values', () => { + expect(toValidTestCells({ key: { nested: 'value' } })).to.deep.equal({}); + }); + + it('Should filter out entries with boolean values', () => { + expect(toValidTestCells({ key: true })).to.deep.equal({}); + }); + + it('Should filter out entries with empty string values', () => { + expect(toValidTestCells({ key: '' })).to.deep.equal({}); + }); + + it('Should keep entries with valid non-empty string values', () => { + expect(toValidTestCells({ key: 'valid' })).to.deep.equal({ key: 'valid' }); + }); + + it('Should keep only valid string entries from mixed input', () => { + const input = { + valid1: 'bar', + nullVal: null, + numVal: 123, + valid2: 'baz', + emptyStr: '', + objVal: {}, + undefVal: undefined, + }; + expect(toValidTestCells(input)).to.deep.equal({ valid1: 'bar', valid2: 'baz' }); + }); + }); }); diff --git a/src/constructorio.js b/src/constructorio.js index 452f0054..8a469785 100644 --- a/src/constructorio.js +++ b/src/constructorio.js @@ -130,7 +130,7 @@ class ConstructorIO { clientId: clientId || client_id, userId, segments, - testCells, + testCells: helpers.toValidTestCells(testCells), fetch: fetchFromOptions || fetch, trackingSendDelay, sendTrackingEvents, @@ -179,7 +179,7 @@ class ConstructorIO { } if (testCells) { - this.options.testCells = testCells; + this.options.testCells = helpers.toValidTestCells(testCells); } if (typeof sendTrackingEvents === 'boolean') { diff --git a/src/utils/helpers.js b/src/utils/helpers.js index 5a34c6c1..6ffc8396 100644 --- a/src/utils/helpers.js +++ b/src/utils/helpers.js @@ -373,6 +373,23 @@ const utils = { truncateString: (string, maxLength) => string.slice(0, maxLength), + // Filter testCells to only include entries with non-empty string values + toValidTestCells: (testCells) => { + if (!testCells || typeof testCells !== 'object' || Array.isArray(testCells)) { + return {}; + } + + const filtered = {}; + + Object.keys(testCells).forEach((key) => { + if (typeof testCells[key] === 'string' && testCells[key].trim() !== '') { + filtered[key] = testCells[key]; + } + }); + + return filtered; + }, + getBehaviorUrl: (mediaServiceUrl) => { const baseUrl = new URL(mediaServiceUrl);