diff --git a/modules/smartadserverBidAdapter.js b/modules/smartadserverBidAdapter.js new file mode 100644 index 00000000000..1e14195d38a --- /dev/null +++ b/modules/smartadserverBidAdapter.js @@ -0,0 +1,153 @@ +import * as utils from '../src/utils'; +import { + BANNER, + VIDEO +} from '../src/mediaTypes'; +import { + config +} from '../src/config'; +import { + registerBidder +} from '../src/adapters/bidderFactory'; +const BIDDER_CODE = 'smartadserver'; +export const spec = { + code: BIDDER_CODE, + aliases: ['smart'], // short code + supportedMediaTypes: [BANNER, VIDEO], + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (bid) { + return !!(bid.params && bid.params.siteId && bid.params.pageId && bid.params.formatId); + }, + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @param {bidderRequest} - bidder request object + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function (validBidRequests, bidderRequest) { + // use bidderRequest.bids[] to get bidder-dependent request info + + // if your bidder supports multiple currencies, use config.getConfig(currency) + // to find which one the ad server needs + + // pull requested transaction ID from bidderRequest.bids[].transactionId + return validBidRequests.map(bid => { + // Common bid request attributes for banner, outstream and instream. + var payload = { + siteid: bid.params.siteId, + pageid: bid.params.pageId, + formatid: bid.params.formatId, + currencyCode: config.getConfig('currency.adServerCurrency'), + bidfloor: bid.params.bidfloor || 0.0, + targeting: bid.params.target && bid.params.target != '' ? bid.params.target : undefined, + buid: bid.params.buId && bid.params.buId != '' ? bid.params.buId : undefined, + appname: bid.params.appName && bid.params.appName != '' ? bid.params.appName : undefined, + ckid: bid.params.ckId || 0, + tagId: bid.adUnitCode, + pageDomain: bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.referer ? bidderRequest.refererInfo.referer : undefined, + transactionId: bid.transactionId, + timeout: config.getConfig('bidderTimeout'), + bidId: bid.bidId, + prebidVersion: '$prebid.version$' + }; + + const videoMediaType = utils.deepAccess(bid, 'mediaTypes.video'); + if (!videoMediaType) { + const bannerMediaType = utils.deepAccess(bid, 'mediaTypes.banner'); + payload.sizes = bannerMediaType.sizes.map(size => ({ + w: size[0], + h: size[1] + })); + } else if (videoMediaType && videoMediaType.context === 'instream') { + // Specific attributes for instream. + var playerSize = videoMediaType.playerSize[0]; + payload.isVideo = true; + payload.videoData = { + videoProtocol: bid.params.video.protocol, + playerWidth: playerSize[0], + playerHeight: playerSize[1], + adBreak: bid.params.video.startDelay || 0 + }; + } else { + return {}; + } + + if (bidderRequest && bidderRequest.gdprConsent) { + payload.gdpr_consent = bidderRequest.gdprConsent.consentString; + payload.gdpr = bidderRequest.gdprConsent.gdprApplies; // we're handling the undefined case server side + } + + var payloadString = JSON.stringify(payload); + return { + method: 'POST', + url: (bid.params.domain !== undefined ? bid.params.domain : 'https://prg.smartadserver.com') + '/prebid/v1', + data: payloadString, + }; + }); + }, + /** + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function (serverResponse, bidRequestString) { + const bidResponses = []; + var response = serverResponse.body; + try { + if (response) { + const bidRequest = JSON.parse(bidRequestString.data); + + var bidResponse = { + requestId: bidRequest.bidId, + cpm: response.cpm, + width: response.width, + height: response.height, + creativeId: response.creativeId, + dealId: response.dealId, + currency: response.currency, + netRevenue: response.isNetCpm, + ttl: response.ttl + }; + + if (bidRequest.isVideo) { + bidResponse.mediaType = VIDEO; + bidResponse.vastUrl = response.adUrl; + bidResponse.vastXml = response.ad; + } else { + bidResponse.adUrl = response.adUrl; + bidResponse.ad = response.ad; + } + + bidResponses.push(bidResponse); + } + } catch (error) { + utils.logError('Error while parsing smart server response', error); + } + return bidResponses; + }, + /** + * User syncs. + * + * @param {*} syncOptions Publisher prebid configuration. + * @param {*} serverResponses A successful response from the server. + * @return {Syncs[]} An array of syncs that should be executed. + */ + getUserSyncs: function (syncOptions, serverResponses) { + const syncs = [] + if (syncOptions.iframeEnabled && serverResponses.length > 0) { + syncs.push({ + type: 'iframe', + url: serverResponses[0].body.cSyncUrl + }); + } + return syncs; + } +} +registerBidder(spec); diff --git a/modules/smartadserverBidAdapter.md b/modules/smartadserverBidAdapter.md index c1fc0d819c5..84fe5ff8afc 100644 --- a/modules/smartadserverBidAdapter.md +++ b/modules/smartadserverBidAdapter.md @@ -20,12 +20,16 @@ Please reach out to your Technical account manager for more information. var adUnits = [ { code: 'test-div', - sizes: [[300, 250]], // a display size + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, bids: [ { bidder: "smart", params: { - domain: 'http://ww251.smartadserver.com', + domain: 'https://ww251.smartadserver.com', siteId: 207435, pageId: 896536, formatId: 62913, @@ -42,12 +46,16 @@ Please reach out to your Technical account manager for more information. var adUnits = [ { code: 'test-div', - sizes: [[300, 250]], // a display size + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, bids: [ { bidder: "smart", params: { - domain: 'http://ww251.smartadserver.com', + domain: 'https://ww251.smartadserver.com', siteId: 207435, pageId: 896536, formatId: 65906, @@ -74,7 +82,7 @@ Please reach out to your Technical account manager for more information. bids: [{ bidder: "smart", params: { - domain: 'http://ww251.smartadserver.com', + domain: 'https://ww251.smartadserver.com', siteId: 326147, pageId: 1153895, formatId: 55710 diff --git a/test/spec/modules/smartadserverBidAdapter_spec.js b/test/spec/modules/smartadserverBidAdapter_spec.js new file mode 100644 index 00000000000..caa806f9ccc --- /dev/null +++ b/test/spec/modules/smartadserverBidAdapter_spec.js @@ -0,0 +1,407 @@ +import { + expect +} from 'chai'; +import { + spec +} from 'modules/smartadserverBidAdapter'; +import { + newBidder +} from 'src/adapters/bidderFactory'; +import { + config +} from 'src/config'; +import * as utils from 'src/utils'; +import { requestBidsHook } from 'modules/consentManagement'; + +// Default params with optional ones +describe('Smart bid adapter tests', function () { + var DEFAULT_PARAMS = [{ + adUnitCode: 'sas_42', + bidId: 'abcd1234', + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 200] + ] + } + }, + bidder: 'smartadserver', + params: { + domain: 'https://prg.smartadserver.com', + siteId: '1234', + pageId: '5678', + formatId: '90', + target: 'test=prebid', + bidfloor: 0.420, + buId: '7569', + appName: 'Mozilla', + ckId: 42 + }, + requestId: 'efgh5678', + transactionId: 'zsfgzzg' + }]; + + // Default params without optional ones + var DEFAULT_PARAMS_WO_OPTIONAL = [{ + adUnitCode: 'sas_42', + bidId: 'abcd1234', + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 200] + ], + } + }, + + bidder: 'smartadserver', + params: { + domain: 'https://prg.smartadserver.com', + siteId: '1234', + pageId: '5678', + formatId: '90' + }, + requestId: 'efgh5678' + }]; + + var BID_RESPONSE = { + body: { + cpm: 12, + width: 300, + height: 250, + creativeId: 'zioeufg', + currency: 'GBP', + isNetCpm: true, + ttl: 300, + adUrl: 'http://awesome.fake.url', + ad: '< --- awesome script --- >', + cSyncUrl: 'http://awesome.fake.csync.url' + } + }; + + it('Verify build request', function () { + config.setConfig({ + 'currency': { + 'adServerCurrency': 'EUR' + } + }); + const request = spec.buildRequests(DEFAULT_PARAMS); + expect(request[0]).to.have.property('url').and.to.equal('https://prg.smartadserver.com/prebid/v1'); + expect(request[0]).to.have.property('method').and.to.equal('POST'); + const requestContent = JSON.parse(request[0].data); + + expect(requestContent).to.have.property('siteid').and.to.equal('1234'); + expect(requestContent).to.have.property('pageid').and.to.equal('5678'); + expect(requestContent).to.have.property('formatid').and.to.equal('90'); + expect(requestContent).to.have.property('currencyCode').and.to.equal('EUR'); + expect(requestContent).to.have.property('bidfloor').and.to.equal(0.42); + expect(requestContent).to.have.property('targeting').and.to.equal('test=prebid'); + expect(requestContent).to.have.property('tagId').and.to.equal('sas_42'); + expect(requestContent).to.have.property('sizes'); + expect(requestContent.sizes[0]).to.have.property('w').and.to.equal(300); + expect(requestContent.sizes[0]).to.have.property('h').and.to.equal(250); + expect(requestContent.sizes[1]).to.have.property('w').and.to.equal(300); + expect(requestContent.sizes[1]).to.have.property('h').and.to.equal(200); + expect(requestContent).to.not.have.property('pageDomain'); + expect(requestContent).to.have.property('transactionId').and.to.not.equal(null).and.to.not.be.undefined; + expect(requestContent).to.have.property('buid').and.to.equal('7569'); + expect(requestContent).to.have.property('appname').and.to.equal('Mozilla'); + expect(requestContent).to.have.property('ckid').and.to.equal(42); + }); + + it('Verify parse response', function () { + const request = spec.buildRequests(DEFAULT_PARAMS); + const bids = spec.interpretResponse(BID_RESPONSE, request[0]); + expect(bids).to.have.lengthOf(1); + const bid = bids[0]; + expect(bid.cpm).to.equal(12); + expect(bid.adUrl).to.equal('http://awesome.fake.url'); + expect(bid.ad).to.equal('< --- awesome script --- >'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.creativeId).to.equal('zioeufg'); + expect(bid.currency).to.equal('GBP'); + expect(bid.netRevenue).to.equal(true); + expect(bid.ttl).to.equal(300); + expect(bid.requestId).to.equal(DEFAULT_PARAMS[0].bidId); + + expect(function () { + spec.interpretResponse(BID_RESPONSE, { + data: 'invalid Json' + }) + }).to.not.throw(); + }); + + it('Verifies bidder code', function () { + expect(spec.code).to.equal('smartadserver'); + }); + + it('Verifies bidder aliases', function () { + expect(spec.aliases).to.have.lengthOf(1); + expect(spec.aliases[0]).to.equal('smart'); + }); + + it('Verifies if bid request valid', function () { + expect(spec.isBidRequestValid(DEFAULT_PARAMS[0])).to.equal(true); + expect(spec.isBidRequestValid(DEFAULT_PARAMS_WO_OPTIONAL[0])).to.equal(true); + expect(spec.isBidRequestValid({})).to.equal(false); + expect(spec.isBidRequestValid({ + params: {} + })).to.equal(false); + expect(spec.isBidRequestValid({ + params: { + pageId: 123 + } + })).to.equal(false); + expect(spec.isBidRequestValid({ + params: { + siteId: 123 + } + })).to.equal(false); + expect(spec.isBidRequestValid({ + params: { + formatId: 123, + pageId: 234 + } + })).to.equal(false); + expect(spec.isBidRequestValid({ + params: { + domain: 'www.test.com', + pageId: 234 + } + })).to.equal(false); + expect(spec.isBidRequestValid({ + params: { + domain: 'www.test.com', + formatId: 123, + siteId: 456, + pageId: 234 + } + })).to.equal(true); + expect(spec.isBidRequestValid({ + params: { + domain: 'www.test.com', + formatId: 123, + siteId: 456, + pageId: 234, + buId: 789, + appName: 'Mozilla' + } + })).to.equal(true); + expect(spec.isBidRequestValid({ + params: { + domain: 'www.test.com', + formatId: 123, + pageId: 234, + buId: 789, + appName: 'Mozilla' + } + })).to.equal(false); + }); + + it('Verifies user sync', function () { + var syncs = spec.getUserSyncs({ + iframeEnabled: true + }, [BID_RESPONSE]); + expect(syncs).to.have.lengthOf(1); + expect(syncs[0].type).to.equal('iframe'); + expect(syncs[0].url).to.equal('http://awesome.fake.csync.url'); + + syncs = spec.getUserSyncs({ + iframeEnabled: false + }, [BID_RESPONSE]); + expect(syncs).to.have.lengthOf(0); + + syncs = spec.getUserSyncs({ + iframeEnabled: true + }, []); + expect(syncs).to.have.lengthOf(0); + }); + + describe('gdpr tests', function () { + afterEach(function () { + config.resetConfig(); + $$PREBID_GLOBAL$$.requestBids.removeAll(); + }); + + it('Verify build request with GDPR', function () { + config.setConfig({ + 'currency': { + 'adServerCurrency': 'EUR' + }, + consentManagement: { + cmp: 'iab', + consentRequired: true, + timeout: 1000, + allowAuctionWithoutConsent: true + } + }); + const request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, { + gdprConsent: { + consentString: 'BOKAVy4OKAVy4ABAB8AAAAAZ+A==', + gdprApplies: true + } + }); + const requestContent = JSON.parse(request[0].data); + expect(requestContent).to.have.property('gdpr').and.to.equal(true); + expect(requestContent).to.have.property('gdpr_consent').and.to.equal('BOKAVy4OKAVy4ABAB8AAAAAZ+A=='); + }); + + it('Verify build request with GDPR without gdprApplies', function () { + config.setConfig({ + 'currency': { + 'adServerCurrency': 'EUR' + }, + consentManagement: { + cmp: 'iab', + consentRequired: true, + timeout: 1000, + allowAuctionWithoutConsent: true + } + }); + const request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, { + gdprConsent: { + consentString: 'BOKAVy4OKAVy4ABAB8AAAAAZ+A==' + } + }); + const requestContent = JSON.parse(request[0].data); + expect(requestContent).to.not.have.property('gdpr'); + expect(requestContent).to.have.property('gdpr_consent').and.to.equal('BOKAVy4OKAVy4ABAB8AAAAAZ+A=='); + }); + }); + + describe('Instream video tests', function () { + afterEach(function () { + config.resetConfig(); + $$PREBID_GLOBAL$$.requestBids.removeAll(); + }); + + const INSTREAM_DEFAULT_PARAMS = [{ + adUnitCode: 'sas_42', + bidId: 'abcd1234', + bidder: 'smartadserver', + mediaTypes: { + video: { + context: 'instream', + playerSize: [[640, 480]] // It seems prebid.js transforms the player size array into an array of array... + } + }, + params: { + siteId: '1234', + pageId: '5678', + formatId: '90', + target: 'test=prebid', + bidfloor: 0.420, + buId: '7569', + appName: 'Mozilla', + ckId: 42, + video: { + protocol: 6 + } + }, + requestId: 'efgh5678', + transactionId: 'zsfgzzg' + }]; + + var INSTREAM_BID_RESPONSE = { + body: { + cpm: 12, + width: 640, + height: 480, + creativeId: 'zioeufg', + currency: 'GBP', + isNetCpm: true, + ttl: 300, + adUrl: 'http://awesome.fake-vast.url', + ad: '', + cSyncUrl: 'http://awesome.fake.csync.url' + } + }; + + it('Verify instream video build request', function () { + config.setConfig({ + 'currency': { + 'adServerCurrency': 'EUR' + } + }); + const request = spec.buildRequests(INSTREAM_DEFAULT_PARAMS); + expect(request[0]).to.have.property('url').and.to.equal('https://prg.smartadserver.com/prebid/v1'); + expect(request[0]).to.have.property('method').and.to.equal('POST'); + const requestContent = JSON.parse(request[0].data); + expect(requestContent).to.have.property('siteid').and.to.equal('1234'); + expect(requestContent).to.have.property('pageid').and.to.equal('5678'); + expect(requestContent).to.have.property('formatid').and.to.equal('90'); + expect(requestContent).to.have.property('currencyCode').and.to.equal('EUR'); + expect(requestContent).to.have.property('bidfloor').and.to.equal(0.42); + expect(requestContent).to.have.property('targeting').and.to.equal('test=prebid'); + expect(requestContent).to.have.property('tagId').and.to.equal('sas_42'); + expect(requestContent).to.not.have.property('pageDomain'); + expect(requestContent).to.have.property('transactionId').and.to.not.equal(null).and.to.not.be.undefined; + expect(requestContent).to.have.property('buid').and.to.equal('7569'); + expect(requestContent).to.have.property('appname').and.to.equal('Mozilla'); + expect(requestContent).to.have.property('ckid').and.to.equal(42); + expect(requestContent).to.have.property('isVideo').and.to.equal(true); + expect(requestContent).to.have.property('videoData'); + expect(requestContent.videoData).to.have.property('videoProtocol').and.to.equal(6); + expect(requestContent.videoData).to.have.property('playerWidth').and.to.equal(640); + expect(requestContent.videoData).to.have.property('playerHeight').and.to.equal(480); + }); + + it('Verify instream parse response', function () { + const request = spec.buildRequests(INSTREAM_DEFAULT_PARAMS); + const bids = spec.interpretResponse(INSTREAM_BID_RESPONSE, request[0]); + expect(bids).to.have.lengthOf(1); + const bid = bids[0]; + expect(bid.cpm).to.equal(12); + expect(bid.mediaType).to.equal('video'); + expect(bid.vastUrl).to.equal('http://awesome.fake-vast.url'); + expect(bid.vastXml).to.equal(''); + expect(bid.width).to.equal(640); + expect(bid.height).to.equal(480); + expect(bid.creativeId).to.equal('zioeufg'); + expect(bid.currency).to.equal('GBP'); + expect(bid.netRevenue).to.equal(true); + expect(bid.ttl).to.equal(300); + expect(bid.requestId).to.equal(INSTREAM_DEFAULT_PARAMS[0].bidId); + + expect(function () { + spec.interpretResponse(INSTREAM_BID_RESPONSE, { + data: 'invalid Json' + }) + }).to.not.throw(); + }); + + it('Verify not handled media type return empty request', function () { + config.setConfig({ + 'currency': { + 'adServerCurrency': 'EUR' + } + }); + const request = spec.buildRequests([{ + adUnitCode: 'sas_42', + bidder: 'smartadserver', + mediaTypes: { + video: { + context: 'badcontext' + } + }, + params: { + domain: 'https://prg.smartadserver.com', + siteId: '1234', + pageId: '5678', + formatId: '90', + target: 'test=prebid', + bidfloor: 0.420, + buId: '7569', + appName: 'Mozilla', + ckId: 42 + }, + requestId: 'efgh5678', + transactionId: 'zsfgzzg' + }, INSTREAM_DEFAULT_PARAMS[0]]); + expect(request[0]).to.be.empty; + expect(request[1]).to.not.be.empty; + }); + }); +});