diff --git a/libraries/schainSerializer/schainSerializer.js b/libraries/schainSerializer/schainSerializer.js new file mode 100644 index 000000000000..7d9a3c4ddc6d --- /dev/null +++ b/libraries/schainSerializer/schainSerializer.js @@ -0,0 +1,24 @@ +/** + * Serialize the SupplyChain for Non-OpenRTB Requests + * https://github.com/InteractiveAdvertisingBureau/openrtb/blob/main/supplychainobject.md + * + * @param {Object} schain The supply chain object. + * @param {string} schain.ver The version of the supply chain. + * @param {number} schain.complete Indicates if the chain is complete (1) or not (0). + * @param {Array} schain.nodes An array of nodes in the supply chain. + * @param {Array} nodesProperties The list of node properties to include in the serialized string. + * Can include: 'asi', 'sid', 'hp', 'rid', 'name', 'domain', 'ext'. + * @returns {string|null} The serialized supply chain string or null if the nodes are not present. + */ +export function serializeSupplyChain(schain, nodesProperties) { + if (!schain?.nodes) return null; + + const header = `${schain.ver},${schain.complete}!`; + const nodes = schain.nodes.map( + node => nodesProperties.map( + prop => node[prop] ? encodeURIComponent(node[prop]).replace(/!/g, '%21') : '' + ).join(',') + ).join('!'); + + return header + nodes; +} diff --git a/modules/smilewantedBidAdapter.js b/modules/smilewantedBidAdapter.js index 7d4a4bca6157..7f83dc025cb5 100644 --- a/modules/smilewantedBidAdapter.js +++ b/modules/smilewantedBidAdapter.js @@ -4,6 +4,7 @@ import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import {INSTREAM, OUTSTREAM} from '../src/video.js'; +import {serializeSupplyChain} from '../libraries/schainSerializer/schainSerializer.js' import {convertOrtbRequestToProprietaryNative, toOrtbNativeRequest, toLegacyResponse} from '../src/native.js'; const BIDDER_CODE = 'smilewanted'; @@ -82,7 +83,8 @@ export const spec = { or from mediaTypes.banner.pos */ positionType: bid.params.positionType || '', - prebidVersion: '$prebid.version$' + prebidVersion: '$prebid.version$', + schain: serializeSupplyChain(bid.schain, ['asi', 'sid', 'hp', 'rid', 'name', 'domain', 'ext']), }; const floor = getBidFloor(bid); @@ -154,16 +156,16 @@ export const spec = { if (response) { const dealId = response.dealId || ''; const bidResponse = { - requestId: bidRequestData.bidId, + ad: response.ad, cpm: response.cpm, - width: response.width, - height: response.height, creativeId: response.creativeId, - dealId: response.dealId, currency: response.currency, + dealId: response.dealId, + height: response.height, netRevenue: response.isNetCpm, + requestId: bidRequestData.bidId, ttl: response.ttl, - ad: response.ad, + width: response.width, }; if (response.formatTypeSw === 'video_instream' || response.formatTypeSw === 'video_outstream') { @@ -209,28 +211,30 @@ export const spec = { * @param {Object} uspConsent The USP consent parameters * @return {UserSync[]} The user syncs which should be dropped. */ - getUserSyncs: function(syncOptions, responses, gdprConsent, uspConsent) { - let params = ''; - - if (gdprConsent && typeof gdprConsent.consentString === 'string') { - // add 'gdpr' only if 'gdprApplies' is defined - if (typeof gdprConsent.gdprApplies === 'boolean') { - params += `?gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; - } else { - params += `?gdpr_consent=${gdprConsent.consentString}`; + getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent) { + const syncs = []; + + if (syncOptions.iframeEnabled) { + let params = []; + + if (gdprConsent && typeof gdprConsent.consentString === 'string') { + // add 'gdpr' only if 'gdprApplies' is defined + if (typeof gdprConsent.gdprApplies === 'boolean') { + params.push(`gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`); + } else { + params.push(`gdpr_consent=${gdprConsent.consentString}`); + } } - } - if (uspConsent) { - params += `${params ? '&' : '?'}us_privacy=${encodeURIComponent(uspConsent)}`; - } + if (uspConsent) { + params.push(`us_privacy=${encodeURIComponent(uspConsent)}`); + } - const syncs = [] + const paramsStr = params.length > 0 ? '?' + params.join('&') : ''; - if (syncOptions.iframeEnabled) { syncs.push({ type: 'iframe', - url: 'https://csync.smilewanted.com' + params + url: 'https://csync.smilewanted.com' + paramsStr }); } diff --git a/test/spec/modules/smilewantedBidAdapter_spec.js b/test/spec/modules/smilewantedBidAdapter_spec.js index 99c4034610f5..1c71c7bee079 100644 --- a/test/spec/modules/smilewantedBidAdapter_spec.js +++ b/test/spec/modules/smilewantedBidAdapter_spec.js @@ -76,6 +76,49 @@ const DISPLAY_REQUEST_WITH_POSITION_TYPE = [{ }, }]; +const SCHAIN = { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'exchange1.com', + 'sid': '1234', + 'hp': 1, + 'rid': 'bid-request-1', + 'name': 'publisher', + 'domain': 'publisher.com' + }, + { + 'asi': 'exchange2.com', + 'sid': 'abcd', + 'hp': 1, + 'rid': 'bid-request-2', + 'name': 'intermediary', + 'domain': 'intermediary.com' + } + ] +}; + +const DISPLAY_REQUEST_WITH_SCHAIN = [{ + adUnitCode: 'sw_300x250', + bidId: '12345', + sizes: [ + [300, 250], + [300, 200] + ], + bidder: 'smilewanted', + params: { + zoneId: 1, + }, + requestId: 'request_abcd1234', + ortb2Imp: { + ext: { + tid: 'trans_abcd1234', + } + }, + schain: SCHAIN, +}]; + const BID_RESPONSE_DISPLAY = { body: { cpm: 3, @@ -580,8 +623,21 @@ describe('smilewantedBidAdapterTests', function () { expect(requestContent).to.have.property('positionType').and.to.equal('infeed'); }); + it('SmileWanted - Verify if schain is well passed', function () { + const request = spec.buildRequests(DISPLAY_REQUEST_WITH_SCHAIN, {}); + const requestContent = JSON.parse(request[0].data); + expect(requestContent).to.have.property('schain').and.to.equal('1.0,1!exchange1.com,1234,1,bid-request-1,publisher,publisher.com,!exchange2.com,abcd,1,bid-request-2,intermediary,intermediary.com,'); + }); + + it('SmileWanted - Verify user sync - empty data', function () { + let syncs = spec.getUserSyncs({iframeEnabled: true}, {}, {}, null); + expect(syncs).to.have.lengthOf(1); + expect(syncs[0].type).to.equal('iframe'); + expect(syncs[0].url).to.equal('https://csync.smilewanted.com'); + }); + it('SmileWanted - Verify user sync', function () { - var syncs = spec.getUserSyncs({iframeEnabled: true}, {}, { + let syncs = spec.getUserSyncs({iframeEnabled: true}, {}, { consentString: 'foo' }, '1NYN'); expect(syncs).to.have.lengthOf(1); diff --git a/test/spec/schainSerializer/schainSerializer_spec.js b/test/spec/schainSerializer/schainSerializer_spec.js new file mode 100644 index 000000000000..d04e6de98472 --- /dev/null +++ b/test/spec/schainSerializer/schainSerializer_spec.js @@ -0,0 +1,138 @@ +import {serializeSupplyChain} from '../../../libraries/schainSerializer/schainSerializer.js' +describe('serializeSupplyChain', () => { + describe('Single Hop - Chain Complete', () => { + it('should serialize a single hop chain with complete information', () => { + const schain = { + ver: '1.0', + complete: 1, + nodes: [ + { + asi: 'exchange1.com', + sid: '1234', + hp: 1, + rid: 'bid-request-1', + name: 'publisher', + domain: 'publisher.com' + } + ] + }; + const nodesProperties = ['asi', 'sid', 'hp', 'rid', 'name', 'domain']; + const expectedResult = '1.0,1!exchange1.com,1234,1,bid-request-1,publisher,publisher.com'; + expect(serializeSupplyChain(schain, nodesProperties)).to.equal(expectedResult); + }); + }); + + describe('Single Hop - Chain Complete, optional fields missing', () => { + it('should serialize a single hop chain with missing optional fields', () => { + const schain = { + ver: '1.0', + complete: 1, + nodes: [ + { + asi: 'exchange1.com', + sid: '1234', + hp: 1 + } + ] + }; + const nodesProperties = ['asi', 'sid', 'hp', 'rid', 'name', 'domain']; + const expectedResult = '1.0,1!exchange1.com,1234,1,,,'; + expect(serializeSupplyChain(schain, nodesProperties)).to.equal(expectedResult); + }); + }); + + describe('Multiple Hops - With all properties supplied', () => { + it('should serialize multiple hops with all properties supplied', () => { + const schain = { + ver: '1.0', + complete: 1, + nodes: [ + { + asi: 'exchange1.com', + sid: '1234', + hp: 1, + rid: 'bid-request-1', + name: 'publisher', + domain: 'publisher.com' + }, + { + asi: 'exchange2.com', + sid: 'abcd', + hp: 1, + rid: 'bid-request-2', + name: 'intermediary', + domain: 'intermediary.com' + } + ] + }; + const nodesProperties = ['asi', 'sid', 'hp', 'rid', 'name', 'domain']; + const expectedResult = '1.0,1!exchange1.com,1234,1,bid-request-1,publisher,publisher.com!exchange2.com,abcd,1,bid-request-2,intermediary,intermediary.com'; + expect(serializeSupplyChain(schain, nodesProperties)).to.equal(expectedResult); + }); + }); + + describe('Multiple Hops - Chain Complete, optional fields missing', () => { + it('should serialize multiple hops with missing optional fields', () => { + const schain = { + ver: '1.0', + complete: 1, + nodes: [ + { + asi: 'exchange1.com', + sid: '1234', + hp: 1 + }, + { + asi: 'exchange2.com', + sid: 'abcd', + hp: 1 + } + ] + }; + const nodesProperties = ['asi', 'sid', 'hp', 'rid', 'name', 'domain']; + const expectedResult = '1.0,1!exchange1.com,1234,1,,,!exchange2.com,abcd,1,,,'; + expect(serializeSupplyChain(schain, nodesProperties)).to.equal(expectedResult); + }); + }); + + describe('Multiple Hops Expected - Chain Incomplete', () => { + it('should serialize multiple hops with chain incomplete', () => { + const schain = { + ver: '1.0', + complete: 0, + nodes: [ + { + asi: 'exchange2.com', + sid: 'abcd', + hp: 1 + } + ] + }; + const nodesProperties = ['asi', 'sid', 'hp', 'rid', 'name', 'domain']; + const expectedResult = '1.0,0!exchange2.com,abcd,1,,,'; + expect(serializeSupplyChain(schain, nodesProperties)).to.equal(expectedResult); + }); + }); + + describe('Single Hop - Chain Complete, encoded values', () => { + it('should serialize a single hop chain with encoded values', () => { + const schain = { + ver: '1.0', + complete: 1, + nodes: [ + { + asi: 'exchange1.com', + sid: '1234!abcd', + hp: 1, + rid: 'bid-request-1', + name: 'publisher, Inc.', + domain: 'publisher.com' + } + ] + }; + const nodesProperties = ['asi', 'sid', 'hp', 'rid', 'name', 'domain']; + const expectedResult = '1.0,1!exchange1.com,1234%21abcd,1,bid-request-1,publisher%2C%20Inc.,publisher.com'; + expect(serializeSupplyChain(schain, nodesProperties)).to.equal(expectedResult); + }); + }); +});