From 9abb40fbc7bb0be622aaf8dc3289e2fc5db37673 Mon Sep 17 00:00:00 2001 From: Shannon Broekhoven Date: Thu, 27 Feb 2020 10:46:33 -0500 Subject: [PATCH 1/8] Add Sortable adapter for Prebid 3.x Update tests to reflect changes. --- modules/sortableBidAdapter.js | 343 ++++++++++++ test/spec/modules/sortableBidAdapter_spec.js | 539 +++++++++++++++++++ 2 files changed, 882 insertions(+) create mode 100644 modules/sortableBidAdapter.js create mode 100644 test/spec/modules/sortableBidAdapter_spec.js diff --git a/modules/sortableBidAdapter.js b/modules/sortableBidAdapter.js new file mode 100644 index 00000000000..3e059a2c798 --- /dev/null +++ b/modules/sortableBidAdapter.js @@ -0,0 +1,343 @@ +import * as utils from '../src/utils'; +import { registerBidder } from '../src/adapters/bidderFactory'; +import { config } from '../src/config'; +import { parse } from '../src/url'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes'; + +const BIDDER_CODE = 'sortable'; +const SERVER_URL = 'c.deployads.com'; + +function setAssetRequired(native, asset) { + if (native.required) { + asset.required = 1; + } + return asset; +} + +function buildNativeRequest(nativeMediaType) { + const assets = []; + const title = nativeMediaType.title; + if (title) { + assets.push(setAssetRequired(title, { + title: {len: title.len} + })); + } + const img = nativeMediaType.image; + if (img) { + assets.push(setAssetRequired(img, { + img: { + type: 3, // Main + wmin: 1, + hmin: 1 + } + })); + } + const icon = nativeMediaType.icon; + if (icon) { + assets.push(setAssetRequired(icon, { + img: { + type: 1, // Icon + wmin: 1, + hmin: 1 + } + })); + } + const body = nativeMediaType.body; + if (body) { + assets.push(setAssetRequired(body, {data: {type: 2}})); + } + const cta = nativeMediaType.cta; + if (cta) { + assets.push(setAssetRequired(cta, {data: {type: 12}})); + } + const sponsoredBy = nativeMediaType.sponsoredBy; + if (sponsoredBy) { + assets.push(setAssetRequired(sponsoredBy, {data: {type: 1}})); + } + + utils._each(assets, (asset, id) => asset.id = id); + return { + ver: '1', + request: JSON.stringify({ + ver: '1', + assets + }) + }; +} + +function tryParseNativeResponse(adm) { + let native = null; + try { + native = JSON.parse(adm); + } catch (e) { + utils.logError('Sortable bid adapter unable to parse native bid response:\n\n' + e); + } + return native && native.native; +} + +function createImgObject(img) { + if (img.w || img.h) { + return { + url: img.url, + width: img.w, + height: img.h + }; + } else { + return img.url; + } +} + +function interpretNativeResponse(response) { + const native = {}; + if (response.link) { + native.clickUrl = response.link.url; + } + utils._each(response.assets, asset => { + switch (asset.id) { + case 1: + native.title = asset.title.text; + break; + case 2: + native.image = createImgObject(asset.img); + break; + case 3: + native.icon = createImgObject(asset.img); + break; + case 4: + native.body = asset.data.value; + break; + case 5: + native.cta = asset.data.value; + break; + case 6: + native.sponsoredBy = asset.data.value; + break; + } + }); + return native; +} + +function transformSyncs(responses, type, syncs) { + utils._each(responses, res => { + if (res.body && res.body.ext && res.body.ext.sync_dsps && res.body.ext.sync_dsps.length) { + utils._each(res.body.ext.sync_dsps, sync => { + if (sync[0] === type && sync[1]) { + syncs.push({type, url: sync[1]}); + } + }); + } + }); +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, NATIVE, VIDEO], + + isBidRequestValid: function(bid) { + const sortableConfig = config.getConfig('sortable'); + const haveSiteId = (sortableConfig && !!sortableConfig.siteId) || bid.params.siteId; + const validFloor = !bid.params.floor || utils.isNumber(bid.params.floor); + const validSize = /\d+x\d+/; + const validFloorSizeMap = !bid.params.floorSizeMap || + (utils.isPlainObject(bid.params.floorSizeMap) && + Object.keys(bid.params.floorSizeMap).every(size => + size.match(validSize) && utils.isNumber(bid.params.floorSizeMap[size]) + )) + const validKeywords = !bid.params.keywords || + (utils.isPlainObject(bid.params.keywords) && + Object.keys(bid.params.keywords).every(key => + utils.isStr(key) && utils.isStr(bid.params.keywords[key]) + )) + const isBanner = !bid.mediaTypes || bid.mediaTypes[BANNER] || !(bid.mediaTypes[NATIVE] || bid.mediaTypes[VIDEO]); + const bannerSizes = isBanner ? utils.deepAccess(bid, `mediaType.${BANNER}.sizes`) || bid.sizes : null; + return !!(bid.params.tagId && haveSiteId && validFloor && validFloorSizeMap && validKeywords && (!isBanner || + (bannerSizes && bannerSizes.length > 0 && bannerSizes.every(sizeArr => sizeArr.length == 2 && sizeArr.every(num => utils.isNumber(num)))))); + }, + + buildRequests: function(validBidReqs, bidderRequest) { + const sortableConfig = config.getConfig('sortable') || {}; + const globalSiteId = sortableConfig.siteId; + let loc = parse(bidderRequest.refererInfo.referer); + + const sortableImps = utils._map(validBidReqs, bid => { + const rv = { + id: bid.bidId, + tagid: bid.params.tagId, + ext: {} + }; + const bannerMediaType = utils.deepAccess(bid, `mediaTypes.${BANNER}`); + const nativeMediaType = utils.deepAccess(bid, `mediaTypes.${NATIVE}`); + const videoMediaType = utils.deepAccess(bid, `mediaTypes.${VIDEO}`); + if (bannerMediaType || !(nativeMediaType || videoMediaType)) { + const bannerSizes = (bannerMediaType && bannerMediaType.sizes) || bid.sizes; + rv.banner = { + format: utils._map(bannerSizes, ([width, height]) => ({w: width, h: height})) + }; + } + if (nativeMediaType) { + rv.native = buildNativeRequest(nativeMediaType); + } + if (videoMediaType && videoMediaType.context === 'instream') { + const video = {placement: 1}; + video.mimes = videoMediaType.mimes || []; + video.minduration = utils.deepAccess(bid, 'params.video.minduration') || 10; + video.maxduration = utils.deepAccess(bid, 'params.video.maxduration') || 60; + const startDelay = utils.deepAccess(bid, 'params.video.startdelay'); + if (startDelay != null) { + video.startdelay = startDelay; + } + if (videoMediaType.playerSize && videoMediaType.playerSize.length) { + const size = videoMediaType.playerSize[0]; + video.w = size[0]; + video.h = size[1]; + } + if (videoMediaType.api) { + video.api = videoMediaType.api; + } + if (videoMediaType.protocols) { + video.protocols = videoMediaType.protocols; + } + if (videoMediaType.playbackmethod) { + video.playbackmethod = videoMediaType.playbackmethod; + } + rv.video = video; + } + if (bid.params.floor) { + rv.bidfloor = bid.params.floor; + } + if (bid.params.keywords) { + rv.ext.keywords = bid.params.keywords; + } + if (bid.params.bidderParams) { + utils._each(bid.params.bidderParams, (params, partner) => { + rv.ext[partner] = params; + }); + } + if (bid.params.floorSizeMap) { + rv.ext.floorSizeMap = bid.params.floorSizeMap; + } + return rv; + }); + const gdprConsent = bidderRequest && bidderRequest.gdprConsent; + const sortableBidReq = { + id: utils.getUniqueIdentifierStr(), + imp: sortableImps, + source: { + ext: { + schain: validBidReqs[0].schain + } + }, + regs: { + ext: {} + }, + site: { + domain: loc.hostname, + page: loc.href, + ref: loc.href, + publisher: { + id: globalSiteId || validBidReqs[0].params.siteId, + }, + device: { + w: screen.width, + h: screen.height + }, + }, + }; + if (bidderRequest && bidderRequest.timeout > 0) { + sortableBidReq.tmax = bidderRequest.timeout; + } + if (gdprConsent) { + sortableBidReq.user = { + ext: { + consent: gdprConsent.consentString + } + }; + if (typeof gdprConsent.gdprApplies == 'boolean') { + sortableBidReq.regs.ext.gdpr = gdprConsent.gdprApplies ? 1 : 0 + } + } + if (bidderRequest.uspConsent) { + sortableBidReq.regs.ext.us_privacy = bidderRequest.uspConsent; + } + return { + method: 'POST', + url: `//${SERVER_URL}/openrtb2/auction?src=$$REPO_AND_VERSION$$&host=${loc.host}`, + data: JSON.stringify(sortableBidReq), + options: {contentType: 'text/plain'} + }; + }, + + interpretResponse: function(serverResponse) { + const { body: {id, seatbid} } = serverResponse; + const sortableBids = []; + if (id && seatbid) { + utils._each(seatbid, seatbid => { + utils._each(seatbid.bid, bid => { + const bidObj = { + requestId: bid.impid, + cpm: parseFloat(bid.price), + width: parseInt(bid.w), + height: parseInt(bid.h), + creativeId: bid.crid || bid.id, + dealId: bid.dealid || null, + currency: 'USD', + netRevenue: true, + mediaType: BANNER, + ttl: 60 + }; + if (bid.adm) { + const adFormat = utils.deepAccess(bid, 'ext.ad_format') + if (adFormat === 'native') { + let native = tryParseNativeResponse(bid.adm); + if (!native) { + return; + } + bidObj.mediaType = NATIVE; + bidObj.native = interpretNativeResponse(native); + } else if (adFormat === 'instream') { + bidObj.mediaType = VIDEO; + bidObj.vastXml = bid.adm; + } else { + bidObj.mediaType = BANNER; + bidObj.ad = bid.adm; + if (bid.nurl) { + bidObj.ad += utils.createTrackPixelHtml(decodeURIComponent(bid.nurl)); + } + } + } else if (bid.nurl) { + bidObj.adUrl = bid.nurl; + } + if (bid.ext) { + bidObj[BIDDER_CODE] = bid.ext; + } + sortableBids.push(bidObj); + }); + }); + } + return sortableBids; + }, + + getUserSyncs: (syncOptions, responses) => { + const syncs = []; + if (syncOptions.iframeEnabled) { + transformSyncs(responses, 'iframe', syncs); + } + if (syncOptions.pixelEnabled) { + transformSyncs(responses, 'image', syncs); + } + return syncs; + }, + + onTimeout(details) { + fetch(`//${SERVER_URL}/prebid/timeout`, { + method: 'POST', + body: JSON.stringify(details), + mode: 'no-cors', + headers: new Headers({ + 'Content-Type': 'text/plain' + }) + }); + } +}; + +registerBidder(spec); diff --git a/test/spec/modules/sortableBidAdapter_spec.js b/test/spec/modules/sortableBidAdapter_spec.js new file mode 100644 index 00000000000..782c303f002 --- /dev/null +++ b/test/spec/modules/sortableBidAdapter_spec.js @@ -0,0 +1,539 @@ +import { expect } from 'chai'; +import { spec } from 'modules/sortableBidAdapter'; +import { newBidder } from 'src/adapters/bidderFactory'; +import * as utils from 'src/utils'; + +describe('sortableBidAdapter', function() { + const adapter = newBidder(spec); + + describe('isBidRequestValid', function () { + function makeBid() { + return { + 'bidder': 'sortable', + 'params': { + 'tagId': '403370', + 'siteId': 'example.com', + 'keywords': { + 'key1': 'val1', + 'key2': 'val2' + }, + 'floorSizeMap': { + '728x90': 0.15, + '300x250': 1.20 + } + }, + 'adUnitCode': 'adunit-code', + 'sizes': [ + [300, 250] + ], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; + } + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(makeBid())).to.equal(true); + }); + + it('should return false when tagId not passed correctly', function () { + let bid = makeBid(); + delete bid.params.tagId; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when sizes not passed correctly', function () { + let bid = makeBid(); + delete bid.sizes; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when sizes are wrong length', function () { + let bid = makeBid(); + bid.sizes = [[300]]; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when sizes are empty', function () { + let bid = makeBid(); + bid.sizes = []; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when require params are not passed', function () { + let bid = makeBid(); + bid.params = {}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when the floorSizeMap is invalid', function () { + let bid = makeBid(); + bid.params.floorSizeMap = { + 'sixforty by foureighty': 1234 + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + bid.params.floorSizeMap = { + '728x90': 'three' + } + expect(spec.isBidRequestValid(bid)).to.equal(false); + bid.params.floorSizeMap = 'a'; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return true when the floorSizeMap is missing or empty', function () { + let bid = makeBid(); + bid.params.floorSizeMap = {}; + expect(spec.isBidRequestValid(bid)).to.equal(true); + delete bid.params.floorSizeMap; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + it('should return false when the keywords are invalid', function () { + let bid = makeBid(); + bid.params.keywords = { + 'badval': 1234 + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + bid.params.keywords = 'a'; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return true when the keywords are missing or empty', function () { + let bid = makeBid(); + bid.params.keywords = {}; + expect(spec.isBidRequestValid(bid)).to.equal(true); + delete bid.params.keywords; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return true with video media type', () => { + const videoBid = { + 'bidder': 'sortable', + 'params': { + 'tagId': '403370', + 'siteId': 'example.com', + }, + 'adUnitCode': 'adunit-code', + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'mediaTypes': { + 'video': { + } + } + }; + expect(spec.isBidRequestValid(videoBid)).to.equal(true); + }); + }); + + describe('buildRequests', function () { + const bidRequests = [{ + 'bidder': 'sortable', + 'params': { + 'tagId': '403370', + 'siteId': 'example.com', + 'floor': 0.21, + 'keywords': { + 'key1': 'val1', + 'key2': 'val2' + }, + 'floorSizeMap': { + '728x90': 0.15, + '300x250': 1.20 + } + }, + 'sizes': [ + [300, 250] + ], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475' + }, { + 'bidder': 'sortable', + 'params': { + 'tagId': '403371', + 'siteId': 'example.com', + 'floor': 0.21 + }, + 'sizes': [ + [300, 250] + ], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'mediaTypes': { + 'native': { + 'body': {'required': true, 'sendId': true}, + 'clickUrl': {'required': true, 'sendId': true}, + 'cta': {'required': true, 'sendId': true}, + 'icon': {'required': true, 'sendId': true}, + 'image': {'required': true, 'sendId': true}, + 'sponsoredBy': {'required': true, 'sendId': true}, + 'title': {'required': true, 'sendId': true, 'len': 100} + } + } + }]; + + const request = spec.buildRequests(bidRequests, {refererInfo: { referer: 'http://example.com/page?param=val' }}); + const requestBody = JSON.parse(request.data); + + it('sends bid request to our endpoint via POST', function () { + expect(request.method).to.equal('POST'); + }); + + it('attaches source and version to endpoint URL as query params', function () { + const ENDPOINT = `//c.deployads.com/openrtb2/auction?src=$$REPO_AND_VERSION$$&host=example.com`; + expect(request.url).to.equal(ENDPOINT); + }); + + it('sends screen dimensions', function () { + expect(requestBody.site.device.w).to.equal(screen.width); + expect(requestBody.site.device.h).to.equal(screen.height); + }); + + it('includes the ad size in the bid request', function () { + expect(requestBody.imp[0].banner.format[0].w).to.equal(300); + expect(requestBody.imp[0].banner.format[0].h).to.equal(250); + }); + + it('includes the params in the bid request', function () { + expect(requestBody.imp[0].ext.keywords).to.deep.equal( + {'key1': 'val1', + 'key2': 'val2'} + ); + expect(requestBody.site.publisher.id).to.equal('example.com'); + expect(requestBody.imp[0].tagid).to.equal('403370'); + expect(requestBody.imp[0].bidfloor).to.equal(0.21); + }); + + it('should have the floor size map set', function () { + expect(requestBody.imp[0].ext.floorSizeMap).to.deep.equal({ + '728x90': 0.15, + '300x250': 1.20 + }); + }); + + it('sets domain and href correctly', function () { + expect(requestBody.site.domain).to.equal('example.com'); + expect(requestBody.site.page).to.equal('http://example.com/page?param=val'); + }); + + it('should have the version in native object set for native bid', function() { + expect(requestBody.imp[1].native.ver).to.equal('1'); + }); + + it('should have the assets set for native bid', function() { + const assets = JSON.parse(requestBody.imp[1].native.request).assets; + expect(assets[0]).to.deep.equal({'title': {'len': 100}, 'required': 1, 'id': 0}); + expect(assets[1]).to.deep.equal({'img': {'type': 3, 'wmin': 1, 'hmin': 1}, 'required': 1, 'id': 1}); + expect(assets[2]).to.deep.equal({'img': {'type': 1, 'wmin': 1, 'hmin': 1}, 'required': 1, 'id': 2}); + expect(assets[3]).to.deep.equal({'data': {'type': 2}, 'required': 1, 'id': 3}); + expect(assets[4]).to.deep.equal({'data': {'type': 12}, 'required': 1, 'id': 4}); + expect(assets[5]).to.deep.equal({'data': {'type': 1}, 'required': 1, 'id': 5}); + }); + + const videoBidRequests = [{ + 'bidder': 'sortable', + 'params': { + 'tagId': '403370', + 'siteId': 'example.com', + 'video': { + 'minduration': 5, + 'maxduration': 10, + 'startdelay': 0 + } + }, + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'mediaTypes': { + 'video': { + 'context': 'instream', + 'mimes': ['video/x-ms-wmv'], + 'playerSize': [[400, 300]], + 'api': [0], + 'protocols': [2, 3], + 'playbackmethod': [1] + } + } + }]; + + const videoRequest = spec.buildRequests(videoBidRequests, {refererInfo: { referer: 'http://localhost:9876/' }}); + const videoRequestBody = JSON.parse(videoRequest.data); + + it('should include video params', () => { + const video = videoRequestBody.imp[0].video; + expect(video.mimes).to.deep.equal(['video/x-ms-wmv']); + expect(video.w).to.equal(400); + expect(video.h).to.equal(300); + expect(video.api).to.deep.equal([0]); + expect(video.protocols).to.deep.equal([2, 3]); + expect(video.playbackmethod).to.deep.equal([1]); + expect(video.minduration).to.equal(5); + expect(video.maxduration).to.equal(10); + expect(video.startdelay).to.equal(0); + }); + + it('sets domain and href correctly', function () { + expect(videoRequestBody.site.domain).to.equal('localhost'); + expect(videoRequestBody.site.page).to.equal('http://localhost:9876/'); + }); + + const gdprBidRequests = [{ + 'bidder': 'sortable', + 'params': { + 'tagId': '403370', + 'siteId': 'example.com', + 'floor': 0.21, + 'keywords': {}, + 'floorSizeMap': {} + }, + 'sizes': [ + [300, 250] + ], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475' + }]; + const consentString = 'BOJ/P2HOJ/P2HABABMAAAAAZ+A=='; + + function getGdprRequestBody(gdprApplies, consentString) { + const gdprRequest = spec.buildRequests(gdprBidRequests, {'gdprConsent': { + 'gdprApplies': gdprApplies, + 'consentString': consentString + }, + refererInfo: { + referer: 'http://localhost:9876/' + }}); + return JSON.parse(gdprRequest.data); + } + + it('should handle gdprApplies being present and true', function() { + const gdprRequestBody = getGdprRequestBody(true, consentString); + expect(gdprRequestBody.regs.ext.gdpr).to.equal(1); + expect(gdprRequestBody.user.ext.consent).to.equal(consentString); + }) + + it('should handle gdprApplies being present and false', function() { + const gdprRequestBody = getGdprRequestBody(false, consentString); + expect(gdprRequestBody.regs.ext.gdpr).to.equal(0); + expect(gdprRequestBody.user.ext.consent).to.equal(consentString); + }) + + it('should handle gdprApplies being undefined', function() { + const gdprRequestBody = getGdprRequestBody(undefined, consentString); + expect(gdprRequestBody.regs).to.deep.equal({ext: {}}); + expect(gdprRequestBody.user.ext.consent).to.equal(consentString); + }) + + it('should handle gdprConsent being undefined', function() { + const gdprRequest = spec.buildRequests(gdprBidRequests, {refererInfo: { referer: 'http://localhost:9876/' }}); + const gdprRequestBody = JSON.parse(gdprRequest.data); + expect(gdprRequestBody.regs).to.deep.equal({ext: {}}); + expect(gdprRequestBody.user).to.equal(undefined); + }) + }); + + describe('interpretResponse', function () { + function makeResponse() { + return { + body: { + 'id': '5e5c23a5ba71e78', + 'seatbid': [ + { + 'bid': [ + { + 'id': '6vmb3isptf', + 'crid': 'sortablescreative', + 'impid': '322add653672f68', + 'price': 1.22, + 'adm': '', + 'attr': [5], + 'h': 90, + 'nurl': 'http://nurl', + 'w': 728 + } + ], + 'seat': 'MOCK' + } + ], + 'bidid': '5e5c23a5ba71e78' + } + }; + } + + function makeNativeResponse() { + return { + body: { + 'id': '5e5c23a5ba71e77', + 'seatbid': [ + { + 'bid': [ + { + 'id': '6vmb3isptf', + 'crid': 'sortablescreative', + 'impid': '322add653672f67', + 'price': 1.55, + 'adm': '{"native":{"link":{"clicktrackers":[],"url":"https://www.sortable.com/"},"assets":[{"title":{"text":"Ads With Sortable"},"id":1},{"img":{"w":790,"url":"https://path.to/image","h":294},"id":2},{"img":{"url":"https://path.to/icon"},"id":3},{"data":{"value":"Body here"},"id":4},{"data":{"value":"Learn More"},"id":5},{"data":{"value":"Sortable"},"id":6}],"imptrackers":[],"ver":1}}', + 'ext': {'ad_format': 'native'}, + 'h': 90, + 'nurl': 'http://nurl', + 'w': 728 + } + ], + 'seat': 'MOCK' + } + ], + 'bidid': '5e5c23a5ba71e77' + } + }; + } + + const expectedBid = { + 'requestId': '322add653672f68', + 'cpm': 1.22, + 'width': 728, + 'height': 90, + 'creativeId': 'sortablescreative', + 'dealId': null, + 'currency': 'USD', + 'netRevenue': true, + 'mediaType': 'banner', + 'ttl': 60, + 'ad': '
' + }; + + const expectedNativeBid = { + 'requestId': '322add653672f67', + 'cpm': 1.55, + 'width': 728, + 'height': 90, + 'creativeId': 'sortablescreative', + 'dealId': null, + 'currency': 'USD', + 'netRevenue': true, + 'sortable': { 'ad_format': 'native' }, + 'mediaType': 'native', + 'ttl': 60, + 'native': { + 'clickUrl': 'https://www.sortable.com/', + 'title': 'Ads With Sortable', + 'image': {'url': 'https://path.to/image', 'height': 294, 'width': 790}, + 'icon': 'https://path.to/icon', + 'body': 'Body here', + 'cta': 'Learn More', + 'sponsoredBy': 'Sortable' + } + }; + + it('should get the correct bid response', function () { + let result = spec.interpretResponse(makeResponse()); + expect(result.length).to.equal(1); + expect(result[0]).to.deep.equal(expectedBid); + }); + + it('should handle a missing crid', function () { + let noCridResponse = makeResponse(); + delete noCridResponse.body.seatbid[0].bid[0].crid; + const fallbackCrid = noCridResponse.body.seatbid[0].bid[0].id; + let noCridResult = Object.assign({}, expectedBid, {'creativeId': fallbackCrid}); + let result = spec.interpretResponse(noCridResponse); + expect(result.length).to.equal(1); + expect(result[0]).to.deep.equal(noCridResult); + }); + + it('should handle a missing nurl', function () { + let noNurlResponse = makeResponse(); + delete noNurlResponse.body.seatbid[0].bid[0].nurl; + let noNurlResult = Object.assign({}, expectedBid); + noNurlResult.ad = ''; + let result = spec.interpretResponse(noNurlResponse); + expect(result.length).to.equal(1); + expect(result[0]).to.deep.equal(noNurlResult); + }); + + it('should handle a missing adm', function () { + let noAdmResponse = makeResponse(); + delete noAdmResponse.body.seatbid[0].bid[0].adm; + let noAdmResult = Object.assign({}, expectedBid); + delete noAdmResult.ad; + noAdmResult.adUrl = 'http://nurl'; + let result = spec.interpretResponse(noAdmResponse); + expect(result.length).to.equal(1); + expect(result[0]).to.deep.equal(noAdmResult); + }); + + it('handles empty bid response', function () { + let response = { + body: { + 'id': '5e5c23a5ba71e78', + 'seatbid': [] + } + }; + let result = spec.interpretResponse(response); + expect(result.length).to.equal(0); + }); + + it('should get the correct native bid response', function () { + let result = spec.interpretResponse(makeNativeResponse()); + expect(result.length).to.equal(1); + expect(result[0]).to.deep.equal(expectedNativeBid); + }); + + it('fail to parse invalid native bid response', function () { + let response = makeNativeResponse(); + response.body.seatbid[0].bid[0].adm = ''; + let result = spec.interpretResponse(response); + expect(result.length).to.equal(0); + }); + + it('should keep custom properties', () => { + const customProperties = {test: 'a test message', param: {testParam: 1}}; + const expectedResult = Object.assign({}, expectedBid, {[spec.code]: customProperties}); + const response = makeResponse(); + response.body.seatbid[0].bid[0].ext = customProperties; + const result = spec.interpretResponse(response); + expect(result.length).to.equal(1); + expect(result[0]).to.deep.equal(expectedResult); + }); + + it('should handle instream response', () => { + const response = makeResponse(); + const bid = response.body.seatbid[0].bid[0]; + delete bid.nurl; + bid.ext = {ad_format: 'instream'}; + const result = spec.interpretResponse(response)[0]; + expect(result.mediaType).to.equal('video'); + expect(result.vastXml).to.equal(bid.adm); + }); + + it('should return iframe syncs', () => { + const syncResponse = { + ext: { + sync_dsps: [ + ['iframe', 'http://example-dsp/sync-iframe'], + ['image', 'http://example-dsp/sync-image'] + ] + } + }; + expect(spec.getUserSyncs({iframeEnabled: true}, [{body: syncResponse}])).to.deep.equal([{ + type: 'iframe', + url: 'http://example-dsp/sync-iframe' + }]); + }); + + it('should return image syncs', () => { + const syncResponse = { + ext: { + sync_dsps: [ + ['iframe', 'http://example-dsp/sync-iframe'], + ['image', 'http://example-dsp/sync-image'] + ] + } + }; + expect(spec.getUserSyncs({pixelEnabled: true}, [{body: syncResponse}])).to.deep.equal([{ + type: 'image', + url: 'http://example-dsp/sync-image' + }]); + }); + }); +}); From 02d79ead9bd69bf63744a6b644109e45919273f5 Mon Sep 17 00:00:00 2001 From: Shannon Broekhoven Date: Thu, 27 Feb 2020 15:48:12 -0500 Subject: [PATCH 2/8] Add .js in imports --- modules/sortableBidAdapter.js | 10 +++++----- test/spec/modules/sortableBidAdapter_spec.js | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/modules/sortableBidAdapter.js b/modules/sortableBidAdapter.js index 3e059a2c798..64456d212ba 100644 --- a/modules/sortableBidAdapter.js +++ b/modules/sortableBidAdapter.js @@ -1,8 +1,8 @@ -import * as utils from '../src/utils'; -import { registerBidder } from '../src/adapters/bidderFactory'; -import { config } from '../src/config'; -import { parse } from '../src/url'; -import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes'; +import * as utils from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; +import { parse } from '../src/url.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; const BIDDER_CODE = 'sortable'; const SERVER_URL = 'c.deployads.com'; diff --git a/test/spec/modules/sortableBidAdapter_spec.js b/test/spec/modules/sortableBidAdapter_spec.js index 782c303f002..173a2ab7639 100644 --- a/test/spec/modules/sortableBidAdapter_spec.js +++ b/test/spec/modules/sortableBidAdapter_spec.js @@ -1,7 +1,7 @@ import { expect } from 'chai'; -import { spec } from 'modules/sortableBidAdapter'; -import { newBidder } from 'src/adapters/bidderFactory'; -import * as utils from 'src/utils'; +import { spec } from 'modules/sortableBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import * as utils from 'src/utils.js'; describe('sortableBidAdapter', function() { const adapter = newBidder(spec); From ca89e970a35c1e604ffde2488c18974b9b03a9b3 Mon Sep 17 00:00:00 2001 From: Shannon Broekhoven Date: Thu, 27 Feb 2020 17:10:50 -0500 Subject: [PATCH 3/8] hostname not host: don't include port --- modules/sortableBidAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/sortableBidAdapter.js b/modules/sortableBidAdapter.js index 64456d212ba..33c4fbcf08a 100644 --- a/modules/sortableBidAdapter.js +++ b/modules/sortableBidAdapter.js @@ -261,7 +261,7 @@ export const spec = { } return { method: 'POST', - url: `//${SERVER_URL}/openrtb2/auction?src=$$REPO_AND_VERSION$$&host=${loc.host}`, + url: `//${SERVER_URL}/openrtb2/auction?src=$$REPO_AND_VERSION$$&host=${loc.hostname}`, data: JSON.stringify(sortableBidReq), options: {contentType: 'text/plain'} }; From f8bcfe11a8bde4add1eaad01f45d2660b8cd1fe4 Mon Sep 17 00:00:00 2001 From: Shannon Broekhoven Date: Thu, 27 Feb 2020 17:27:49 -0500 Subject: [PATCH 4/8] Trivial change to trigger build: failure wasn't our adapter --- modules/sortableBidAdapter.js | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/sortableBidAdapter.js b/modules/sortableBidAdapter.js index 33c4fbcf08a..b6ecae114e1 100644 --- a/modules/sortableBidAdapter.js +++ b/modules/sortableBidAdapter.js @@ -259,6 +259,7 @@ export const spec = { if (bidderRequest.uspConsent) { sortableBidReq.regs.ext.us_privacy = bidderRequest.uspConsent; } + return { method: 'POST', url: `//${SERVER_URL}/openrtb2/auction?src=$$REPO_AND_VERSION$$&host=${loc.hostname}`, From 7109aecee4506f729f00e163e85fa06e4552664a Mon Sep 17 00:00:00 2001 From: Shannon Broekhoven Date: Thu, 27 Feb 2020 17:33:24 -0500 Subject: [PATCH 5/8] More failures in other adapters --- modules/sortableBidAdapter.js | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/sortableBidAdapter.js b/modules/sortableBidAdapter.js index b6ecae114e1..33c4fbcf08a 100644 --- a/modules/sortableBidAdapter.js +++ b/modules/sortableBidAdapter.js @@ -259,7 +259,6 @@ export const spec = { if (bidderRequest.uspConsent) { sortableBidReq.regs.ext.us_privacy = bidderRequest.uspConsent; } - return { method: 'POST', url: `//${SERVER_URL}/openrtb2/auction?src=$$REPO_AND_VERSION$$&host=${loc.hostname}`, From 8734207266f86e9346b55b3ffab3e0db7135facb Mon Sep 17 00:00:00 2001 From: Shannon Broekhoven Date: Mon, 2 Mar 2020 13:50:55 -0500 Subject: [PATCH 6/8] PR Feedback - use https for URL - fix examples in markdown - request to endpoint should work now --- modules/sortableBidAdapter.js | 6 +++--- modules/sortableBidAdapter.md | 18 +++++++++++++++--- test/spec/modules/sortableBidAdapter_spec.js | 2 +- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/modules/sortableBidAdapter.js b/modules/sortableBidAdapter.js index 33c4fbcf08a..9fbb02ad08f 100644 --- a/modules/sortableBidAdapter.js +++ b/modules/sortableBidAdapter.js @@ -5,7 +5,7 @@ import { parse } from '../src/url.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; const BIDDER_CODE = 'sortable'; -const SERVER_URL = 'c.deployads.com'; +const SERVER_URL = 'https://c.deployads.com'; function setAssetRequired(native, asset) { if (native.required) { @@ -261,7 +261,7 @@ export const spec = { } return { method: 'POST', - url: `//${SERVER_URL}/openrtb2/auction?src=$$REPO_AND_VERSION$$&host=${loc.hostname}`, + url: `${SERVER_URL}/openrtb2/auction?src=$$REPO_AND_VERSION$$&host=${loc.hostname}`, data: JSON.stringify(sortableBidReq), options: {contentType: 'text/plain'} }; @@ -329,7 +329,7 @@ export const spec = { }, onTimeout(details) { - fetch(`//${SERVER_URL}/prebid/timeout`, { + fetch(`${SERVER_URL}/prebid/timeout`, { method: 'POST', body: JSON.stringify(details), mode: 'no-cors', diff --git a/modules/sortableBidAdapter.md b/modules/sortableBidAdapter.md index 027d6390e87..87ececfe5a1 100644 --- a/modules/sortableBidAdapter.md +++ b/modules/sortableBidAdapter.md @@ -16,7 +16,11 @@ Sortable's adapter integration to the Prebid library. Posts plain-text JSON to t var adUnits = [ { code: 'test-pb-leaderboard', - sizes: [[728, 90]], + mediaTypes: { + banner: { + sizes: [[728, 90]], + } + }, bids: [{ bidder: 'sortable', params: { @@ -30,7 +34,11 @@ var adUnits = [ }] }, { code: 'test-pb-banner', - sizes: [[300, 250]], + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, bids: [{ bidder: 'sortable', params: { @@ -40,7 +48,11 @@ var adUnits = [ }] }, { code: 'test-pb-sidebar', - size: [[160, 600]], + mediaTypes: { + banner: { + sizes: [[160, 600]], + } + }, bids: [{ bidder: 'sortable', params: { diff --git a/test/spec/modules/sortableBidAdapter_spec.js b/test/spec/modules/sortableBidAdapter_spec.js index 173a2ab7639..8d2b4eea87d 100644 --- a/test/spec/modules/sortableBidAdapter_spec.js +++ b/test/spec/modules/sortableBidAdapter_spec.js @@ -181,7 +181,7 @@ describe('sortableBidAdapter', function() { }); it('attaches source and version to endpoint URL as query params', function () { - const ENDPOINT = `//c.deployads.com/openrtb2/auction?src=$$REPO_AND_VERSION$$&host=example.com`; + const ENDPOINT = `https://c.deployads.com/openrtb2/auction?src=$$REPO_AND_VERSION$$&host=example.com`; expect(request.url).to.equal(ENDPOINT); }); From e4d31fd53b528fdf47fcf1edbaadaa61931beca1 Mon Sep 17 00:00:00 2001 From: Shannon Broekhoven Date: Tue, 3 Mar 2020 14:14:26 -0500 Subject: [PATCH 7/8] Feedback: add native and video examples --- modules/sortableBidAdapter.md | 41 +++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/modules/sortableBidAdapter.md b/modules/sortableBidAdapter.md index 87ececfe5a1..c24ad85b752 100644 --- a/modules/sortableBidAdapter.md +++ b/modules/sortableBidAdapter.md @@ -63,6 +63,47 @@ var adUnits = [ } } }] + }, { + code: 'test-pb-native', + mediaTypes: { + native: { + title: { + required: true, + len: 800 + }, + image: { + required: true, + sizes: [790, 294], + }, + sponsoredBy: { + required: true + } + } + }, + bids: [{ + bidder: 'sortable', + params: { + tagId: 'test-pb-native', + siteId: 'prebid.example.com' + } + }] + }, { + code: 'test-pb-video', + mediaTypes: { + video: { + playerSize: [640,480], + context: 'instream' + } + }, + bids: [ + { + bidder: 'sortable', + params: { + tagId: 'test-pb-video', + siteId: 'prebid.example.com' + } + } + ] } ] ``` From fd10b73cfdff3981daa6b0cdd68b1b6cb004da08 Mon Sep 17 00:00:00 2001 From: Karent Narvaez Date: Fri, 9 Apr 2021 18:09:45 -0400 Subject: [PATCH 8/8] Update unit tests --- modules/sortableBidAdapter.js | 3 +++ test/spec/modules/sortableBidAdapter_spec.js | 12 ++++++------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/modules/sortableBidAdapter.js b/modules/sortableBidAdapter.js index 5b7865f126a..6989abff143 100644 --- a/modules/sortableBidAdapter.js +++ b/modules/sortableBidAdapter.js @@ -244,6 +244,9 @@ export const spec = { h: screen.height }, }, + user: { + ext: {} + } }; if (bidderRequest && bidderRequest.timeout > 0) { sortableBidReq.tmax = bidderRequest.timeout; diff --git a/test/spec/modules/sortableBidAdapter_spec.js b/test/spec/modules/sortableBidAdapter_spec.js index f8c83f17f83..97f80e8dfeb 100644 --- a/test/spec/modules/sortableBidAdapter_spec.js +++ b/test/spec/modules/sortableBidAdapter_spec.js @@ -329,7 +329,7 @@ describe('sortableBidAdapter', function() { const gdprRequest = spec.buildRequests(gdprBidRequests, {refererInfo: { referer: 'http://localhost:9876/' }}); const gdprRequestBody = JSON.parse(gdprRequest.data); expect(gdprRequestBody.regs).to.deep.equal({ext: {}}); - expect(gdprRequestBody.user).to.equal(undefined); + expect(gdprRequestBody.user.ext.consent).to.equal(undefined); }) const eidsBidRequests = [{ @@ -346,23 +346,23 @@ describe('sortableBidAdapter', function() { 'auctionId': '1d1a030790a475' }]; - it('should set user ids when present', function() { - eidsBidRequests[0].userId = { criteoId: 'sample-userid' }; + it('should not set user ids when none present', function() { const eidsRequest = spec.buildRequests(eidsBidRequests, {refererInfo: { referer: 'http://localhost:9876/' }}); const eidsRequestBody = JSON.parse(eidsRequest.data); - expect(eidsRequestBody.user.ext.eids.length).to.equal(1); + expect(eidsRequestBody.user.ext.eids).to.equal(undefined); }) - it('should not set user ids when none present', function() { + it('should set user ids when present', function() { + eidsBidRequests[0].userId = { criteoId: 'sample-userid' }; const eidsRequest = spec.buildRequests(eidsBidRequests, {refererInfo: { referer: 'http://localhost:9876/' }}); const eidsRequestBody = JSON.parse(eidsRequest.data); - expect(eidsRequestBody.user.ext.eids).to.equal(undefined); + expect(eidsRequestBody.user.ext.eids.length).to.equal(1); }) });