From aadcc5b192d5d3eafd9fd30c8d77186d473ec098 Mon Sep 17 00:00:00 2001 From: Danijel Predarski Date: Tue, 28 Feb 2023 11:10:21 +0100 Subject: [PATCH 1/5] Initial implementation of kulturemedia bid adapter --- modules/kulturemediaBidAdapter.js | 461 +++++++++++++ modules/kulturemediaBidAdapter.md | 55 ++ .../modules/kulturemediaBidAdapter_spec.js | 613 ++++++++++++++++++ 3 files changed, 1129 insertions(+) create mode 100644 modules/kulturemediaBidAdapter.js create mode 100644 modules/kulturemediaBidAdapter.md create mode 100644 test/spec/modules/kulturemediaBidAdapter_spec.js diff --git a/modules/kulturemediaBidAdapter.js b/modules/kulturemediaBidAdapter.js new file mode 100644 index 00000000000..f68ee28f806 --- /dev/null +++ b/modules/kulturemediaBidAdapter.js @@ -0,0 +1,461 @@ +import { + deepSetValue, + logInfo, + deepAccess, + logError, + isFn, + isPlainObject, + isStr, + isNumber, + isArray, logMessage +} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'kulturemedia'; +const DEFAULT_BID_TTL = 300; +const DEFAULT_CURRENCY = 'USD'; +const DEFAULT_NET_REVENUE = true; +const DEFAULT_NETWORK_ID = 1; +const OPENRTB_VIDEO_PARAMS = [ + 'mimes', + 'minduration', + 'maxduration', + 'placement', + 'protocols', + 'startdelay', + 'skip', + 'skipafter', + 'minbitrate', + 'maxbitrate', + 'delivery', + 'playbackmethod', + 'api', + 'linearity' +]; + +export const spec = { + code: BIDDER_CODE, + VERSION: '1.0.0', + supportedMediaTypes: [BANNER, VIDEO], + ENDPOINT: 'https://ads.kulture.media/pbjs', + + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bidRequest The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (bid) { + return ( + _validateParams(bid) && + _validateBanner(bid) && + _validateVideo(bid) + ); + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} validBidRequests A non-empty list of bid requests which should be sent to the Server. + * @param {BidderRequest} bidderRequest bidder request object. + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function (validBidRequests, bidderRequest) { + if (!validBidRequests || !bidderRequest) { + return; + } + + // We need to refactor this to support mixed content when there are both + // banner and video bid requests + let openrtbRequest; + if (hasBannerMediaType(validBidRequests[0])) { + openrtbRequest = buildBannerRequestData(validBidRequests, bidderRequest); + } else if (hasVideoMediaType(validBidRequests[0])) { + openrtbRequest = buildVideoRequestData(validBidRequests[0], bidderRequest); + } + + // adding schain object + if (validBidRequests[0].schain) { + deepSetValue(openrtbRequest, 'source.ext.schain', validBidRequests[0].schain); + } + + // Attaching GDPR Consent Params + if (bidderRequest.gdprConsent) { + deepSetValue(openrtbRequest, 'user.ext.consent', bidderRequest.gdprConsent.consentString); + deepSetValue(openrtbRequest, 'regs.ext.gdpr', (bidderRequest.gdprConsent.gdprApplies ? 1 : 0)); + } + + // CCPA + if (bidderRequest.uspConsent) { + deepSetValue(openrtbRequest, 'regs.ext.us_privacy', bidderRequest.uspConsent); + } + + // EIDS + const eids = deepAccess(validBidRequests[0], 'userIdAsEids'); + if (Array.isArray(eids) && eids.length > 0) { + deepSetValue(openrtbRequest, 'user.ext.eids', eids); + } + + let publisherId = validBidRequests[0].params.publisherId; + let placementId = validBidRequests[0].params.placementId; + const networkId = validBidRequests[0].params.networkId || DEFAULT_NETWORK_ID; + + if (validBidRequests[0].params.e2etest) { + logMessage('E2E test mode enabled'); + publisherId = 'e2etest' + } + let baseEndpoint = spec.ENDPOINT + '?pid=' + publisherId; + + if (placementId) { + baseEndpoint += '&placementId=' + placementId + } + if (networkId) { + baseEndpoint += '&nId=' + networkId + } + + const payloadString = JSON.stringify(openrtbRequest); + return { + method: 'POST', + url: baseEndpoint, + data: payloadString, + }; + }, + + interpretResponse: function (serverResponse) { + const bidResponses = []; + const response = (serverResponse || {}).body; + // response is always one seat (exchange) with (optional) bids for each impression + if (response && response.seatbid && response.seatbid.length === 1 && response.seatbid[0].bid && response.seatbid[0].bid.length) { + response.seatbid[0].bid.forEach(bid => { + if (bid.adm && bid.price) { + bidResponses.push(_createBidResponse(bid)); + } + }) + } else { + logInfo('kulturemedia.interpretResponse :: no valid responses to interpret'); + } + return bidResponses; + }, + + getUserSyncs: function (syncOptions, serverResponses) { + logInfo('kulturemedia.getUserSyncs', 'syncOptions', syncOptions, 'serverResponses', serverResponses); + let syncs = []; + + if (!syncOptions.iframeEnabled && !syncOptions.pixelEnabled) { + return syncs; + } + + serverResponses.forEach(resp => { + const userSync = deepAccess(resp, 'body.ext.usersync'); + if (userSync) { + let syncDetails = []; + Object.keys(userSync).forEach(key => { + const value = userSync[key]; + if (value.syncs && value.syncs.length) { + syncDetails = syncDetails.concat(value.syncs); + } + }); + syncDetails.forEach(syncDetails => { + syncs.push({ + type: syncDetails.type === 'iframe' ? 'iframe' : 'image', + url: syncDetails.url + }); + }); + + if (!syncOptions.iframeEnabled) { + syncs = syncs.filter(s => s.type !== 'iframe') + } + if (!syncOptions.pixelEnabled) { + syncs = syncs.filter(s => s.type !== 'image') + } + } + }); + logInfo('kulturemedia.getUserSyncs result=%o', syncs); + return syncs; + }, + +}; + +/* ======================================= + * Util Functions + *======================================= */ + +/** + * @param {BidRequest} bidRequest bid request + */ +function hasBannerMediaType(bidRequest) { + return !!deepAccess(bidRequest, 'mediaTypes.banner'); +} + +/** + * @param {BidRequest} bidRequest bid request + */ +function hasVideoMediaType(bidRequest) { + return !!deepAccess(bidRequest, 'mediaTypes.video'); +} + +function _validateParams(bidRequest) { + if (!bidRequest.params) { + return false; + } + + if (bidRequest.params.e2etest) { + return true; + } + + if (!bidRequest.params.publisherId) { + logError('Validation failed: publisherId not declared'); + return false; + } + + if (!bidRequest.params.placementId) { + logError('Validation failed: placementId not declared'); + return false; + } + + const mediaTypesExists = hasVideoMediaType(bidRequest) || hasBannerMediaType(bidRequest); + if (!mediaTypesExists) { + return false; + } + + return true; +} + +/** + * Validates banner bid request. If it is not banner media type returns true. + * @param {object} bid, bid to validate + * @return boolean, true if valid, otherwise false + */ +function _validateBanner(bidRequest) { + // If there's no banner no need to validate + if (!hasBannerMediaType(bidRequest)) { + return true; + } + const banner = deepAccess(bidRequest, 'mediaTypes.banner'); + if (!Array.isArray(banner.sizes)) { + return false; + } + + return true; +} + +/** + * Validates video bid request. If it is not video media type returns true. + * @param {object} bid, bid to validate + * @return boolean, true if valid, otherwise false + */ +function _validateVideo(bidRequest) { + // If there's no video no need to validate + if (!hasVideoMediaType(bidRequest)) { + return true; + } + + const videoPlacement = deepAccess(bidRequest, 'mediaTypes.video', {}); + const videoBidderParams = deepAccess(bidRequest, 'params.video', {}); + const params = deepAccess(bidRequest, 'params', {}); + + if (params && params.e2etest) { + return true; + } + + const videoParams = { + ...videoPlacement, + ...videoBidderParams // Bidder Specific overrides + }; + + if (!videoParams.context) { + logError('Validation failed: context id not declared'); + return false; + } + if (videoParams.context !== 'instream') { + logError('Validation failed: only context instream is supported '); + return false; + } + + if (typeof videoParams.playerSize === 'undefined' || !Array.isArray(videoParams.playerSize) || !Array.isArray(videoParams.playerSize[0])) { + logError('Validation failed: player size not declared or is not in format [[w,h]]'); + return false; + } + + return true; +} + +/** + * Prepares video request data. + * + * @param bidRequest + * @param bidderRequest + * @returns openrtbRequest + */ +function buildVideoRequestData(bidRequest, bidderRequest) { + const {params} = bidRequest; + + const videoAdUnit = deepAccess(bidRequest, 'mediaTypes.video', {}); + const videoBidderParams = deepAccess(bidRequest, 'params.video', {}); + + const videoParams = { + ...videoAdUnit, + ...videoBidderParams // Bidder Specific overrides + }; + + if (bidRequest.params && bidRequest.params.e2etest) { + videoParams.playerSize = [[640, 480]] + videoParams.conext = 'instream' + } + + const video = { + w: parseInt(videoParams.playerSize[0][0], 10), + h: parseInt(videoParams.playerSize[0][1], 10), + } + + // Obtain all ORTB params related video from Ad Unit + OPENRTB_VIDEO_PARAMS.forEach((param) => { + if (videoParams.hasOwnProperty(param)) { + video[param] = videoParams[param]; + } + }); + + // Placement Inference Rules: + // - If no placement is defined then default to 1 (In Stream) + video.placement = video.placement || 2; + + // - If product is instream (for instream context) then override placement to 1 + if (params.context === 'instream') { + video.startdelay = video.startdelay || 0; + video.placement = 1; + } + + // bid floor + const bidFloorRequest = { + currency: bidRequest.params.cur || 'USD', + mediaType: 'video', + size: '*' + }; + let floorData = bidRequest.params + if (isFn(bidRequest.getFloor)) { + floorData = bidRequest.getFloor(bidFloorRequest); + } else { + if (params.bidfloor) { + floorData = {floor: params.bidfloor, currency: params.currency || 'USD'}; + } + } + + const openrtbRequest = { + id: bidRequest.bidId, + imp: [ + { + id: '1', + video: video, + secure: isSecure() ? 1 : 0, + bidfloor: floorData.floor, + bidfloorcur: floorData.currency + } + ], + site: { + domain: bidderRequest.refererInfo.domain, + page: bidderRequest.refererInfo.page, + ref: bidderRequest.refererInfo.ref, + }, + ext: { + hb: 1, + prebidver: '$prebid.version$', + adapterver: spec.VERSION, + }, + }; + + // content + if (videoParams.content && isPlainObject(videoParams.content)) { + openrtbRequest.site.content = {}; + const contentStringKeys = ['id', 'title', 'series', 'season', 'genre', 'contentrating', 'language', 'url']; + const contentNumberkeys = ['episode', 'prodq', 'context', 'livestream', 'len']; + const contentArrayKeys = ['cat']; + const contentObjectKeys = ['ext']; + for (const contentKey in videoBidderParams.content) { + if ( + (contentStringKeys.indexOf(contentKey) > -1 && isStr(videoParams.content[contentKey])) || + (contentNumberkeys.indexOf(contentKey) > -1 && isNumber(videoParams.content[contentKey])) || + (contentObjectKeys.indexOf(contentKey) > -1 && isPlainObject(videoParams.content[contentKey])) || + (contentArrayKeys.indexOf(contentKey) > -1 && isArray(videoParams.content[contentKey]) && + videoParams.content[contentKey].every(catStr => isStr(catStr)))) { + openrtbRequest.site.content[contentKey] = videoParams.content[contentKey]; + } else { + logMessage('KultureMedia bid adapter validation error: ', contentKey, ' is either not supported is OpenRTB V2.5 or value is undefined'); + } + } + } + + return openrtbRequest; +} + +/** + * Prepares video request data. + * + * @param bidRequest + * @param bidderRequest + * @returns openrtbRequest + */ +function buildBannerRequestData(bidRequests, bidderRequest) { + const impr = bidRequests.map(bidRequest => ({ + id: bidRequest.bidId, + banner: { + format: bidRequest.mediaTypes.banner.sizes.map(sizeArr => ({ + w: sizeArr[0], + h: sizeArr[1] + })) + }, + ext: { + exchange: { + placementId: bidRequest.params.placementId + } + } + })); + + const openrtbRequest = { + id: bidderRequest.auctionId, + imp: impr, + site: { + domain: bidderRequest.refererInfo?.domain, + page: bidderRequest.refererInfo?.page, + ref: bidderRequest.refererInfo?.ref, + }, + ext: {} + }; + return openrtbRequest; +} + +function _createBidResponse(bid) { + const isADomainPresent = + bid.adomain && bid.adomain.length; + const bidResponse = { + requestId: bid.impid, + bidderCode: spec.code, + cpm: bid.price, + width: bid.w, + height: bid.h, + ad: bid.adm, + ttl: typeof bid.exp === 'number' ? bid.exp : DEFAULT_BID_TTL, + creativeId: bid.crid, + netRevenue: DEFAULT_NET_REVENUE, + currency: DEFAULT_CURRENCY, + mediaType: deepAccess(bid, 'ext.prebid.type', BANNER) + } + + if (isADomainPresent) { + bidResponse.meta = { + advertiserDomains: bid.adomain + }; + } + + if (bidResponse.mediaType === VIDEO) { + bidResponse.vastXml = bid.adm; + } + + return bidResponse; +} + +function isSecure() { + return document.location.protocol === 'https:'; +} + +registerBidder(spec); diff --git a/modules/kulturemediaBidAdapter.md b/modules/kulturemediaBidAdapter.md new file mode 100644 index 00000000000..247878222d9 --- /dev/null +++ b/modules/kulturemediaBidAdapter.md @@ -0,0 +1,55 @@ +# Overview + +``` +Module Name: Kulture Media Bid Adapter +Module Type: Bidder Adapter +Maintainer: devops@kulture.media +``` + +# Description + +Module that connects to Kulture Media's demand sources. +Kulture Media bid adapter supports Banner and Video. + + +# Test Parameters + +## Banner + +``` +var adUnits = [ + { + code: 'banner-ad-div', + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]] + } + }, + bids: [{ + bidder: 'kulturemedia', + params: { + placementId: 'test', + publisherId: 'test', + } + }] + } +]; +``` + +## Video + +We support the following OpenRTB params that can be specified in `mediaTypes.video` or in `bids[].params.video` +- 'mimes', +- 'minduration', +- 'maxduration', +- 'placement', +- 'protocols', +- 'startdelay', +- 'skip', +- 'skipafter', +- 'minbitrate', +- 'maxbitrate', +- 'delivery', +- 'playbackmethod', +- 'api', +- 'linearity' diff --git a/test/spec/modules/kulturemediaBidAdapter_spec.js b/test/spec/modules/kulturemediaBidAdapter_spec.js new file mode 100644 index 00000000000..b5fe7657725 --- /dev/null +++ b/test/spec/modules/kulturemediaBidAdapter_spec.js @@ -0,0 +1,613 @@ +import {expect} from 'chai'; +import {spec} from 'modules/kulturemediaBidAdapter.js'; + +const BANNER_REQUEST = { + 'bidderCode': 'kulturemedia', + 'auctionId': 'auctionId-56a2-4f71-9098-720a68f2f708', + 'bidderRequestId': 'requestId', + 'bidRequest': [{ + 'bidder': 'kulturemedia', + 'params': { + 'placementId': 123456, + }, + 'placementCode': 'div-gpt-dummy-placement-code', + 'mediaTypes': {'banner': {'sizes': [[300, 250]]}}, + 'bidId': 'bidId1', + 'bidderRequestId': 'bidderRequestId', + 'auctionId': 'auctionId-56a2-4f71-9098-720a68f2f708' + }, + { + 'bidder': 'kulturemedia', + 'params': { + 'placementId': 123456, + }, + 'placementCode': 'div-gpt-dummy-placement-code', + 'mediaTypes': {'banner': {'sizes': [[300, 250]]}}, + 'bidId': 'bidId2', + 'bidderRequestId': 'bidderRequestId', + 'auctionId': 'auctionId-56a2-4f71-9098-720a68f2f708' + }], + 'start': 1487883186070, + 'auctionStart': 1487883186069, + 'timeout': 3000 +}; + +const RESPONSE = { + 'headers': null, + 'body': { + 'id': 'responseId', + 'seatbid': [ + { + 'bid': [ + { + 'id': 'bidId1', + 'impid': 'bidId1', + 'price': 0.18, + 'adm': '', + 'adid': '144762342', + 'adomain': [ + 'https://dummydomain.com' + ], + 'iurl': 'iurl', + 'cid': '109', + 'crid': 'creativeId', + 'cat': [], + 'w': 300, + 'h': 250, + 'ext': { + 'prebid': { + 'type': 'banner' + }, + 'bidder': { + 'appnexus': { + 'brand_id': 334553, + 'auction_id': 514667951122925701, + 'bidder_id': 2, + 'bid_ad_type': 0 + } + } + } + }, + { + 'id': 'bidId2', + 'impid': 'bidId2', + 'price': 0.1, + 'adm': '', + 'adid': '144762342', + 'adomain': [ + 'https://dummydomain.com' + ], + 'iurl': 'iurl', + 'cid': '109', + 'crid': 'creativeId', + 'cat': [], + 'w': 300, + 'h': 250, + 'ext': { + 'prebid': { + 'type': 'banner' + }, + 'bidder': { + 'appnexus': { + 'brand_id': 386046, + 'auction_id': 517067951122925501, + 'bidder_id': 2, + 'bid_ad_type': 0 + } + } + } + } + ], + 'seat': 'kulturemedia' + } + ], + 'ext': { + 'usersync': { + 'sovrn': { + 'status': 'none', + 'syncs': [ + { + 'url': 'urlsovrn', + 'type': 'iframe' + } + ] + }, + 'appnexus': { + 'status': 'none', + 'syncs': [ + { + 'url': 'urlappnexus', + 'type': 'pixel' + } + ] + } + }, + 'responsetimemillis': { + 'appnexus': 127 + } + } + } +}; + +const DEFAULT_NETWORK_ID = 1; + +describe('kulturemediaBidAdapter:', function () { + let videoBidRequest; + + const VIDEO_REQUEST = { + 'bidderCode': 'kulturemedia', + 'auctionId': 'e158486f-8c7f-472f-94ce-b0cbfbb50ab4', + 'bidderRequestId': '34feaad34lkj2', + 'bids': videoBidRequest, + 'auctionStart': 1520001292880, + 'timeout': 3000, + 'start': 1520001292884, + 'doneCbCallCount': 0, + 'refererInfo': { + 'numIframes': 1, + 'reachedTop': true, + 'referer': 'test.com' + } + }; + + beforeEach(function () { + videoBidRequest = { + mediaTypes: { + video: { + context: 'instream', + playerSize: [[640, 480]], + } + }, + bidder: 'kulturemedia', + sizes: [640, 480], + bidId: '30b3efwfwe1e', + adUnitCode: 'video1', + params: { + video: { + playerWidth: 640, + playerHeight: 480, + mimes: ['video/mp4', 'application/javascript'], + protocols: [2, 5], + api: [2], + position: 1, + delivery: [2], + sid: 134, + rewarded: 1, + placement: 1, + hp: 1, + inventoryid: 123 + }, + site: { + id: 1, + page: 'https://test.com', + referrer: 'http://test.com' + }, + publisherId: 'km123' + } + }; + }); + + describe('isBidRequestValid', function () { + context('basic validation', function () { + beforeEach(function () { + // Basic Valid BidRequest + this.bid = { + bidder: 'kulturemedia', + mediaTypes: { + banner: { + sizes: [[250, 300]] + } + }, + params: { + placementId: 'placementId', + publisherId: 'publisherId', + } + }; + }); + + it('should accept request if placementId and publisherId are passed', function () { + expect(spec.isBidRequestValid(this.bid)).to.be.true; + }); + + it('reject requests without params', function () { + this.bid.params = {}; + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }); + + it('returns false when banner mediaType does not exist', function () { + this.bid.mediaTypes = {} + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }); + }); + + context('banner validation', function () { + it('returns true when banner sizes are defined', function () { + const bid = { + bidder: 'kulturemedia', + mediaTypes: { + banner: { + sizes: [[250, 300]] + } + }, + params: { + placementId: 'placementId', + publisherId: 'publisherId', + } + }; + + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + + it('returns false when banner sizes are invalid', function () { + const invalidSizes = [ + undefined, + '2:1', + 123, + 'test' + ]; + + invalidSizes.forEach((sizes) => { + const bid = { + bidder: 'kulturemedia', + mediaTypes: { + banner: { + sizes + } + }, + params: { + placementId: 'placementId', + publisherId: 'publisherId', + } + }; + + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); + }); + + context('video validation', function () { + beforeEach(function () { + // Basic Valid BidRequest + this.bid = { + bidder: 'kulturemedia', + mediaTypes: { + video: { + playerSize: [[300, 50]], + context: 'outstream', + mimes: ['foo', 'bar'], + protocols: [1, 2] + } + }, + params: { + placementId: 'placementId', + publisherId: 'publisherId', + } + }; + }); + + it('should return true (skip validations) when e2etest = true', function () { + this.bid.params = { + e2etest: true + }; + expect(spec.isBidRequestValid(this.bid)).to.equal(true); + }); + + it('returns false when video context is not defined', function () { + delete this.bid.mediaTypes.video.context; + + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }); + + it('returns false when video playserSize is invalid', function () { + const invalidSizes = [ + undefined, + '2:1', + 123, + 'test' + ]; + + invalidSizes.forEach((playerSize) => { + this.bid.mediaTypes.video.playerSize = playerSize; + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }); + }); + + it('returns false when video mimes is invalid', function () { + const invalidMimes = [ + undefined, + 'test', + 1, + [] + ] + + invalidMimes.forEach((mimes) => { + this.bid.mediaTypes.video.mimes = mimes; + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }) + }); + + it('returns false when video protocols is invalid', function () { + const invalidMimes = [ + undefined, + 'test', + 1, + [] + ] + + invalidMimes.forEach((protocols) => { + this.bid.mediaTypes.video.protocols = protocols; + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }) + }); + }); + }); + + describe('buildRequests', function () { + context('when mediaType is banner', function () { + it('creates request data', function () { + let request = spec.buildRequests(BANNER_REQUEST.bidRequest, BANNER_REQUEST); + + expect(request).to.exist.and.to.be.a('object'); + const payload = JSON.parse(request.data); + expect(payload.imp[0]).to.have.property('id', BANNER_REQUEST.bidRequest[0].bidId); + expect(payload.imp[1]).to.have.property('id', BANNER_REQUEST.bidRequest[1].bidId); + }); + + it('has gdpr data if applicable', function () { + const req = Object.assign({}, BANNER_REQUEST, { + gdprConsent: { + consentString: 'consentString', + gdprApplies: true, + } + }); + let request = spec.buildRequests(BANNER_REQUEST.bidRequest, req); + + const payload = JSON.parse(request.data); + expect(payload.user.ext).to.have.property('consent', req.gdprConsent.consentString); + expect(payload.regs.ext).to.have.property('gdpr', 1); + }); + + it('should properly forward eids parameters', function () { + const req = Object.assign({}, BANNER_REQUEST); + req.bidRequest[0].userIdAsEids = [ + { + source: 'dummy.com', + uids: [ + { + id: 'd6d0a86c-20c6-4410-a47b-5cba383a698a', + atype: 1 + } + ] + }]; + let request = spec.buildRequests(req.bidRequest, req); + + const payload = JSON.parse(request.data); + expect(payload.user.ext.eids[0].source).to.equal('dummy.com'); + expect(payload.user.ext.eids[0].uids[0].id).to.equal('d6d0a86c-20c6-4410-a47b-5cba383a698a'); + expect(payload.user.ext.eids[0].uids[0].atype).to.equal(1); + }); + }); + + context('when mediaType is video', function () { + it('should create a POST request for every bid', function () { + const requests = spec.buildRequests([videoBidRequest], VIDEO_REQUEST); + expect(requests.method).to.equal('POST'); + expect(requests.url.trim()).to.equal(spec.ENDPOINT + '?pid=' + videoBidRequest.params.publisherId + '&nId=' + DEFAULT_NETWORK_ID); + }); + + it('should attach request data', function () { + const requests = spec.buildRequests([videoBidRequest], VIDEO_REQUEST); + const data = JSON.parse(requests.data); + const [width, height] = videoBidRequest.sizes; + const VERSION = '1.0.0'; + expect(data.imp[0].video.w).to.equal(width); + expect(data.imp[0].video.h).to.equal(height); + expect(data.imp[0].bidfloor).to.equal(videoBidRequest.params.bidfloor); + expect(data.ext.prebidver).to.equal('$prebid.version$'); + expect(data.ext.adapterver).to.equal(spec.VERSION); + }); + + it('should set pubId to e2etest when bid.params.e2etest = true', function () { + videoBidRequest.params.e2etest = true; + const requests = spec.buildRequests([videoBidRequest], VIDEO_REQUEST); + expect(requests.method).to.equal('POST'); + expect(requests.url).to.equal(spec.ENDPOINT + '?pid=e2etest&nId=' + DEFAULT_NETWORK_ID); + }); + + it('should attach End 2 End test data', function () { + videoBidRequest.params.e2etest = true; + const requests = spec.buildRequests([videoBidRequest], VIDEO_REQUEST); + const data = JSON.parse(requests.data); + expect(data.imp[0].bidfloor).to.not.exist; + expect(data.imp[0].video.w).to.equal(640); + expect(data.imp[0].video.h).to.equal(480); + }); + }); + }); + + describe('interpretResponse', function () { + context('when mediaType is banner', function () { + it('have bids', function () { + let bids = spec.interpretResponse(RESPONSE, BANNER_REQUEST); + expect(bids).to.be.an('array').that.is.not.empty; + validateBidOnIndex(0); + validateBidOnIndex(1); + + function validateBidOnIndex(index) { + expect(bids[index]).to.have.property('currency', 'USD'); + expect(bids[index]).to.have.property('requestId', RESPONSE.body.seatbid[0].bid[index].impid); + expect(bids[index]).to.have.property('cpm', RESPONSE.body.seatbid[0].bid[index].price); + expect(bids[index]).to.have.property('width', RESPONSE.body.seatbid[0].bid[index].w); + expect(bids[index]).to.have.property('height', RESPONSE.body.seatbid[0].bid[index].h); + expect(bids[index]).to.have.property('ad', RESPONSE.body.seatbid[0].bid[index].adm); + expect(bids[index]).to.have.property('creativeId', RESPONSE.body.seatbid[0].bid[index].crid); + expect(bids[index].meta).to.have.property('advertiserDomains', RESPONSE.body.seatbid[0].bid[index].adomain); + expect(bids[index]).to.have.property('ttl', 300); + expect(bids[index]).to.have.property('netRevenue', true); + } + }); + + it('handles empty response', function () { + const EMPTY_RESP = Object.assign({}, RESPONSE, {'body': {}}); + const bids = spec.interpretResponse(EMPTY_RESP, BANNER_REQUEST); + + expect(bids).to.be.empty; + }); + }); + + context('when mediaType is video', function () { + it('should return no bids if the response is not valid', function () { + const bidResponse = spec.interpretResponse({ + body: null + }, { + videoBidRequest + }); + expect(bidResponse.length).to.equal(0); + }); + + it('should return no bids if the response "nurl" and "adm" are missing', function () { + const serverResponse = { + seatbid: [{ + bid: [{ + price: 6.01 + }] + }] + }; + const bidResponse = spec.interpretResponse({ + body: serverResponse + }, { + videoBidRequest + }); + expect(bidResponse.length).to.equal(0); + }); + + it('should return no bids if the response "price" is missing', function () { + const serverResponse = { + seatbid: [{ + bid: [{ + adm: '' + }] + }] + }; + const bidResponse = spec.interpretResponse({ + body: serverResponse + }, { + videoBidRequest + }); + expect(bidResponse.length).to.equal(0); + }); + + it('should return a valid video bid response with just "adm"', function () { + const serverResponse = { + id: '123', + seatbid: [{ + bid: [{ + id: 1, + adid: 123, + impid: 456, + crid: 2, + price: 6.01, + adm: '', + adomain: [ + 'kulturemedia.com' + ], + w: 640, + h: 480, + ext: { + prebid: { + type: 'video' + }, + } + }] + }], + cur: 'USD' + }; + const bidResponse = spec.interpretResponse({ + body: serverResponse + }, { + videoBidRequest + }); + let o = { + requestId: serverResponse.seatbid[0].bid[0].impid, + ad: '', + bidderCode: spec.code, + cpm: serverResponse.seatbid[0].bid[0].price, + creativeId: serverResponse.seatbid[0].bid[0].crid, + vastXml: serverResponse.seatbid[0].bid[0].adm, + width: 640, + height: 480, + mediaType: 'video', + currency: 'USD', + ttl: 300, + netRevenue: true, + meta: { + advertiserDomains: ['kulturemedia.com'] + } + }; + expect(bidResponse[0]).to.deep.equal(o); + }); + + it('should default ttl to 300', function () { + const serverResponse = { + seatbid: [{bid: [{id: 1, adid: 123, crid: 2, price: 6.01, adm: ''}]}], + cur: 'USD' + }; + const bidResponse = spec.interpretResponse({body: serverResponse}, {videoBidRequest}); + expect(bidResponse[0].ttl).to.equal(300); + }); + it('should not allow ttl above 3601, default to 300', function () { + videoBidRequest.params.video.ttl = 3601; + const serverResponse = { + seatbid: [{bid: [{id: 1, adid: 123, crid: 2, price: 6.01, adm: ''}]}], + cur: 'USD' + }; + const bidResponse = spec.interpretResponse({body: serverResponse}, {videoBidRequest}); + expect(bidResponse[0].ttl).to.equal(300); + }); + it('should not allow ttl below 1, default to 300', function () { + videoBidRequest.params.video.ttl = 0; + const serverResponse = { + seatbid: [{bid: [{id: 1, adid: 123, crid: 2, price: 6.01, adm: ''}]}], + cur: 'USD' + }; + const bidResponse = spec.interpretResponse({body: serverResponse}, {videoBidRequest}); + expect(bidResponse[0].ttl).to.equal(300); + }); + }); + }); + + describe('getUserSyncs', function () { + it('handles no parameters', function () { + let opts = spec.getUserSyncs({}); + expect(opts).to.be.an('array').that.is.empty; + }); + it('returns non if sync is not allowed', function () { + let opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: false}); + + expect(opts).to.be.an('array').that.is.empty; + }); + + it('iframe sync enabled should return results', function () { + let opts = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: false}, [RESPONSE]); + + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('iframe'); + expect(opts[0].url).to.equal(RESPONSE.body.ext.usersync['sovrn'].syncs[0].url); + }); + + it('pixel sync enabled should return results', function () { + let opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [RESPONSE]); + + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('image'); + expect(opts[0].url).to.equal(RESPONSE.body.ext.usersync['appnexus'].syncs[0].url); + }); + + it('all sync enabled should return all results', function () { + let opts = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, [RESPONSE]); + + expect(opts.length).to.equal(2); + }); + }); +}) +; From d2c56e7136470e0ea22f727d5ebcf7b1bc33b754 Mon Sep 17 00:00:00 2001 From: Danijel Predarski Date: Tue, 28 Feb 2023 18:24:37 +0100 Subject: [PATCH 2/5] Changing outstream to instream --- modules/kulturemediaBidAdapter.js | 11 +++++++++++ test/spec/modules/kulturemediaBidAdapter_spec.js | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/modules/kulturemediaBidAdapter.js b/modules/kulturemediaBidAdapter.js index f68ee28f806..0acdd6406cb 100644 --- a/modules/kulturemediaBidAdapter.js +++ b/modules/kulturemediaBidAdapter.js @@ -264,10 +264,21 @@ function _validateVideo(bidRequest) { ...videoBidderParams // Bidder Specific overrides }; + if (!Array.isArray(videoParams.mimes) || videoParams.mimes.length === 0) { + logError('Validation failed: mimes are invalid'); + return false; + } + + if (!Array.isArray(videoParams.protocols) || videoParams.protocols.length === 0) { + logError('Validation failed: protocols are invalid'); + return false; + } + if (!videoParams.context) { logError('Validation failed: context id not declared'); return false; } + if (videoParams.context !== 'instream') { logError('Validation failed: only context instream is supported '); return false; diff --git a/test/spec/modules/kulturemediaBidAdapter_spec.js b/test/spec/modules/kulturemediaBidAdapter_spec.js index b5fe7657725..1872f6c171a 100644 --- a/test/spec/modules/kulturemediaBidAdapter_spec.js +++ b/test/spec/modules/kulturemediaBidAdapter_spec.js @@ -273,7 +273,7 @@ describe('kulturemediaBidAdapter:', function () { mediaTypes: { video: { playerSize: [[300, 50]], - context: 'outstream', + context: 'instream', mimes: ['foo', 'bar'], protocols: [1, 2] } From 74c1a14b5c9b535ac03dd3e884540774747e0988 Mon Sep 17 00:00:00 2001 From: Danijel Predarski Date: Wed, 1 Mar 2023 15:09:16 +0100 Subject: [PATCH 3/5] Enriching md file with test examples --- modules/kulturemediaBidAdapter.md | 115 ++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/modules/kulturemediaBidAdapter.md b/modules/kulturemediaBidAdapter.md index 247878222d9..240f995cf97 100644 --- a/modules/kulturemediaBidAdapter.md +++ b/modules/kulturemediaBidAdapter.md @@ -53,3 +53,118 @@ We support the following OpenRTB params that can be specified in `mediaTypes.vid - 'playbackmethod', - 'api', - 'linearity' + + +## Instream Video adUnit using mediaTypes.video +*Note:* By default, the adapter will read the mandatory parameters from mediaTypes.video. +*Note:* The Video SSP ad server will respond with an VAST XML to load into your defined player. +``` + var adUnits = [ + { + code: 'video1', + mediaTypes: { + video: { + context: 'instream', + playerSize: [640, 480], + mimes: ['video/mp4', 'application/javascript'], + protocols: [2,5], + api: [2], + position: 1, + delivery: [2], + minduration: 10, + maxduration: 30, + placement: 1, + playbackmethod: [1,5], + protocols: [2,5], + api: [2], + } + }, + bids: [ + { + bidder: 'kulturemedia', + params: { + bidfloor: 0.5, + publisherId: '12345', + placementId: '6789' + } + } + ] + } + ] +``` + +## Instream Video adUnit with placement, nid and content params +``` + var adUnits = [ + { + code: 'video1', + mediaTypes: { + video: { + context: 'instream', + playerSize: [640, 480], + mimes: ['video/mp4', 'application/javascript'], + protocols: [2,5], + api: [2], + position: 1, + delivery: [2], + minduration: 10, + maxduration: 30, + placement: 1, + playbackmethod: [1,5], + protocols: [2,5], + api: [2], + } + }, + bids: [ + { + bidder: 'kulturemedia', + params: { + bidfloor: 0.5, + publisherId: '12345', + placementId: '6789', + nid: '1234', + video: { + content:{ + id: "uuid", + url: "https://kulture.media/demo/demo.mp4", + title: "Awesome video", + genre: "Comedy", + language: "en", + season: "1", + series: "1", + + } + } + } + } + ] + } + ] +``` + +# End To End testing mode +By passing bid.params.e2etest = true you will be able to receive a test creative + +``` +var adUnits = [ + { + code: 'video1', + mediaTypes: { + video: { + context: "instream", + playerSize: [[640, 480]], + mimes: ['video/mp4'], + protocols: [2,5], + } + }, + bids: [ + { + bidder: 'kulturemedia', + params: { + e2etest: true + } + } + ] + } +] +``` From 909476ad4ee1e8b3fd8307d716e9a1cb744f8828 Mon Sep 17 00:00:00 2001 From: Danijel Predarski Date: Wed, 1 Mar 2023 15:46:01 +0100 Subject: [PATCH 4/5] Changing nId to networkId --- modules/kulturemediaBidAdapter.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/kulturemediaBidAdapter.md b/modules/kulturemediaBidAdapter.md index 240f995cf97..b4940b4c2ae 100644 --- a/modules/kulturemediaBidAdapter.md +++ b/modules/kulturemediaBidAdapter.md @@ -93,7 +93,7 @@ We support the following OpenRTB params that can be specified in `mediaTypes.vid ] ``` -## Instream Video adUnit with placement, nid and content params +## Instream Video adUnit with placement, networkId and content params ``` var adUnits = [ { @@ -122,7 +122,7 @@ We support the following OpenRTB params that can be specified in `mediaTypes.vid bidfloor: 0.5, publisherId: '12345', placementId: '6789', - nid: '1234', + networkId: '1234', video: { content:{ id: "uuid", From 0679940c0a01414fc5e7f72cb6a0d3eb34921db6 Mon Sep 17 00:00:00 2001 From: Danijel Predarski Date: Thu, 2 Mar 2023 11:00:29 +0100 Subject: [PATCH 5/5] Cleaning up md file --- modules/kulturemediaBidAdapter.md | 68 +++++++++---------------------- 1 file changed, 19 insertions(+), 49 deletions(-) diff --git a/modules/kulturemediaBidAdapter.md b/modules/kulturemediaBidAdapter.md index b4940b4c2ae..0bd17e97982 100644 --- a/modules/kulturemediaBidAdapter.md +++ b/modules/kulturemediaBidAdapter.md @@ -75,8 +75,6 @@ We support the following OpenRTB params that can be specified in `mediaTypes.vid maxduration: 30, placement: 1, playbackmethod: [1,5], - protocols: [2,5], - api: [2], } }, bids: [ @@ -93,58 +91,30 @@ We support the following OpenRTB params that can be specified in `mediaTypes.vid ] ``` -## Instream Video adUnit with placement, networkId and content params +# End To End testing mode +By passing bid.params.e2etest = true you will be able to receive a test creative + +## Banner ``` - var adUnits = [ +var adUnits = [ { - code: 'video1', - mediaTypes: { - video: { - context: 'instream', - playerSize: [640, 480], - mimes: ['video/mp4', 'application/javascript'], - protocols: [2,5], - api: [2], - position: 1, - delivery: [2], - minduration: 10, - maxduration: 30, - placement: 1, - playbackmethod: [1,5], - protocols: [2,5], - api: [2], + code: 'banner-ad-div', + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]] } - }, - bids: [ - { - bidder: 'kulturemedia', - params: { - bidfloor: 0.5, - publisherId: '12345', - placementId: '6789', - networkId: '1234', - video: { - content:{ - id: "uuid", - url: "https://kulture.media/demo/demo.mp4", - title: "Awesome video", - genre: "Comedy", - language: "en", - season: "1", - series: "1", - - } - } - } - } - ] - } - ] + }, + bids: [{ + bidder: 'kulturemedia', + params: { + e2etest: true + } + }] + } +]; ``` -# End To End testing mode -By passing bid.params.e2etest = true you will be able to receive a test creative - +## Video ``` var adUnits = [ {