diff --git a/modules/ccxBidAdapter.js b/modules/ccxBidAdapter.js new file mode 100644 index 00000000000..90eb7e7ed9f --- /dev/null +++ b/modules/ccxBidAdapter.js @@ -0,0 +1,232 @@ +import * as utils from '../src/utils' +import { registerBidder } from '../src/adapters/bidderFactory' +import { config } from '../src/config' +const BIDDER_CODE = 'ccx' +const BID_URL = 'https://delivery.clickonometrics.pl/ortb/prebid/bid' +const SUPPORTED_VIDEO_PROTOCOLS = [2, 3, 5, 6] +const SUPPORTED_VIDEO_MIMES = ['video/mp4', 'video/x-flv'] +const SUPPORTED_VIDEO_PLAYBACK_METHODS = [1, 2, 3, 4] + +function _getDeviceObj () { + let device = {} + device.w = screen.width + device.y = screen.height + device.ua = navigator.userAgent + return device +} + +function _getSiteObj (bidderRequest) { + let site = {} + let url = config.getConfig('pageUrl') || utils.deepAccess(window, 'location.href'); + if (url.length > 0) { + url = url.split('?')[0] + } + site.page = url + + return site +} + +function _validateSizes (sizeObj, type) { + if (!utils.isArray(sizeObj) || typeof sizeObj[0] === 'undefined') { + return false + } + + if (type === 'video' && (!utils.isArray(sizeObj[0]) || sizeObj[0].length !== 2)) { + return false + } + + let result = true + + if (type === 'banner') { + utils._each(sizeObj, function (size) { + if (!utils.isArray(size) || (size.length !== 2)) { + result = false + } + }) + return result + } + + if (type === 'old') { + if (!utils.isArray(sizeObj[0]) && sizeObj.length !== 2) { + result = false + } else if (utils.isArray(sizeObj[0])) { + utils._each(sizeObj, function (size) { + if (!utils.isArray(size) || (size.length !== 2)) { + result = false + } + }) + } + return result; + } + + return true +} + +function _buildBid (bid) { + let placement = {} + placement.id = bid.bidId + placement.secure = 1 + + let sizes = utils.deepAccess(bid, 'mediaTypes.banner.sizes') || utils.deepAccess(bid, 'mediaTypes.video.playerSize') || utils.deepAccess(bid, 'sizes') + + if (utils.deepAccess(bid, 'mediaTypes.banner') || utils.deepAccess(bid, 'mediaType') === 'banner' || (!utils.deepAccess(bid, 'mediaTypes.video') && !utils.deepAccess(bid, 'mediaType'))) { + placement.banner = {'format': []} + if (utils.isArray(sizes[0])) { + utils._each(sizes, function (size) { + placement.banner.format.push({'w': size[0], 'h': size[1]}) + }) + } else { + placement.banner.format.push({'w': sizes[0], 'h': sizes[1]}) + } + } else if (utils.deepAccess(bid, 'mediaTypes.video') || utils.deepAccess(bid, 'mediaType') === 'video') { + placement.video = {} + + if (typeof sizes !== 'undefined') { + if (utils.isArray(sizes[0])) { + placement.video.w = sizes[0][0] + placement.video.h = sizes[0][1] + } else { + placement.video.w = sizes[0] + placement.video.h = sizes[1] + } + } + + placement.video.protocols = utils.deepAccess(bid, 'params.video.protocols') || SUPPORTED_VIDEO_PROTOCOLS + placement.video.mimes = utils.deepAccess(bid, 'params.video.mimes') || SUPPORTED_VIDEO_MIMES + placement.video.playbackmethod = utils.deepAccess(bid, 'params.video.playbackmethod') || SUPPORTED_VIDEO_PLAYBACK_METHODS + placement.video.skip = utils.deepAccess(bid, 'params.video.skip') || 0 + if (placement.video.skip === 1 && utils.deepAccess(bid, 'params.video.skipafter')) { + placement.video.skipafter = utils.deepAccess(bid, 'params.video.skipafter') + } + } + + placement.ext = {'pid': bid.params.placementId} + + return placement +} + +function _buildResponse (bid, currency, ttl) { + let resp = { + requestId: bid.impid, + cpm: bid.price, + width: bid.w, + height: bid.h, + creativeId: bid.crid, + netRevenue: false, + ttl: ttl, + currency: currency + } + + if (bid.ext.type === 'video') { + resp.vastXml = bid.adm + } else { + resp.ad = bid.adm + } + + if (utils.deepAccess(bid, 'dealid')) { + resp.dealId = bid.dealid + } + + return resp +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: ['banner', 'video'], + + isBidRequestValid: function (bid) { + if (!utils.deepAccess(bid, 'params.placementId')) { + utils.logWarn('placementId param is reqeuired.') + return false + } + if (utils.deepAccess(bid, 'mediaTypes.banner.sizes')) { + let isValid = _validateSizes(bid.mediaTypes.banner.sizes, 'banner') + if (!isValid) { + utils.logWarn('Bid sizes are invalid.') + } + return isValid + } else if (utils.deepAccess(bid, 'mediaTypes.video.playerSize')) { + let isValid = _validateSizes(bid.mediaTypes.video.playerSize, 'video') + if (!isValid) { + utils.logWarn('Bid sizes are invalid.') + } + return isValid + } else if (utils.deepAccess(bid, 'sizes')) { + let isValid = _validateSizes(bid.sizes, 'old') + if (!isValid) { + utils.logWarn('Bid sizes are invalid.') + } + return isValid + } else { + utils.logWarn('Bid sizes are required.') + return false + } + }, + buildRequests: function (validBidRequests, bidderRequest) { + // check if validBidRequests is not empty + if (validBidRequests.length > 0) { + let requestBody = {} + requestBody.imp = [] + requestBody.site = _getSiteObj(bidderRequest) + requestBody.device = _getDeviceObj() + requestBody.id = bidderRequest.bids[0].auctionId + requestBody.ext = {'ce': (utils.cookiesAreEnabled() ? 1 : 0)} + + // Attaching GDPR Consent Params + if (bidderRequest && bidderRequest.gdprConsent) { + requestBody.user = { + ext: { + consent: bidderRequest.gdprConsent.consentString + } + }; + + requestBody.regs = { + ext: { + gdpr: (bidderRequest.gdprConsent.gdprApplies ? 1 : 0) + } + }; + } + + utils._each(validBidRequests, function (bid) { + requestBody.imp.push(_buildBid(bid)) + }) + // Return the server request + return { + 'method': 'POST', + 'url': BID_URL, + 'data': JSON.stringify(requestBody) + } + } + }, + interpretResponse: function (serverResponse, request) { + const bidResponses = [] + + // response is not empty (HTTP 204) + if (!utils.isEmpty(serverResponse.body)) { + utils._each(serverResponse.body.seatbid, function (seatbid) { + utils._each(seatbid.bid, function (bid) { + bidResponses.push(_buildResponse(bid, serverResponse.body.cur, serverResponse.body.ext.ttl)) + }) + }) + } + + return bidResponses + }, + getUserSyncs: function (syncOptions, serverResponses) { + const syncs = [] + + if (utils.deepAccess(serverResponses[0], 'body.ext.usersync') && !utils.isEmpty(serverResponses[0].body.ext.usersync)) { + utils._each(serverResponses[0].body.ext.usersync, function (match) { + if ((syncOptions.iframeEnabled && match.type === 'iframe') || (syncOptions.pixelEnabled && match.type === 'image')) { + syncs.push({ + type: match.type, + url: match.url + }) + } + }) + } + + return syncs + } +} +registerBidder(spec) diff --git a/test/spec/modules/ccxBidAdapter_spec.js b/test/spec/modules/ccxBidAdapter_spec.js new file mode 100644 index 00000000000..01e52c3559a --- /dev/null +++ b/test/spec/modules/ccxBidAdapter_spec.js @@ -0,0 +1,428 @@ +import { expect } from 'chai'; +import { spec } from 'modules/ccxBidAdapter'; +import * as utils from 'src/utils'; + +describe('ccxAdapter', function () { + let bids = [ + { + adUnitCode: 'banner', + auctionId: '0b9de793-8eda-481e-a548-c187d58b28d9', + bidId: '2e56e1af51a5d7', + bidder: 'ccx', + bidderRequestId: '17e7b9f58a607e', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + params: { + placementId: 607 + }, + sizes: [[300, 250]], + transactionId: 'aefddd38-cfa0-48ab-8bdd-325de4bab5f9' + }, + { + adUnitCode: 'video', + auctionId: '0b9de793-8eda-481e-a548-c187d58b28d9', + bidId: '3u94t90ut39tt3t', + bidder: 'ccx', + bidderRequestId: '23ur20r239r2r', + mediaTypes: { + video: { + playerSize: [[640, 480]] + } + }, + params: { + placementId: 608 + }, + sizes: [[640, 480]], + transactionId: 'aefddd38-cfa0-48ab-8bdd-325de4bab5f9' + } + ]; + describe('isBidRequestValid', function () { + it('Valid bid requests', function () { + expect(spec.isBidRequestValid(bids[0])).to.be.true; + expect(spec.isBidRequestValid(bids[1])).to.be.true; + }); + it('Invalid bid reqeusts - no placementId', function () { + let bidsClone = utils.deepClone(bids); + bidsClone[0].params = undefined; + expect(spec.isBidRequestValid(bidsClone[0])).to.be.false; + }); + it('Invalid bid reqeusts - invalid banner sizes', function () { + let bidsClone = utils.deepClone(bids); + bidsClone[0].mediaTypes.banner.sizes = [300, 250]; + expect(spec.isBidRequestValid(bidsClone[0])).to.be.false; + bidsClone[0].mediaTypes.banner.sizes = [[300, 250], [750]]; + expect(spec.isBidRequestValid(bidsClone[0])).to.be.false; + bidsClone[0].mediaTypes.banner.sizes = []; + expect(spec.isBidRequestValid(bidsClone[0])).to.be.false; + }); + it('Invalid bid reqeusts - invalid video sizes', function () { + let bidsClone = utils.deepClone(bids); + bidsClone[1].mediaTypes.video.playerSize = []; + expect(spec.isBidRequestValid(bidsClone[1])).to.be.false; + bidsClone[1].mediaTypes.video.sizes = [640, 480]; + expect(spec.isBidRequestValid(bidsClone[1])).to.be.false; + }); + it('Valid bid reqeust - old style sizes', function () { + let bidsClone = utils.deepClone(bids); + delete (bidsClone[0].mediaTypes); + delete (bidsClone[1].mediaTypes); + expect(spec.isBidRequestValid(bidsClone[0])).to.be.true; + expect(spec.isBidRequestValid(bidsClone[1])).to.be.true; + bidsClone[0].sizes = [300, 250]; + expect(spec.isBidRequestValid(bidsClone[0])).to.be.true; + }); + }); + describe('buildRequests', function () { + it('No valid bids', function () { + expect(spec.buildRequests([])).to.be.undefined; + }); + + it('Valid bid request - default', function () { + let response = spec.buildRequests(bids, {bids}); + expect(response).to.be.not.empty; + expect(response.data).to.be.not.empty; + + let data = JSON.parse(response.data); + + expect(data).to.be.an('object'); + expect(data).to.have.keys('site', 'imp', 'id', 'ext', 'device'); + + let imps = [ + { + banner: { + format: [ + { + w: 300, + h: 250 + } + ] + }, + ext: { + pid: 607 + }, + id: '2e56e1af51a5d7', + secure: 1 + }, + { + video: { + w: 640, + h: 480, + protocols: [2, 3, 5, 6], + mimes: ['video/mp4', 'video/x-flv'], + playbackmethod: [1, 2, 3, 4], + skip: 0 + }, + id: '3u94t90ut39tt3t', + secure: 1, + ext: { + pid: 608 + } + } + ]; + expect(data.imp).to.deep.have.same.members(imps); + }); + + it('Valid bid request - custom', function () { + let bidsClone = utils.deepClone(bids); + let imps = [ + { + banner: { + format: [ + { + w: 300, + h: 250 + } + ] + }, + ext: { + pid: 607 + }, + id: '2e56e1af51a5d7', + secure: 1 + }, + { + video: { + w: 640, + h: 480, + protocols: [5, 6], + mimes: ['video/mp4'], + playbackmethod: [3], + skip: 1, + skipafter: 5 + }, + id: '3u94t90ut39tt3t', + secure: 1, + ext: { + pid: 608 + } + } + ]; + + bidsClone[1].params.video = {}; + bidsClone[1].params.video.protocols = [5, 6]; + bidsClone[1].params.video.mimes = ['video/mp4']; + bidsClone[1].params.video.playbackmethod = [3]; + bidsClone[1].params.video.skip = 1; + bidsClone[1].params.video.skipafter = 5; + + let response = spec.buildRequests(bidsClone, {'bids': bidsClone}); + let data = JSON.parse(response.data); + + expect(data.imp).to.deep.have.same.members(imps); + }); + it('Valid bid request - sizes old style', function () { + let bidsClone = utils.deepClone(bids); + delete (bidsClone[0].mediaTypes); + delete (bidsClone[1].mediaTypes); + bidsClone[0].mediaType = 'banner'; + bidsClone[1].mediaType = 'video'; + + let imps = [ + { + banner: { + format: [ + { + w: 300, + h: 250 + } + ] + }, + ext: { + pid: 607 + }, + id: '2e56e1af51a5d7', + secure: 1 + }, + { + video: { + w: 640, + h: 480, + protocols: [2, 3, 5, 6], + mimes: ['video/mp4', 'video/x-flv'], + playbackmethod: [1, 2, 3, 4], + skip: 0 + }, + id: '3u94t90ut39tt3t', + secure: 1, + ext: { + pid: 608 + } + } + ]; + + let response = spec.buildRequests(bidsClone, {'bids': bidsClone}); + let data = JSON.parse(response.data); + + expect(data.imp).to.deep.have.same.members(imps); + }); + it('Valid bid request - sizes old style - no media type', function () { + let bidsClone = utils.deepClone(bids); + delete (bidsClone[0].mediaTypes); + delete (bidsClone[1]); + + let imps = [ + { + banner: { + format: [ + { + w: 300, + h: 250 + } + ] + }, + ext: { + pid: 607 + }, + id: '2e56e1af51a5d7', + secure: 1 + } + ]; + + let response = spec.buildRequests(bidsClone, {'bids': bidsClone}); + let data = JSON.parse(response.data); + + expect(data.imp).to.deep.have.same.members(imps); + }); + }); + + describe('GDPR conformity', function () { + it('should transmit correct data', function () { + let bidsClone = utils.deepClone(bids); + let gdprConsent = { + consentString: 'awefasdfwefasdfasd', + gdprApplies: true + }; + let response = spec.buildRequests(bidsClone, {'bids': bidsClone, 'gdprConsent': gdprConsent}); + let data = JSON.parse(response.data); + + expect(data.regs.ext.gdpr).to.equal(1); + expect(data.user.ext.consent).to.equal('awefasdfwefasdfasd'); + }); + }); + + describe('GDPR absence conformity', function () { + it('should transmit correct data', function () { + let response = spec.buildRequests(bids, {bids}); + let data = JSON.parse(response.data); + + expect(data.regs).to.be.undefined; + expect(data.user).to.be.undefined; + }); + }); + + let response = { + id: '0b9de793-8eda-481e-a548-c187d58b28d9', + seatbid: [ + { + bid: [ + { + id: '2e56e1af51a5d7_221', + impid: '2e56e1af51a5d7', + price: 8.1, + adid: '221', + adm: '', + adomain: ['clickonometrics.com'], + crid: '221', + w: 300, + h: 250, + ext: { + type: 'standard' + } + }, + { + id: '2e56e1af51a5d8_222', + impid: '2e56e1af51a5d8', + price: 5.68, + adid: '222', + adm: '', + adomain: ['clickonometrics.com'], + crid: '222', + w: 640, + h: 480, + ext: { + type: 'video' + } + } + ] + } + ], + cur: 'PLN', + ext: { + ttl: 5, + usersync: [ + { + type: 'image', + url: 'http://foo.sync?param=1' + }, + { + type: 'iframe', + url: 'http://foo.sync?param=2' + } + ] + } + }; + + describe('interpretResponse', function () { + it('Valid bid response - multi', function () { + let bidResponses = [ + { + requestId: '2e56e1af51a5d7', + cpm: 8.1, + width: 300, + height: 250, + creativeId: '221', + netRevenue: false, + ttl: 5, + currency: 'PLN', + ad: '' + }, + { + requestId: '2e56e1af51a5d8', + cpm: 5.68, + width: 640, + height: 480, + creativeId: '222', + netRevenue: false, + ttl: 5, + currency: 'PLN', + vastXml: '' + } + ]; + expect(spec.interpretResponse({body: response})).to.deep.have.same.members(bidResponses); + }); + + it('Valid bid response - single', function () { + delete response.seatbid[0].bid[1]; + let bidResponses = [ + { + requestId: '2e56e1af51a5d7', + cpm: 8.1, + width: 300, + height: 250, + creativeId: '221', + netRevenue: false, + ttl: 5, + currency: 'PLN', + ad: '' + } + ]; + expect(spec.interpretResponse({body: response})).to.deep.have.same.members(bidResponses); + }); + + it('Empty bid response', function () { + expect(spec.interpretResponse({})).to.be.empty; + }); + }); + describe('getUserSyncs', function () { + it('Valid syncs - all', function () { + let syncOptions = { + iframeEnabled: true, + pixelEnabled: true + }; + + let expectedSyncs = [ + { + type: 'image', + url: 'http://foo.sync?param=1' + }, + { + type: 'iframe', + url: 'http://foo.sync?param=2' + } + ]; + expect(spec.getUserSyncs(syncOptions, [{body: response}])).to.deep.have.same.members(expectedSyncs); + }); + + it('Valid syncs - only image', function () { + let syncOptions = { + iframeEnabled: false, + pixelEnabled: true + }; + let expectedSyncs = [ + { + type: 'image', url: 'http://foo.sync?param=1' + } + ]; + expect(spec.getUserSyncs(syncOptions, [{body: response}])).to.deep.have.same.members(expectedSyncs); + }); + + it('Valid syncs - only iframe', function () { + let syncOptions = {iframeEnabled: true, pixelEnabled: false}; + let expectedSyncs = [ + { + type: 'iframe', url: 'http://foo.sync?param=2' + } + ]; + expect(spec.getUserSyncs(syncOptions, [{body: response}])).to.deep.have.same.members(expectedSyncs); + }); + + it('Valid syncs - empty', function () { + let syncOptions = {iframeEnabled: true, pixelEnabled: true}; + response.ext.usersync = {}; + expect(spec.getUserSyncs(syncOptions, [{body: response}])).to.be.empty; + }); + }); +});