diff --git a/modules/adoceanBidAdapter.js b/modules/adoceanBidAdapter.js new file mode 100644 index 00000000000..6f6548a1ba8 --- /dev/null +++ b/modules/adoceanBidAdapter.js @@ -0,0 +1,164 @@ +import { _each, isStr, isArray, parseSizesInput } from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'adocean'; +const URL_SAFE_FIELDS = { + slaves: true +}; + +function buildEndpointUrl(emitter, payloadMap) { + const payload = []; + _each(payloadMap, function(v, k) { + payload.push(k + '=' + (URL_SAFE_FIELDS[k] ? v : encodeURIComponent(v))); + }); + + const randomizedPart = Math.random().toString().slice(2); + return 'https://' + emitter + '/_' + randomizedPart + '/ad.json?' + payload.join('&'); +} + +function buildRequest(bid, gdprConsent) { + const emitter = bid.params.emitter; + const masterId = bid.params.masterId; + const slaveId = bid.params.slaveId; + const payload = { + id: masterId, + slaves: "" + }; + if (gdprConsent) { + payload.gdpr_consent = gdprConsent.consentString || undefined; + payload.gdpr = gdprConsent.gdprApplies ? 1 : 0; + } + + if (bid.userId && bid.userId.gemiusId) { + payload.aouserid = bid.userId.gemiusId; + } + + const bidIdMap = {}; + const uniquePartLength = 10; + + const rawSlaveId = bid.params.slaveId.replace('adocean', ''); + payload.slaves = rawSlaveId.slice(-uniquePartLength); + + bidIdMap[slaveId] = bid.bidId; + + if (bid.mediaTypes.video) { + if (bid.mediaTypes.video.context === 'instream') { + if (bid.mediaTypes.video.maxduration) { + payload.dur = bid.mediaTypes.video.maxduration; + payload.maxdur = bid.mediaTypes.video.maxduration; + } + if (bid.mediaTypes.video.minduration) { + payload.mindur = bid.mediaTypes.video.minduration; + } + payload.spots = 1; + } + if (bid.mediaTypes.video.context === 'adpod') { + const durationRangeSec = bid.mediaTypes.video.durationRangeSec; + if (!bid.mediaTypes.video.adPodDurationSec || !isArray(durationRangeSec) || durationRangeSec.length === 0) { + return; + } + const spots = calculateAdPodSpotsNumber(bid.mediaTypes.video.adPodDurationSec, bid.mediaTypes.video.durationRangeSec); + const maxDuration = Math.max(...durationRangeSec); + payload.dur = bid.mediaTypes.video.adPodDurationSec; + payload.maxdur = maxDuration; + payload.spots = spots; + } + } else if (bid.mediaTypes.banner) { + payload.aosize = parseSizesInput(bid.mediaTypes.banner.sizes).join(','); + } + + return { + method: 'GET', + url: buildEndpointUrl(emitter, payload), + data: '', + bidIdMap: bidIdMap + }; +} + +function calculateAdPodSpotsNumber(adPodDurationSec, durationRangeSec) { + const minAllowedDuration = Math.min(...durationRangeSec); + const numberOfSpots = Math.floor(adPodDurationSec / minAllowedDuration); + return numberOfSpots; +} + +function interpretResponse(placementResponse, bidRequest, bids) { + const requestId = bidRequest.bidIdMap[placementResponse.id]; + if (!placementResponse.error && requestId) { + if (!placementResponse.code || !placementResponse.height || !placementResponse.width || !placementResponse.price) { + return; + } + let adCode = decodeURIComponent(placementResponse.code); + + const bid = { + cpm: parseFloat(placementResponse.price), + currency: placementResponse.currency, + height: parseInt(placementResponse.height, 10), + requestId: requestId, + width: parseInt(placementResponse.width, 10), + netRevenue: false, + ttl: parseInt(placementResponse.ttl), + creativeId: placementResponse.crid, + meta: { + advertiserDomains: placementResponse.adomain || [] + } + }; + if (placementResponse.isVideo) { + bid.meta.mediaType = VIDEO; + bid.vastXml = adCode; + } else { + bid.meta.mediaType = BANNER; + bid.ad = adCode; + } + + bids.push(bid); + } +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO], + + isBidRequestValid: function(bid) { + const requiredParams = ['slaveId', 'masterId', 'emitter']; + if (requiredParams.some(name => !isStr(bid.params[name]) || !bid.params[name].length)) { + return false; + } + + if (bid.mediaTypes.banner) { + return true; + } + if (bid.mediaTypes.video) { + if (bid.mediaTypes.video.context === 'instream') { + return true; + } + if (bid.mediaTypes.video.context === 'adpod') { + return !bid.mediaTypes.video.requireExactDuration; + } + } + return false; + }, + + buildRequests: function(validBidRequests, bidderRequest) { + let requests = []; + + _each(validBidRequests, function(bidRequest) { + requests.push(buildRequest(bidRequest, bidderRequest.gdprConsent)); + }); + + return requests; + }, + + interpretResponse: function(serverResponse, bidRequest) { + let bids = []; + + if (isArray(serverResponse.body)) { + _each(serverResponse.body, function(placementResponse) { + interpretResponse(placementResponse, bidRequest, bids); + }); + } + + return bids; + } +}; +registerBidder(spec); diff --git a/modules/adoceanBidAdapter.md b/modules/adoceanBidAdapter.md new file mode 100644 index 00000000000..c7b55ddc5c3 --- /dev/null +++ b/modules/adoceanBidAdapter.md @@ -0,0 +1,58 @@ +# Overview + +Module Name: AdOcean Bidder Adapter +Module Type: Bidder Adapter +Maintainer: prebid@gemius.com + +# Description + +AdOcean Bidder Adapter for Prebid.js. +Banner and video formats are supported. + +# Test Parameters Banner +```js + var adUnits = [ + { + code: 'test-div', + mediaTypes: { + banner: { + sizes: [[300, 200]] + } + }, + bids: [ + { + bidder: "adocean", + params: { + slaveId: 'adoceanmyaotcpiltmmnj', + masterId: 'ek1AWtSWh3BOa_x2P1vlMQ_uXXJpJcbhsHAY5PFQjWD.D7', + emitter: 'myao.adocean.pl' + } + } + ] + } + ]; +``` +# Test Parameters Video +```js + var adUnits = [ + { + code: 'test-div', + mediaTypes: { + video: { + context: 'instream', + playerSize: [300, 200] + } + }, + bids: [ + { + bidder: "adocean", + params: { + slaveId: 'adoceanmyaonenfcoqfnd', + masterId: '2k6gA7RWl08Zn0bi42RV8LNCANpKb6LqhvKzbmK3pzP.U7', + emitter: 'myao.adocean.pl' + } + } + ] + } + ]; +``` diff --git a/test/spec/modules/adoceanBidAdapter_spec.js b/test/spec/modules/adoceanBidAdapter_spec.js new file mode 100644 index 00000000000..53a82ec963d --- /dev/null +++ b/test/spec/modules/adoceanBidAdapter_spec.js @@ -0,0 +1,461 @@ +import { expect } from 'chai'; +import { spec } from 'modules/adoceanBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import { deepClone } from 'src/utils.js'; + +describe('AdoceanAdapter', function () { + const adapter = newBidder(spec); + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + const bannerBid = { + bidder: 'adocean', + params: { + masterId: 'tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7', + slaveId: 'adoceanmyaozpniqismex', + emitter: 'myao.adocean.pl' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + }; + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bannerBid)).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + const invalidBid = Object.assign({}, bannerBid, {params: {masterId: 0}}); + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); + }); + + const videoInscreenBid = { + bidder: 'adocean', + params: { + masterId: 'tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7', + slaveId: 'adoceanmyaozpniqismex', + emitter: 'myao.adocean.pl' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + video: { + context: 'instream', + playerSize: [300, 250] + } + }, + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + }; + + it('should return true for instream video', function () { + expect(spec.isBidRequestValid(videoInscreenBid)).to.equal(true); + }); + + const videoAdpodBid = { + bidder: 'adocean', + params: { + masterId: 'tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7', + slaveId: 'adoceanmyaozpniqismex', + emitter: 'myao.adocean.pl' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + video: { + context: 'adpod', + playerSize: [300, 250], + adPodDurationSec: 300, + durationRangeSec: [15, 30], + requireExactDuration: false + } + }, + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + }; + + it('should return true for adpod video without requireExactDuration', function () { + expect(spec.isBidRequestValid(videoAdpodBid)).to.equal(true); + }); + + it('should return false for adpod video with requireExactDuration', function () { + const invalidBid = Object.assign({}, videoAdpodBid); + invalidBid.mediaTypes.video.requireExactDuration = true; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); + }); + + const videoOutstreamBid = { + bidder: 'adocean', + params: { + masterId: 'tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7', + slaveId: 'adoceanmyaozpniqismex', + emitter: 'myao.adocean.pl' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + video: { + context: 'outstream', + playerSize: [300, 250] + } + }, + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + }; + + it('should return false for outstream video', function () { + expect(spec.isBidRequestValid(videoOutstreamBid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + const bidRequests = [ + { + bidder: 'adocean', + params: { + masterId: 'tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7', + slaveId: 'adoceanmyaozpniqismex', + emitter: 'myao.adocean.pl' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + }, + { + bidder: 'adocean', + params: { + masterId: 'tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7', + slaveId: 'adoceanmyaozpniqismex', + emitter: 'myao.adocean.pl' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 200], [600, 250]] + } + }, + bidId: '30b31c1838de1f', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + } + ]; + + const bidderRequest = { + gdprConsent: { + consentString: 'BOQHk-4OSlWKFBoABBPLBd-AAAAgWAHAACAAsAPQBSACmgFTAOkA', + gdprApplies: true + } + }; + + it('should send two requests if slave is duplicated', function () { + const nrOfRequests = spec.buildRequests(bidRequests, bidderRequest).length; + expect(nrOfRequests).to.equal(2); + }); + + it('should add bidIdMap with correct slaveId => bidId mapping', function () { + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (let i = 0; i < bidRequests.length; i++) { + expect(requests[i]).to.exist; + expect(requests[i].bidIdMap).to.exist; + expect(requests[i].bidIdMap[bidRequests[i].params.slaveId]).to.equal(bidRequests[i].bidId); + } + }); + + it('sends bid request to url via GET', function () { + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.method).to.equal('GET'); + expect(request.url).to.match(new RegExp(`^https://${bidRequests[0].params.emitter}/_[0-9]*/ad.json`)); + }); + + it('should attach id to url', function () { + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.url).to.include('id=' + bidRequests[0].params.masterId); + }); + + it('should attach consent information to url', function () { + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.url).to.include('gdpr=1'); + expect(request.url).to.include('gdpr_consent=' + bidderRequest.gdprConsent.consentString); + }); + + it('should attach slaves information to url', function () { + let requests = spec.buildRequests(bidRequests, bidderRequest); + expect(requests[0].url).to.include('slaves=zpniqismex'); + expect(requests[1].url).to.include('slaves=zpniqismex'); + expect(requests[0].url).to.include('aosize=300x250%2C300x600'); + expect(requests[1].url).to.include('aosize=300x200%2C600x250'); + + const differentSlavesBids = deepClone(bidRequests); + differentSlavesBids[1].params.slaveId = 'adoceanmyaowafpdwlrks'; + requests = spec.buildRequests(differentSlavesBids, bidderRequest); + expect(requests.length).to.equal(2); + expect(requests[0].url).to.include('slaves=zpniqismex'); + expect(requests[1].url).to.include('slaves=wafpdwlrks'); + }); + + const videoInstreamBidRequests = [ + { + bidder: 'adocean', + params: { + masterId: 'tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7', + slaveId: 'adoceanmyaozpniqismex', + emitter: 'myao.adocean.pl' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + video: { + playerSize: [200, 200], + context: 'instream', + minduration: 10, + maxduration: 60, + } + }, + bidId: '30b31c1838de1g', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a476', + } + ]; + + it('should build correct video instream request', function () { + const request = spec.buildRequests(videoInstreamBidRequests, bidderRequest)[0]; + expect(request).to.exist; + expect(request.url).to.include('id=' + videoInstreamBidRequests[0].params.masterId); + expect(request.url).to.include('slaves=zpniqismex'); + expect(request.url).to.include('spots=1'); + expect(request.url).to.include('dur=60'); + expect(request.url).to.include('maxdur=60'); + expect(request.url).to.include('mindur=10'); + }); + + const videoAdpodBidRequests = [ + { + bidder: 'adocean', + params: { + masterId: 'tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7', + slaveId: 'adoceanmyaozpniqismex', + emitter: 'myao.adocean.pl' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + video: { + playerSize: [200, 200], + context: 'adpod', + adPodDurationSec: 300, + durationRangeSec: [15, 30], + requireExactDuration: false + } + }, + bidId: '30b31c1838de1h', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a476', + } + ]; + + it('should build correct video adpod request', function () { + const request = spec.buildRequests(videoAdpodBidRequests, bidderRequest)[0]; + expect(request).to.exist; + expect(request.url).to.include('id=' + videoAdpodBidRequests[0].params.masterId); + expect(request.url).to.include('slaves=zpniqismex'); + expect(request.url).to.include('spots=20'); // 300 / 15 = 20 + expect(request.url).to.include('dur=300'); + expect(request.url).to.include('maxdur=30'); + expect(request.url).to.not.include('mindur='); + }); + }); + + describe('interpretResponseBanner', function () { + const response = { + 'body': [ + { + 'id': 'adoceanmyaozpniqismex', + 'price': '0.019000', + 'ttl': '360', + 'crid': 'veeinoriep', + 'currency': 'EUR', + 'width': '300', + 'height': '250', + 'isVideo': false, + 'code': '%3C!--%20Creative%20--%3E', + 'adomain': ['adocean.pl'] + } + ], + 'headers': { + 'get': function() {} + } + }; + + const bidRequest = { + bidder: 'adocean', + params: { + masterId: 'tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7', + slaveId: 'adoceanmyaozpniqismex', + emitter: 'myao.adocean.pl' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bidIdMap: { + adoceanmyaozpniqismex: '30b31c1838de1e' + }, + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + }; + + it('should get correct bid response', function () { + const expectedResponse = [ + { + 'requestId': '30b31c1838de1e', + 'cpm': 0.019000, + 'currency': 'EUR', + 'width': 300, + 'height': 250, + 'ad': '', + 'creativeId': 'veeinoriep', + 'ttl': 360, + 'netRevenue': false, + 'meta': { + 'advertiserDomains': ['adocean.pl'], + 'mediaType': 'banner' + } + } + ]; + + const result = spec.interpretResponse(response, bidRequest); + expect(result).to.have.lengthOf(1, 'Response should contain 1 bid'); + let resultKeys = Object.keys(result[0]); + expect(resultKeys.sort()).to.deep.equal(Object.keys(expectedResponse[0]).sort(), 'Response keys do not match'); + resultKeys.forEach(function(k) { + if (k === 'ad') { + expect(result[0][k]).to.match(/$/, 'ad does not match'); + } else if (k === 'meta') { + expect(result[0][k]).to.deep.equal(expectedResponse[0][k], 'meta does not match'); + } else { + expect(result[0][k]).to.equal(expectedResponse[0][k], `${k} does not match`); + } + }); + }); + + it('handles nobid responses', function () { + response.body = [ + { + 'id': 'adoceanmyaolafpjwftbz', + 'error': 'true' + } + ]; + + const result = spec.interpretResponse(response, bidRequest); + expect(result).to.have.lengthOf(0, 'Error response should be empty'); + }); + }); + + describe('interpretResponseVideo', function () { + const response = { + 'body': [ + { + 'id': 'adoceanmyaolifgmvmpfj', + 'price': '0.019000', + 'ttl': '360', + 'crid': 'qpqhltkgpu', + 'currency': 'EUR', + 'width': '300', + 'height': '250', + 'isVideo': true, + 'code': '%3C!--%20Video%20Creative%20--%3E', + 'adomain': ['adocean.pl'] + } + ], + 'headers': { + 'get': function() {} + } + }; + + const bidRequest = { + bidder: 'adocean', + params: { + masterId: 'tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7', + slaveId: 'adoceanmyaolifgmvmpfj', + emitter: 'myao.adocean.pl' + }, + adUnitCode: 'adunit-code', + bidIdMap: { + adoceanmyaolifgmvmpfj: '30b31c1838de1e' + }, + mediaTypes: { + video: { + playerSize: [200, 200], + context: 'instream' + } + }, + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + }; + + it('should get correct bid response', function () { + const expectedResponse = [ + { + 'requestId': '30b31c1838de1e', + 'cpm': 0.019000, + 'currency': 'EUR', + 'width': 300, + 'height': 250, + 'vastXml': '', + 'creativeId': 'qpqhltkgpu', + 'ttl': 360, + 'netRevenue': false, + 'meta': { + 'advertiserDomains': ['adocean.pl'], + 'mediaType': 'video' + } + } + ]; + + const result = spec.interpretResponse(response, bidRequest); + expect(result).to.have.lengthOf(1, 'Response should contain 1 bid'); + let resultKeys = Object.keys(result[0]); + expect(resultKeys.sort()).to.deep.equal(Object.keys(expectedResponse[0]).sort(), 'Response keys do not match'); + resultKeys.forEach(function(k) { + if (k === 'vastXml') { + expect(result[0][k]).to.match(/$/, 'vastXml does not match'); + } else if (k === 'meta') { + expect(result[0][k]).to.deep.equal(expectedResponse[0][k], 'meta does not match'); + } else { + expect(result[0][k]).to.equal(expectedResponse[0][k], `${k} does not match`); + } + }); + }); + + it('handles nobid responses', function () { + response.body = [ + { + 'id': 'adoceanmyaolafpjwftbz', + 'error': 'true' + } + ]; + + const result = spec.interpretResponse(response, bidRequest); + expect(result).to.have.lengthOf(0, 'Error response should be empty'); + }); + }); +});