diff --git a/modules/aduptechBidAdapter.js b/modules/aduptechBidAdapter.js new file mode 100644 index 00000000000..7d5e018508a --- /dev/null +++ b/modules/aduptechBidAdapter.js @@ -0,0 +1,189 @@ +import { registerBidder } from '../src/adapters/bidderFactory'; +import { BANNER } from '../src/mediaTypes' +import * as utils from '../src/utils'; + +export const BIDDER_CODE = 'aduptech'; +export const PUBLISHER_PLACEHOLDER = '{PUBLISHER}'; +export const ENDPOINT_URL = 'https://rtb.d.adup-tech.com/prebid/' + PUBLISHER_PLACEHOLDER + '_bid'; +export const ENDPOINT_METHOD = 'POST'; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + + /** + * Validate given bid request + * + * @param {*} bidRequest + * @returns {boolean} + */ + isBidRequestValid: (bidRequest) => { + if (!bidRequest) { + return false; + } + + const sizes = extractSizesFromBidRequest(bidRequest); + if (!sizes || sizes.length === 0) { + return false; + } + + const params = extractParamsFromBidRequest(bidRequest); + if (!params || !params.publisher || !params.placement) { + return false; + } + + return true; + }, + + /** + * Build real bid requests + * + * @param {*} validBidRequests + * @param {*} bidderRequest + * @returns {*[]} + */ + buildRequests: (validBidRequests, bidderRequest) => { + const bidRequests = []; + const gdpr = extractGdprFromBidderRequest(bidderRequest); + + validBidRequests.forEach((bidRequest) => { + bidRequests.push({ + url: ENDPOINT_URL.replace(PUBLISHER_PLACEHOLDER, encodeURIComponent(bidRequest.params.publisher)), + method: ENDPOINT_METHOD, + data: { + bidId: bidRequest.bidId, + auctionId: bidRequest.auctionId, + transactionId: bidRequest.transactionId, + adUnitCode: bidRequest.adUnitCode, + pageUrl: extractTopWindowUrlFromBidRequest(bidRequest), + referrer: extractTopWindowReferrerFromBidRequest(bidRequest), + sizes: extractSizesFromBidRequest(bidRequest), + params: extractParamsFromBidRequest(bidRequest), + gdpr: gdpr + } + }); + }); + + return bidRequests; + }, + + /** + * Handle bid response + * + * @param {*} response + * @returns {*[]} + */ + interpretResponse: (response) => { + const bidResponses = []; + + if (!response.body || !response.body.bid || !response.body.creative) { + return bidResponses; + } + + bidResponses.push({ + requestId: response.body.bid.bidId, + cpm: response.body.bid.price, + netRevenue: response.body.bid.net, + currency: response.body.bid.currency, + ttl: response.body.bid.ttl, + creativeId: response.body.creative.id, + width: response.body.creative.width, + height: response.body.creative.height, + ad: response.body.creative.html + }); + + return bidResponses; + } +}; + +/** + * Extracts the possible ad unit sizes from given bid request + * + * @param {*} bidRequest + * @returns {number[]} + */ +export function extractSizesFromBidRequest(bidRequest) { + // since pbjs 3.0 + if (bidRequest && utils.deepAccess(bidRequest, 'mediaTypes.banner.sizes')) { + return bidRequest.mediaTypes.banner.sizes; + + // for backward compatibility + } else if (bidRequest && bidRequest.sizes) { + return bidRequest.sizes; + + // fallback + } else { + return []; + } +} + +/** + * Extracts the custom params from given bid request + * + * @param {*} bidRequest + * @returns {*} + */ +export function extractParamsFromBidRequest(bidRequest) { + if (bidRequest && bidRequest.params) { + return bidRequest.params + } else { + return null; + } +} + +/** + * Extracts the GDPR information from given bidder request + * + * @param {*} bidderRequest + * @returns {*} + */ +export function extractGdprFromBidderRequest(bidderRequest) { + let gdpr = null; + + if (bidderRequest && bidderRequest.gdprConsent) { + gdpr = { + consentString: bidderRequest.gdprConsent.consentString, + consentRequired: (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') ? bidderRequest.gdprConsent.gdprApplies : true + }; + } + + return gdpr; +} + +/** + * Extracts the page url from given bid request or use the (top) window location as fallback + * + * @param {*} bidRequest + * @returns {string} + */ +export function extractTopWindowUrlFromBidRequest(bidRequest) { + if (bidRequest && utils.deepAccess(bidRequest, 'refererInfo.canonicalUrl')) { + return bidRequest.refererInfo.canonicalUrl; + } + + try { + return window.top.location.href; + } catch (e) { + return window.location.href; + } +} + +/** + * Extracts the referrer from given bid request or use the (top) document referrer as fallback + * + * @param {*} bidRequest + * @returns {string} + */ +export function extractTopWindowReferrerFromBidRequest(bidRequest) { + if (bidRequest && utils.deepAccess(bidRequest, 'refererInfo.referer')) { + return bidRequest.refererInfo.referer; + } + + try { + return window.top.document.referrer; + } catch (e) { + return window.document.referrer; + } +} + +registerBidder(spec); diff --git a/modules/aduptechBidAdapter.md b/modules/aduptechBidAdapter.md index 98602a61fe1..281fe24cf9d 100644 --- a/modules/aduptechBidAdapter.md +++ b/modules/aduptechBidAdapter.md @@ -1,25 +1,30 @@ # Overview - ``` -Module Name: Ad Up Technology Bid Adapter +Module Name: AdUp Technology Bid Adapter Module Type: Bidder Adapter -Maintainer: steffen.anders@adup-tech.com, berlin@adup-tech.com +Maintainers: + - steffen.anders@adup-tech.com + - sebastian.briesemeister@adup-tech.com + - marten.lietz@adup-tech.com ``` # Description +Connects to AdUp Technology demand sources to fetch bids. +Please use ```aduptech``` as bidder code. Only banner formats are supported. -Connects to Ad Up Technology demand sources to fetch bids. -Please use ```aduptech``` as bidder code. Only banner formats are supported. - -The Ad Up Technology Bidding adapter requires setup and approval before beginning. -For more information visit [www.adup-tech.com](http://www.adup-tech.com/en). +The AdUp Technology bidding adapter requires setup and approval before beginning. +For more information visit [www.adup-tech.com](https://www.adup-tech.com/en) or contact [info@adup-tech.com](mailto:info@adup-tech.com). # Test Parameters -``` +```js var adUnits = [ { code: 'banner', - sizes: [[300, 250], [300, 600]], + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], + } + }, bids: [{ bidder: 'aduptech', params: { diff --git a/test/spec/modules/aduptechBidAdapter_spec.js b/test/spec/modules/aduptechBidAdapter_spec.js new file mode 100644 index 00000000000..153f6a2c5e9 --- /dev/null +++ b/test/spec/modules/aduptechBidAdapter_spec.js @@ -0,0 +1,502 @@ +import { expect } from 'chai'; +import { + BIDDER_CODE, + PUBLISHER_PLACEHOLDER, + ENDPOINT_URL, + ENDPOINT_METHOD, + spec, + extractGdprFromBidderRequest, + extractParamsFromBidRequest, + extractSizesFromBidRequest, + extractTopWindowReferrerFromBidRequest, + extractTopWindowUrlFromBidRequest +} from '../../../modules/aduptechBidAdapter'; +import { newBidder } from '../../../src/adapters/bidderFactory'; + +describe('AduptechBidAdapter', () => { + describe('extractGdprFromBidderRequest', () => { + it('should handle empty bidder request', () => { + const bidderRequest = null; + expect(extractGdprFromBidderRequest(bidderRequest)).to.be.null; + }); + + it('should handle missing gdprConsent in bidder request', () => { + const bidderRequest = {}; + expect(extractGdprFromBidderRequest(bidderRequest)).to.be.null; + }); + + it('should handle gdprConsent in bidder request', () => { + const bidderRequest = { + gdprConsent: { + consentString: 'consentString', + gdprApplies: true + } + }; + + expect(extractGdprFromBidderRequest(bidderRequest)).to.deep.equal({ + consentString: bidderRequest.gdprConsent.consentString, + consentRequired: true + }); + }); + }); + + describe('extractParamsFromBidRequest', () => { + it('should handle empty bid request', () => { + const bidRequest = null; + expect(extractParamsFromBidRequest(bidRequest)).to.be.null; + }); + + it('should handle missing params in bid request', () => { + const bidRequest = {}; + expect(extractParamsFromBidRequest(bidRequest)).to.be.null; + }); + + it('should handle params in bid request', () => { + const bidRequest = { + params: { + foo: '123', + bar: 456 + } + }; + expect(extractParamsFromBidRequest(bidRequest)).to.deep.equal(bidRequest.params); + }); + }); + + describe('extractSizesFromBidRequest', () => { + it('should handle empty bid request', () => { + const bidRequest = null; + expect(extractSizesFromBidRequest(bidRequest)).to.deep.equal([]); + }); + + it('should handle missing sizes in bid request', () => { + const bidRequest = {}; + expect(extractSizesFromBidRequest(bidRequest)).to.deep.equal([]); + }); + + it('should handle sizes in bid request', () => { + const bidRequest = { + mediaTypes: { + banner: { + sizes: [[12, 34], [56, 78]] + } + } + }; + expect(extractSizesFromBidRequest(bidRequest)).to.deep.equal(bidRequest.mediaTypes.banner.sizes); + }); + + it('should handle sizes in bid request (backward compatibility)', () => { + const bidRequest = { + sizes: [[12, 34], [56, 78]] + }; + expect(extractSizesFromBidRequest(bidRequest)).to.deep.equal(bidRequest.sizes); + }); + + it('should prefer sizes in mediaTypes.banner', () => { + const bidRequest = { + sizes: [[12, 34]], + mediaTypes: { + banner: { + sizes: [[56, 78]] + } + } + }; + expect(extractSizesFromBidRequest(bidRequest)).to.deep.equal(bidRequest.mediaTypes.banner.sizes); + }); + }); + + describe('extractTopWindowReferrerFromBidRequest', () => { + it('should use fallback if bid request is empty', () => { + const bidRequest = null; + expect(extractTopWindowReferrerFromBidRequest(bidRequest)).to.equal(window.top.document.referrer); + }); + + it('should use fallback if refererInfo in bid request is missing', () => { + const bidRequest = {}; + expect(extractTopWindowReferrerFromBidRequest(bidRequest)).to.equal(window.top.document.referrer); + }); + + it('should use fallback if refererInfo.referer in bid request is missing', () => { + const bidRequest = { + refererInfo: {} + }; + expect(extractTopWindowReferrerFromBidRequest(bidRequest)).to.equal(window.top.document.referrer); + }); + + it('should use fallback if refererInfo.referer in bid request is empty', () => { + const bidRequest = { + refererInfo: { + referer: '' + } + }; + expect(extractTopWindowReferrerFromBidRequest(bidRequest)).to.equal(window.top.document.referrer); + }); + + it('should use refererInfo.referer from bid request ', () => { + const bidRequest = { + refererInfo: { + referer: 'foobar' + } + }; + expect(extractTopWindowReferrerFromBidRequest(bidRequest)).to.equal(bidRequest.refererInfo.referer); + }); + }); + + describe('extractTopWindowUrlFromBidRequest', () => { + it('should use fallback if bid request is empty', () => { + const bidRequest = null; + expect(extractTopWindowUrlFromBidRequest(bidRequest)).to.equal(window.top.location.href); + }); + + it('should use fallback if refererInfo in bid request is missing', () => { + const bidRequest = {}; + expect(extractTopWindowUrlFromBidRequest(bidRequest)).to.equal(window.top.location.href); + }); + + it('should use fallback if refererInfo.canonicalUrl in bid request is missing', () => { + const bidRequest = { + refererInfo: {} + }; + expect(extractTopWindowUrlFromBidRequest(bidRequest)).to.equal(window.top.location.href); + }); + + it('should use fallback if refererInfo.canonicalUrl in bid request is empty', () => { + const bidRequest = { + refererInfo: { + canonicalUrl: '' + } + }; + expect(extractTopWindowUrlFromBidRequest(bidRequest)).to.equal(window.top.location.href); + }); + + it('should use refererInfo.canonicalUrl from bid request ', () => { + const bidRequest = { + refererInfo: { + canonicalUrl: 'foobar' + } + }; + expect(extractTopWindowUrlFromBidRequest(bidRequest)).to.equal(bidRequest.refererInfo.canonicalUrl); + }); + }); + + describe('spec', () => { + let adapter; + + beforeEach(() => { + adapter = newBidder(spec); + }); + + describe('inherited functions', () => { + it('exists and is a function', () => { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', () => { + it('should return true when necessary information is given', () => { + expect(spec.isBidRequestValid({ + mediaTypes: { + banner: { + sizes: [[100, 200]] + } + }, + params: { + publisher: 'test', + placement: '1234' + } + })).to.be.true; + }); + + it('should return true when necessary information is given (backward compatibility)', () => { + expect(spec.isBidRequestValid({ + sizes: [[100, 200]], + params: { + publisher: 'test', + placement: '1234' + } + })).to.be.true; + }); + + it('should return false on empty bid', () => { + expect(spec.isBidRequestValid({})).to.be.false; + }); + + it('should return false on missing sizes', () => { + expect(spec.isBidRequestValid({ + params: { + publisher: 'test', + placement: '1234' + } + })).to.be.false; + }); + + it('should return false on empty sizes', () => { + expect(spec.isBidRequestValid({ + mediaTypes: { + banner: { + sizes: [] + } + }, + params: { + publisher: 'test', + placement: '1234' + } + })).to.be.false; + }); + + it('should return false on empty sizes (backward compatibility)', () => { + expect(spec.isBidRequestValid({ + sizes: [], + params: { + publisher: 'test', + placement: '1234' + } + })).to.be.false; + }); + + it('should return false on missing params', () => { + expect(spec.isBidRequestValid({ + mediaTypes: { + banner: { + sizes: [[100, 200]] + } + }, + })).to.be.false; + }); + + it('should return false on invalid params', () => { + expect(spec.isBidRequestValid({ + mediaTypes: { + banner: { + sizes: [[100, 200]] + } + }, + params: 'bar' + })).to.be.false; + }); + + it('should return false on empty params', () => { + expect(spec.isBidRequestValid({ + mediaTypes: { + banner: { + sizes: [[100, 200]] + } + }, + params: {} + })).to.be.false; + }); + + it('should return false on missing publisher', () => { + expect(spec.isBidRequestValid({ + mediaTypes: { + banner: { + sizes: [[100, 200]] + } + }, + params: { + placement: '1234' + } + })).to.be.false; + }); + + it('should return false on missing placement', () => { + expect(spec.isBidRequestValid({ + mediaTypes: { + banner: { + sizes: [[100, 200]] + } + }, + params: { + publisher: 'test' + } + })).to.be.false; + }); + }); + + describe('buildRequests', () => { + it('should send one bid request per ad unit to the endpoint via POST', () => { + const bidRequests = [ + { + bidder: BIDDER_CODE, + bidId: 'bidId1', + adUnitCode: 'adUnitCode1', + transactionId: 'transactionId1', + auctionId: 'auctionId1', + mediaTypes: { + banner: { + sizes: [[100, 200], [300, 400]] + } + }, + params: { + publisher: 'publisher1', + placement: 'placement1' + } + }, + { + bidder: BIDDER_CODE, + bidId: 'bidId2', + adUnitCode: 'adUnitCode2', + transactionId: 'transactionId2', + auctionId: 'auctionId2', + mediaTypes: { + banner: { + sizes: [[500, 600]] + } + }, + params: { + publisher: 'publisher2', + placement: 'placement2' + } + } + ]; + + const result = spec.buildRequests(bidRequests); + expect(result.length).to.equal(2); + + expect(result[0].url).to.equal(ENDPOINT_URL.replace(PUBLISHER_PLACEHOLDER, bidRequests[0].params.publisher)); + expect(result[0].method).to.equal(ENDPOINT_METHOD); + expect(result[0].data).to.deep.equal({ + bidId: bidRequests[0].bidId, + auctionId: bidRequests[0].auctionId, + transactionId: bidRequests[0].transactionId, + adUnitCode: bidRequests[0].adUnitCode, + pageUrl: extractTopWindowUrlFromBidRequest(bidRequests[0]), + referrer: extractTopWindowReferrerFromBidRequest(bidRequests[0]), + sizes: extractSizesFromBidRequest(bidRequests[0]), + params: extractParamsFromBidRequest(bidRequests[0]), + gdpr: null + }); + + expect(result[1].url).to.equal(ENDPOINT_URL.replace(PUBLISHER_PLACEHOLDER, bidRequests[1].params.publisher)); + expect(result[1].method).to.equal(ENDPOINT_METHOD); + expect(result[1].data).to.deep.equal({ + bidId: bidRequests[1].bidId, + auctionId: bidRequests[1].auctionId, + transactionId: bidRequests[1].transactionId, + adUnitCode: bidRequests[1].adUnitCode, + pageUrl: extractTopWindowUrlFromBidRequest(bidRequests[1]), + referrer: extractTopWindowReferrerFromBidRequest(bidRequests[1]), + sizes: extractSizesFromBidRequest(bidRequests[1]), + params: extractParamsFromBidRequest(bidRequests[1]), + gdpr: null + }); + }); + + it('should pass gdpr informations', () => { + const bidderRequest = { + gdprConsent: { + consentString: 'consentString', + gdprApplies: true + } + }; + + const bidRequests = [ + { + bidder: BIDDER_CODE, + bidId: 'bidId3', + adUnitCode: 'adUnitCode3', + transactionId: 'transactionId3', + auctionId: 'auctionId3', + mediaTypes: { + banner: { + sizes: [[100, 200], [300, 400]] + } + }, + params: { + publisher: 'publisher3', + placement: 'placement3' + } + } + ]; + + const result = spec.buildRequests(bidRequests, bidderRequest); + expect(result.length).to.equal(1); + expect(result[0].data.gdpr).to.deep.equal(extractGdprFromBidderRequest(bidderRequest)); + }); + + it('should encode publisher param in endpoint url', () => { + const bidRequests = [ + { + bidder: BIDDER_CODE, + bidId: 'bidId1', + adUnitCode: 'adUnitCode1', + transactionId: 'transactionId1', + auctionId: 'auctionId1', + mediaTypes: { + banner: { + sizes: [[100, 200]] + } + }, + params: { + publisher: 'crazy publisher key äÖÜ', + placement: 'placement1' + } + }, + ]; + + const result = spec.buildRequests(bidRequests); + expect(result[0].url).to.equal(ENDPOINT_URL.replace(PUBLISHER_PLACEHOLDER, encodeURIComponent(bidRequests[0].params.publisher))); + }); + + it('should handle empty bidRequests', () => { + expect(spec.buildRequests([])).to.deep.equal([]); + }); + }); + + describe('interpretResponse', () => { + it('should correctly interpret the server response', () => { + const serverResponse = { + body: { + bid: { + bidId: 'bidId1', + price: 0.12, + net: true, + currency: 'EUR', + ttl: 123 + }, + creative: { + id: 'creativeId1', + width: 100, + height: 200, + html: '