From ad7563bbeb76ec2de6c4ad3875fa9069b4e1dca6 Mon Sep 17 00:00:00 2001 From: Xingwang Liao Date: Wed, 7 Jul 2021 14:26:22 +0800 Subject: [PATCH 1/9] feat(operaads): add Opera Ads bid adapter --- modules/operaadsBidAdapter.js | 786 +++++++++++++++++++ modules/operaadsBidAdapter.md | 155 ++++ test/spec/modules/operaadsBidAdapter_spec.js | 220 ++++++ 3 files changed, 1161 insertions(+) create mode 100644 modules/operaadsBidAdapter.js create mode 100644 modules/operaadsBidAdapter.md create mode 100644 test/spec/modules/operaadsBidAdapter_spec.js diff --git a/modules/operaadsBidAdapter.js b/modules/operaadsBidAdapter.js new file mode 100644 index 00000000000..dabae591275 --- /dev/null +++ b/modules/operaadsBidAdapter.js @@ -0,0 +1,786 @@ +import * as utils from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; +import { BANNER, VIDEO, NATIVE } from '../src/mediaTypes.js'; +import { Renderer } from '../src/Renderer.js'; +import { OUTSTREAM } from '../src/video.js'; + +const BIDDER_CODE = 'operaads'; + +const ENDPOINT = 'https://s.adx.opera.com/ortb/v2/'; +const USER_SYNC_ENDPOINT = 'https://t.adx.opera.com/pbs/sync'; + +const OUTSTREAM_RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; + +const DEFAULT_CURRENCY = 'USD'; +const DEFAULT_LANGUAGE = 'en'; +const NET_REVENUE = true; + +const BANNER_DEFAULTS = { + SIZE: [300, 250] +} + +const VIDEO_DEFAULTS = { + PROTOCOLS: [2, 3, 5, 6], + MIMES: ['video/mp4'], + PLAYBACK_METHODS: [1, 2, 3, 4], + DELIVERY: [1], + API: [1, 2, 5], + SIZE: [640, 480] +} + +const NATIVE_DEFAULTS = { + IMAGE_TYPE: { + ICON: 1, + MAIN: 3, + }, + ASSET_ID: { + TITLE: 1, + IMAGE: 2, + ICON: 3, + BODY: 4, + SPONSORED: 5, + CTA: 6 + }, + DATA_ASSET_TYPE: { + SPONSORED: 1, + DESC: 2, + CTA_TEXT: 12, + }, + LENGTH: { + TITLE: 90, + BODY: 140, + SPONSORED: 25, + CTA: 20 + } +} + +export const spec = { + code: BIDDER_CODE, + + // short code + aliases: ['opera'], + + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid + * @returns boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (bid) { + if (!bid) { + utils.logWarn(BIDDER_CODE, 'Invalid bid,', bid); + return false; + } + + if (!bid.params) { + utils.logWarn(BIDDER_CODE, 'bid.params is required.') + return false; + } + + if (!bid.params.placementId) { + utils.logWarn(BIDDER_CODE, 'bid.params.placementId is required.') + return false; + } + + if (!bid.params.endpointId) { + utils.logWarn(BIDDER_CODE, 'bid.params.endpointId is required.') + return false; + } + + if (!bid.params.publisherId) { + utils.logWarn(BIDDER_CODE, 'bid.params.publisherId is required.') + return false; + } + + return true; + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} validBidRequests An array of bidRequest objects + * @param {bidderRequest} bidderRequest The master bidRequest object. + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function (validBidRequests, bidderRequest) { + return validBidRequests.map(validBidRequest => (buildOpenRtbBidRequest(validBidRequest, bidderRequest))) + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function (serverResponse, bidRequest) { + let bidResponses = []; + + let serverBody; + if ((serverBody = serverResponse.body) && serverBody.seatbid && utils.isArray(serverBody.seatbid)) { + serverBody.seatbid.forEach((seatbidder) => { + if (seatbidder.bid && utils.isArray(seatbidder.bid)) { + bidResponses = seatbidder.bid.map((bid) => buildBidResponse(bid, bidRequest.originalBidRequest, serverBody)); + } + }); + } + + return bidResponses; + }, + + /** + * Register the user sync pixels which should be dropped after the auction. + * + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} serverResponses List of server's responses. + * @return {UserSync[]} The user syncs which should be dropped. + */ + getUserSyncs: function (syncOptions, serverResponses, gdprConsent, uspConsent) { + const syncs = []; + + const params = []; + if (gdprConsent) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + params.push(`gdpr=${Number(gdprConsent.gdprApplies)}`); + } + params.push(`gdpr_consent=${encodeURIComponent(gdprConsent.consentString)}`); + } + + if (uspConsent) { + params.push(`us_privacy=${encodeURIComponent(uspConsent)}`); + } + + if (syncOptions.pixelEnabled && serverResponses.length > 0) { + syncs.push({ + type: 'image', + url: USER_SYNC_ENDPOINT + '?' + params.join('&') + }); + } + + return syncs; + }, + + /** + * Register bidder specific code, which will execute if bidder timed out after an auction + * + * @param {data} timeoutData Containing timeout specific data + */ + onTimeout: function (timeoutData) { }, + + /** + * Register bidder specific code, which will execute if a bid from this bidder won the auction + * + * @param {Bid} bid The bid that won the auction + */ + onBidWon: function (bid) { }, + + /** + * Register bidder specific code, which will execute when the adserver targeting has been set for a bid from this bidder + * + * @param {Bid} bid The bid of which the targeting has been set + */ + onSetTargeting: function (bid) { } +} + +/** + * Buid openRtb request from bidRequest and bidderRequest + * + * @param {BidRequest} bidRequest + * @param {BidderRequest} bidderRequest + * @returns {Request} + */ +function buildOpenRtbBidRequest(bidRequest, bidderRequest) { + const currencies = getCurrencies(bidRequest); + + const pageReferrer = utils.deepAccess(bidderRequest, 'refererInfo.referer'); + + // build OpenRTB request body + const payload = { + id: bidderRequest.auctionId, + tmax: bidderRequest.timeout || config.getConfig('bidderTimeout'), + test: config.getConfig('debug') ? 1 : 0, + imp: createImp(bidRequest, currencies[0]), + device: getDevice(), + site: { + id: String(utils.deepAccess(bidRequest, 'params.publisherId')), + domain: getDomain(pageReferrer), + page: pageReferrer, + ref: window.self === window.top ? document.referrer : '', + }, + at: 1, + bcat: getBcat(bidRequest), + cur: currencies, + regs: { + coppa: config.getConfig('coppa') ? 1 : 0, + ext: {} + }, + user: {} + } + + const gdprConsent = utils.deepAccess(bidderRequest, 'gdprConsent'); + if (!!gdprConsent && gdprConsent.gdprApplies) { + utils.deepSetValue(payload, 'regs.ext.gdpr', 1); + utils.deepSetValue(payload, 'user.ext.consent', gdprConsent.consentString); + } + + const uspConsent = utils.deepAccess(bidderRequest, 'uspConsent'); + if (uspConsent) { + utils.deepSetValue(payload, 'regs.ext.us_privacy', uspConsent); + } + + let userId; + for (const idModule of ['sharedId', 'pubcid', 'tdid']) { + if ((userId = utils.deepAccess(bidRequest, `userId.${idModule}`))) { + break; + } + } + + if (!userId) { + userId = utils.generateUUID(); + } + + utils.deepSetValue(payload, 'user.id', userId); + + const eids = utils.deepAccess(bidRequest, 'userIdAsEids', []); + if (eids.length > 0) { + utils.deepSetValue(payload, 'user.eids', eids); + } + + return { + method: 'POST', + url: ENDPOINT + String(utils.deepAccess(bidRequest, 'params.publisherId')) + + '?ep=' + String(utils.deepAccess(bidRequest, 'params.endpointId')), + data: JSON.stringify(payload), + options: { + contentType: 'application/json', + customHeaders: { + 'x-openrtb-version': 2.5 + } + }, + // set original bid request, so we can get it from interpretResponse + originalBidRequest: bidRequest + } +} + +/** + * Build bid response from openrtb bid response. + * + * @param {OpenRtbBid} bid + * @param {BidRequest} bidRequest + * @param {OpenRtbResponseBody} responseBody + * @returns {BidResponse} + */ +function buildBidResponse(bid, bidRequest, responseBody) { + let mediaType = BANNER; + let nativeResponse; + + if (/VAST\s+version/.test(bid.adm)) { + mediaType = VIDEO; + } else { + let markup; + try { + markup = JSON.parse(bid.adm.replace(/\\/g, '')); + } catch (e) { + markup = null; + } + + if (markup && utils.isPlainObject(markup.native)) { + mediaType = NATIVE; + nativeResponse = markup.native; + } + } + + const categories = utils.deepAccess(bid, 'cat', []); + + const bidResponse = { + requestId: bid.impid, + cpm: (parseFloat(bid.price) || 0).toFixed(2), + currency: responseBody.cur || DEFAULT_CURRENCY, + ad: bid.adm, + width: bid.w, + height: bid.h, + mediaType: mediaType, + ttl: 300, + creativeId: bid.crid || bid.id, + netRevenue: NET_REVENUE, + meta: { + mediaType: mediaType, + primaryCatId: categories[0], + secondaryCatIds: categories.slice(1), + } + }; + + if (bid.adomain && utils.isArray(bid.adomain) && bid.adomain.length > 0) { + bidResponse.meta.advertiserDomains = bid.adomain; + bidResponse.meta.clickUrl = bid.adomain[0]; + } + + switch (mediaType) { + case VIDEO: { + const playerSize = utils.deepAccess(bidRequest, 'mediaTypes.video.playerSize', VIDEO_DEFAULTS.SIZE); + const size = canonicalizeSizesArray(playerSize)[0]; + + bidResponse.width = bid.w || size[0]; + bidResponse.height = bid.h || size[1]; + + bidResponse.vastXml = bid.adm; + + const context = utils.deepAccess(bidRequest, 'mediaTypes.video.context'); + + // if outstream video, add a default render for it. + if (context === OUTSTREAM) { + // fill adResponse, will be used in ANOutstreamVideo.renderAd + bidResponse.adResponse = { + content: bidResponse.vastXml, + width: bidResponse.width, + height: bidResponse.height, + player_width: size[0], + player_height: size[1], + }; + bidResponse.renderer = createRenderer(bidRequest); + } + break; + } + case NATIVE: { + bidResponse.native = interpretNativeAd(nativeResponse); + break; + } + default: { + bidResponse.ad = bid.adm; + } + } + return bidResponse; +} + +/** + * Convert OpenRtb native response to bid native object. + * + * @param {OpenRtbNativeResponse} nativeResponse + * @returns {BidNative} native + */ +function interpretNativeAd(nativeResponse) { + const native = {}; + + const clickUrl = utils.deepAccess(nativeResponse, 'link.url'); + if (clickUrl) { + native.clickUrl = decodeURIComponent(clickUrl); + } + + if (nativeResponse.imptrackers && utils.isArray(nativeResponse.imptrackers)) { + native.impressionTrackers = nativeResponse.imptrackers.map(url => decodeURIComponent(url)); + } + + if (nativeResponse.clicktrackers && utils.isArray(nativeResponse.clicktrackers)) { + native.clickTrackers = nativeResponse.clicktrackers.map(url => decodeURIComponent(url)); + } + + if (nativeResponse.jstracker && utils.isArray(nativeResponse.jstracker)) { + native.jstracker = nativeResponse.jstracker.map(url => decodeURIComponent(url)); + } + + let assets; + if ((assets = nativeResponse.assets) && utils.isArray(assets)) { + assets.forEach((asset) => { + switch (asset.id) { + case NATIVE_DEFAULTS.ASSET_ID.TITLE: { + const title = utils.deepAccess(asset, 'title.text'); + if (title) { + native.title = title; + } + break; + } + case NATIVE_DEFAULTS.ASSET_ID.IMAGE: { + if (asset.img) { + native.image = { + url: decodeURIComponent(asset.img.url), + width: asset.img.w, + height: asset.img.h + } + } + break; + } + case NATIVE_DEFAULTS.ASSET_ID.ICON: { + if (asset.img) { + native.icon = { + url: encodeURIComponent(asset.img.url), + width: asset.img.w, + height: asset.img.h + } + } + break; + } + case NATIVE_DEFAULTS.ASSET_ID.BODY: { + const body = utils.deepAccess(asset, 'data.value'); + if (body) { + native.body = body; + } + break; + } + case NATIVE_DEFAULTS.ASSET_ID.SPONSORED: { + const sponsoredBy = utils.deepAccess(asset, 'data.value'); + if (sponsoredBy) { + native.sponsoredBy = sponsoredBy; + } + break; + } + case NATIVE_DEFAULTS.ASSET_ID.CTA: { + const cta = utils.deepAccess(asset, 'data.value'); + if (cta) { + native.cta = cta; + } + break; + } + } + }); + } + + return native; +} + +/** + * Create an imp array + * + * @param {BidRequest} bidRequest + * @param {Currency} cur + * @returns {Imp[]} + */ +function createImp(bidRequest, cur) { + const imp = []; + + const floor = getBidFloor(bidRequest, cur); + + const impItem = { + id: bidRequest.bidId, + tagid: String(utils.deepAccess(bidRequest, 'params.placementId')), + bidfloor: floor, + }; + + let mediaType; + let bannerReq, videoReq, nativeReq; + + if ((bannerReq = utils.deepAccess(bidRequest, 'mediaTypes.banner'))) { + const size = canonicalizeSizesArray(bannerReq.sizes || BANNER_DEFAULTS.SIZE)[0]; + + impItem.banner = { + w: size[0], + h: size[1], + pos: 0, + }; + + mediaType = BANNER; + } else if ((videoReq = utils.deepAccess(bidRequest, 'mediaTypes.video'))) { + const size = canonicalizeSizesArray(videoReq.playerSize || VIDEO_DEFAULTS.SIZE)[0]; + + impItem.video = { + w: size[0], + h: size[1], + pos: 0, + mimes: videoReq.mimes || VIDEO_DEFAULTS.MIMES, + protocols: videoReq.protocols || VIDEO_DEFAULTS.PROTOCOLS, + startdelay: typeof videoReq.startdelay === 'number' ? videoReq.startdelay : 0, + skip: typeof videoReq.skip === 'number' ? videoReq.skip : 0, + playbackmethod: videoReq.playbackmethod || VIDEO_DEFAULTS.PLAYBACK_METHODS, + delivery: videoReq.delivery || VIDEO_DEFAULTS.DELIVERY, + api: videoReq.api || VIDEO_DEFAULTS.API, + placement: videoReq.context === OUTSTREAM ? 3 : 1, + }; + + mediaType = VIDEO; + } else if ((nativeReq = utils.deepAccess(bidRequest, 'mediaTypes.native'))) { + const params = bidRequest.nativeParams || nativeReq; + + const request = { + native: { + ver: '1.1', + assets: createNativeAssets(params), + } + }; + + impItem.native = { + ver: '1.1', + request: JSON.stringify(request), + }; + + mediaType = NATIVE; + } + + if (mediaType) { + imp.push(impItem); + } + + return imp; +} + +/** + * Convert bid sizes to size array + * + * @param {Size[]|Size[][]} sizes + * @returns {Size[][]} + */ +function canonicalizeSizesArray(sizes) { + if (sizes.length === 2 && !utils.isArray(sizes[0])) { + return [sizes]; + } + return sizes; +} + +/** + * Create Assets Object for Native request + * + * @param {Object} params + * @returns {Asset[]} + */ +function createNativeAssets(params) { + const assets = []; + + if (params.title) { + assets.push({ + id: NATIVE_DEFAULTS.ASSET_ID.TITLE, + required: params.title.required ? 1 : 0, + title: { + len: params.title.len || NATIVE_DEFAULTS.LENGTH.TITLE + } + }) + } + + if (params.image) { + assets.push({ + id: NATIVE_DEFAULTS.ASSET_ID.IMAGE, + required: params.image.required ? 1 : 0, + img: mapNativeImage(params.image, NATIVE_DEFAULTS.IMAGE_TYPE.MAIN) + }) + } + + if (params.icon) { + assets.push({ + id: NATIVE_DEFAULTS.ASSET_ID.ICON, + required: params.icon.required ? 1 : 0, + img: mapNativeImage(params.icon, NATIVE_DEFAULTS.IMAGE_TYPE.ICON) + }) + } + + if (params.sponsoredBy) { + assets.push({ + id: NATIVE_DEFAULTS.ASSET_ID.SPONSORED, + required: params.sponsoredBy.required ? 1 : 0, + data: { + type: NATIVE_DEFAULTS.DATA_ASSET_TYPE.SPONSORED, + len: params.sponsoredBy.len | NATIVE_DEFAULTS.LENGTH.SPONSORED + } + }) + } + + if (params.body) { + assets.push({ + id: NATIVE_DEFAULTS.ASSET_ID.BODY, + required: params.body.required ? 1 : 0, + data: { + type: NATIVE_DEFAULTS.DATA_ASSET_TYPE.DESC, + len: params.body.len || NATIVE_DEFAULTS.LENGTH.BODY + } + }) + } + + if (params.cta) { + assets.push({ + id: NATIVE_DEFAULTS.ASSET_ID.CTA, + required: params.cta.required ? 1 : 0, + data: { + type: NATIVE_DEFAULTS.DATA_ASSET_TYPE.CTA_TEXT, + len: params.cta.len || NATIVE_DEFAULTS.LENGTH.CTA + } + }) + } + + return assets; +} + +/** + * Create native image object + * + * @param {Object} image + * @param {Number} type + * @returns {NativeImage} + */ +function mapNativeImage(image, type) { + const img = { type: type }; + + if (image.aspect_ratios) { + const ratio = image.aspect_ratios[0]; + const minWidth = ratio.min_width || 100; + + img.wmin = minWidth; + img.hmin = (minWidth / ratio.ratio_width * ratio.ratio_height); + } + + if (image.sizes) { + const size = canonicalizeSizesArray(image.sizes)[0]; + + img.w = size[0]; + img.h = size[1]; + } + + return img; +} + +/** + * Get publisher domain + * + * @param {String} referer + * @returns {String} domain + */ +function getDomain(referer) { + let domain; + + if (!(domain = config.getConfig('publisherDomain'))) { + const u = utils.parseUrl(referer); + domain = u.hostname; + } + + return domain.replace(/^https?:\/\/([\w\-\.]+)(?::\d+)?/, '$1'); +} + +/** + * Get bid floor price + * + * @param {BidRequest} bid + * @param {String} cur + * @returns {Number} floor price + */ +function getBidFloor(bid, cur) { + let floorInfo = {}; + + if (typeof bid.getFloor === 'function') { + floorInfo = bid.getFloor({ + currency: cur, + mediaType: '*', + size: '*' + }); + } + + return floorInfo.floor || 0.0; +} + +/** + * Get currencies from bid request + * + * @param {BidRequest} bidRequest + * @returns {String[]} currencies + */ +function getCurrencies(bidRequest) { + let currencies = []; + + const pCur = utils.deepAccess(bidRequest, 'params.currency'); + if (pCur) { + currencies = currencies.concat(pCur); + } + + if (!currencies.length) { + let currency; + if ((currency = config.getConfig('currency')) && currency.adServerCurrency) { + currencies.push(currency.adServerCurrency); + } else { + currencies.push(DEFAULT_CURRENCY); + } + } + + return currencies; +} + +/** + * Get bcat + * + * @param {BidRequest} bidRequest + * @returns {String[]} + */ +function getBcat(bidRequest) { + let bcat = []; + + const pBcat = utils.deepAccess(bidRequest, 'params.bcat'); + if (pBcat) { + bcat = bcat.concat(pBcat); + } + + return bcat; +} + +/** + * Get device info + * + * @returns {Object} + */ +function getDevice() { + const device = config.getConfig('device') || {}; + + device.w = device.w || window.screen.width; + device.h = device.h || window.screen.height; + device.ua = device.ua || navigator.userAgent; + device.language = device.language || getLanguage(); + device.dnt = typeof device.dnt === 'number' + ? device.dnt : (utils.getDNT() ? 1 : 0); + + return device; +} + +/** + * Get browser language + * + * @returns {String} language + */ +function getLanguage() { + const lang = (navigator.languages && navigator.languages[0]) || + navigator.language || navigator.userLanguage; + return lang ? lang.split('-')[0] : DEFAULT_LANGUAGE; +} + +/** + * Create render for outstream video. + * + * @param {BidRequest} bidRequest + * @returns + */ +function createRenderer(bidRequest) { + const globalRenderer = utils.deepAccess(bidRequest, 'renderer'); + const currentRenderer = utils.deepAccess(bidRequest, 'mediaTypes.video.renderer'); + + let url = OUTSTREAM_RENDERER_URL; + let config = {}; + let render = function (bid) { + bid.renderer.push(() => { + window.ANOutstreamVideo.renderAd({ + sizes: [bid.width, bid.height], + targetId: bid.adUnitCode, + adResponse: bid.adResponse, + }); + }); + }; + + if (currentRenderer) { + url = currentRenderer.url; + config = currentRenderer.options; + render = currentRenderer.render; + } else if (globalRenderer) { + url = globalRenderer.url; + config = globalRenderer.options; + render = globalRenderer.render; + } + + const renderer = Renderer.install({ + id: bidRequest.bidId, + url: url, + loaded: false, + config: config, + adUnitCode: bidRequest.adUnitCode + }); + + try { + renderer.setRender(render); + } catch (e) { + utils.logError(BIDDER_CODE, 'Error calling setRender on renderer', e); + } + return renderer; +} + +registerBidder(spec); diff --git a/modules/operaadsBidAdapter.md b/modules/operaadsBidAdapter.md new file mode 100644 index 00000000000..9bfe3e76b88 --- /dev/null +++ b/modules/operaadsBidAdapter.md @@ -0,0 +1,155 @@ +# OperaAds Bidder Adapter + +## Overview + +``` +Module Name: OperaAds Bidder Adapter +Module Type: Bidder Adapter +Maintainer: adtech-prebid-group@opera.com +``` + +## Description + +Module that connects to OperaAds's demand sources + +## Bid Parameters + +| Name | Scope | Type | Description | Example +| ---- | ----- | ---- | ----------- | ------- +| `placementId` | required | String | The Placement Id provided by Opera Ads. | `s12345678` +| `endpointId` | required | String | The Endpoint Id provided by Opera Ads. | `ep12345678` +| `publisherId` | required | String | The Publisher Id provided by Opera Ads. | `pub12345678` +| `currency` | optional | String or String[] | Currency. | `USD` +| `bcat` | optional | String or String[] | The bcat value. | `IAB9-31` + +### Bid Video Parameters + +Set these parameters to `bid.mediaTypes.video`. + +| Name | Scope | Type | Description | Example +| ---- | ----- | ---- | ----------- | ------- +| `context` | optional | String | `instream` or `outstream`. | `instream` +| `mimes` | optional | String[] | Content MIME types supported. | `['video/mp4']` +| `playerSize` | optional | Number[] or Number[][] | Video player size in device independent pixels | `[[640, 480]]` +| `protocols` | optional | Number[] | Array of supported video protocls. | `[1, 2, 3, 4, 5, 6, 7, 8]` +| `startdelay` | optional | Number | Indicates the start delay in seconds for pre-roll, mid-roll, or post-roll ad placements. | `0` +| `skip` | optional | Number | Indicates if the player will allow the video to be skipped, where 0 = no, 1 = yes. | `1` +| `playbackmethod` | optional | Number[] | Playback methods that may be in use. | `[2]` +| `delivery` | optional | Number[] | Supported delivery methods. | `[1]` +| `api` | optional | Number[] | List of supported API frameworks for this impression. | `[1, 2, 5]` + +### Bid Native Parameters + +Set these parameters to `bid.nativeParams` or `bid.mediaTypes.native`. + +| Name | Scope | Type | Description | Example +| ---- | ----- | ---- | ----------- | ------- +| `title` | optional | Object | Config for native asset title. | `{required: true, len: 25}` +| `image` | optional | Object | Config for native asset image. | `{required: true, sizes: [[300, 250]], aspect_ratios: [{min_width: 300, min_height: 250, ratio_width: 1, ratio_height: 1}]}` +| `icon` | optional | Object | Config for native asset icon. | `{required: true, sizes: [[60, 60]], aspect_ratios: [{min_width: 60, min_height: 60, ratio_width: 1, ratio_height: 1}]}}` +| `sponsoredBy` | optional | Object | Config for native asset sponsoredBy. | `{required: true, len: 20}` +| `body` | optional | Object | Config for native asset body. | `{required: true, len: 200}` +| `cta` | optional | Object | Config for native asset cta. | `{required: true, len: 20}` + +## Example + +### Banner Ads + +```javascript +var adUnits = [{ + code: 'banner-ad-div', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [{ + bidder: 'operaads', + params: { + placementId: 's12345678', + endpointId: 's12345678', + publisherId: 's12345678' + } + }] +}]; +``` + +### Video Ads + +```javascript +var adUnits = [{ + code: 'video-ad-div', + mediaTypes: { + video: { + context: 'instream', + playerSize: [640, 480], + mimes: ['video/mp4'], + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + playbackmethod: [2], + skip: 1 + } + }, + bids: [{ + bidder: 'operaads', + params: { + placementId: 's12345678', + endpointId: 's12345678', + publisherId: 's12345678' + } + }] +}]; +``` + +* For video ads, enable prebid cache. + +```javascript +pbjs.setConfig({ + cache: { + url: 'https://prebid.adnxs.com/pbc/v1/cache' + } +}); +``` + +### Native Ads + +```javascript +var adUnits = [{ + code: 'native-ad-div', + mediaTypes: { + native: { + title: { required: true, len: 75 }, + image: { required: true, sizes: [[300, 250]] }, + body: { len: 200 }, + sponsoredBy: { len: 20 } + } + }, + bids: [{ + bidder: 'operaads', + params: { + placementId: 's12345678', + endpointId: 's12345678', + publisherId: 's12345678' + } + }] +}]; +``` + +### User Ids + +Opera Ads Bid Adapter uses `sharedId`, `pubcid` or `tdid`, please config at least one. + +```javascript +pbjs.setConfig({ + ..., + userSync: { + userIds: [{ + name: 'sharedId', + storage: { + name: '_sharedID', // name of the 1st party cookie + type: 'cookie', + expires: 30 + } + }] + } +}); +``` diff --git a/test/spec/modules/operaadsBidAdapter_spec.js b/test/spec/modules/operaadsBidAdapter_spec.js new file mode 100644 index 00000000000..de1d94b4c15 --- /dev/null +++ b/test/spec/modules/operaadsBidAdapter_spec.js @@ -0,0 +1,220 @@ +import { expect } from 'chai'; +import { spec } from 'modules/operaadsBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; + +describe('Opera Ads Bid Adapter', function () { + describe('Test isBidRequestValid', function () { + it('undefined bid should return false', function () { + expect(spec.isBidRequestValid()).to.be.false; + }); + + it('null bid should return false', function () { + expect(spec.isBidRequestValid(null)).to.be.false; + }); + + it('bid.params should be set', function () { + expect(spec.isBidRequestValid({})).to.be.false; + }); + + it('bid.params.placementId should be set', function () { + expect(spec.isBidRequestValid({ + params: { endpointId: 'ep12345678', publisherId: 'pub12345678' } + })).to.be.false; + }); + + it('bid.params.publisherId should be set', function () { + expect(spec.isBidRequestValid({ + params: { placementId: 'ep12345678', endpointId: 'pub12345678' } + })).to.be.false; + }); + + it('valid bid should return true', function () { + expect(spec.isBidRequestValid({ + params: { placementId: 'ep12345678', endpointId: 'pub12345678', publisherId: 'pub12345678' } + })).to.be.true; + }); + }); + + describe('Test buildRequests', function () { + const bidderRequest = { + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + auctionStart: Date.now(), + bidderCode: 'myBidderCode', + bidderRequestId: '15246a574e859f', + refererInfo: { + referer: 'http://example.com', + stack: ['http://example.com'] + }, + userId: { + 'shareId': 'b06c5141-fe8f-4cdf-9d7d-54415490a917' + } + }; + + it('build request object', function () { + const bidRequests = [ + { + adUnitCode: 'test-div', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: '22c4871113f461', + bidder: 'operaads', + bidderRequestId: '15246a574e859f', + mediaTypes: {banner: {sizes: [[300, 250]]}}, + params: { + placementId: 's12345678', + publisherId: 'pub12345678', + endpointId: 'ep12345678' + } + } + ]; + + const reqs = spec.buildRequests(bidRequests, bidderRequest); + + expect(reqs).to.be.an('array'); + + for (const req of reqs) { + expect(req.method).to.equal('POST'); + expect(req.url).to.equal('https://s.adx.opera.com/ortb/v2/pub12345678?ep=ep12345678'); + + expect(req.options).to.be.an('object'); + expect(req.options.contentType).to.contain('application/json'); + expect(req.options.customHeaders).to.be.an('object'); + expect(req.options.customHeaders['x-openrtb-version']).to.equal(2.5); + + expect(req.data).to.be.a('string'); + } + }); + + it('currency in params should be used', function () { + const bidRequests = [ + { + adUnitCode: 'test-div', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: '22c4871113f461', + mediaTypes: {banner: {sizes: [[300, 250]]}}, + params: { + placementId: 's12345678', + publisherId: 'pub12345678', + endpointId: 'ep12345678', + currency: 'RMB' + } + } + ]; + + const reqs = spec.buildRequests(bidRequests, bidderRequest); + + expect(reqs).to.be.an('array'); + + for (const req of reqs) { + let data; + try { + data = JSON.parse(req.data); + } catch (e) { + data = {}; + } + + expect(data.cur).to.be.an('array').that.includes('RMB'); + } + }); + + it('bcat in params should be used', function() { + const bidRequests = [ + { + adUnitCode: 'test-div', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: '22c4871113f461', + mediaTypes: {banner: {sizes: [[300, 250]]}}, + params: { + placementId: 's12345678', + publisherId: 'pub12345678', + endpointId: 'ep12345678', + bcat: ['IAB1-1'] + } + } + ]; + + const reqs = spec.buildRequests(bidRequests, bidderRequest); + + expect(reqs).to.be.an('array'); + + for (const req of reqs) { + let data; + try { + data = JSON.parse(req.data); + } catch (e) { + data = {}; + } + + expect(data.bcat).to.be.an('array').that.includes('IAB1-1'); + } + }); + }); + + describe('Test adapter request', function () { + const adapter = newBidder(spec); + + it('adapter.callBids exists and is a function', function () { + expect(adapter.callBids).to.be.a('function'); + }); + }); + + describe('Test response interpretResponse', function () { + const serverResponse = { + body: { + 'id': 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + 'seatbid': [ + { + 'bid': [ + { + 'id': '003004d9c05c6bc7fec0', + 'impid': '22c4871113f461', + 'price': 1.04, + 'nurl': 'https://s.adx.opera.com/win?a=a5311273992064&burl=aHR0cHM6Ly93d3cub3BlcmEuY29tL2J1cmw%3D&cc=HK&cm=1&crid=0.49379027&dvt=PHONE&ext=_6Ux3PNfxKD5lYt5CVWDTM-TRx6sr__qxRadWTKvNcOIzec2BXxScDVZDKJPkeCOCdUW-0I7YwEsmixrPTT4r1mGH8-plpXh3ws4p0JhEtuvrGK3LJOwhJfT2pBvrMSY&iabCat=IAB9-31%2CIAB8&m=m5311273992833&pubId=pub5311274436800&s=s5323636048704&se=003004d9c05c6bc7fec0&srid=b06c5141-fe8f-4cdf-9d7d-54415490a917&u=68a99bb21f5855f2&ac=${AUCTION_CURRENCY}&ap=${AUCTION_PRICE}', + 'lurl': 'https://s.adx.opera.com/loss?a=a5311273992064&burl=aHR0cHM6Ly93d3cub3BlcmEuY29tL2J1cmw%3D&cc=HK&cm=1&crid=0.49379027&dvt=PHONE&ext=_6Ux3PNfxKD5lYt5CVWDTM-TRx6sr__qxRadWTKvNcOIzec2BXxScDVZDKJPkeCOCdUW-0I7YwEsmixrPTT4r1mGH8-plpXh3ws4p0JhEtuvrGK3LJOwhJfT2pBvrMSY&iabCat=IAB9-31%2CIAB8&m=m5311273992833&pubId=pub5311274436800&s=s5323636048704&se=003004d9c05c6bc7fec0&srid=b06c5141-fe8f-4cdf-9d7d-54415490a917&u=68a99bb21f5855f2&al=${AUCTION_LOSS}', + 'adm': "
\"\"
", + 'adomain': [ + 'opera.com', + 'www.algorx.cn' + ], + 'bundle': 'com.opera.mini.beta', + 'cid': '0.49379027', + 'crid': '0.49379027', + 'cat': [ + 'IAB9-31', + 'IAB8' + ], + 'language': 'EN', + 'h': 300, + 'w': 250, + 'exp': 500, + 'ext': {} + } + ], + 'seat': 'adv4199760017536' + } + ], + 'bidid': '003004d9c05c6bc7fec0', + 'cur': 'USD' + } + }; + + it('interpretResponse', function () { + const bidResponses = spec.interpretResponse(serverResponse, { + adUnitCode: 'test-div', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: '22c4871113f461', + bidder: 'operaads', + bidderRequestId: '15246a574e859f', + mediaTypes: {banner: {sizes: [[300, 250]]}}, + params: { + placementId: 's12345678', + publisherId: 'pub123456', + endpointId: 'ep1234566' + }, + src: 'client', + transactionId: '4781e6ac-93c4-42ba-86fe-ab5f278863cf' + }); + + expect(bidResponses).to.be.an('array').that.is.not.empty; + }); + }); +}); From 599217f5a46c31ed75386f701131fd7ce65b25e6 Mon Sep 17 00:00:00 2001 From: Xingwang Liao Date: Fri, 9 Jul 2021 10:42:52 +0800 Subject: [PATCH 2/9] fix(operaads): fix sharedId support --- modules/operaadsBidAdapter.js | 39 ++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/modules/operaadsBidAdapter.js b/modules/operaadsBidAdapter.js index dabae591275..d30b2a43a0b 100644 --- a/modules/operaadsBidAdapter.js +++ b/modules/operaadsBidAdapter.js @@ -216,7 +216,9 @@ function buildOpenRtbBidRequest(bidRequest, bidderRequest) { coppa: config.getConfig('coppa') ? 1 : 0, ext: {} }, - user: {} + user: { + id: getUserId(bidRequest) + } } const gdprConsent = utils.deepAccess(bidderRequest, 'gdprConsent'); @@ -230,19 +232,6 @@ function buildOpenRtbBidRequest(bidRequest, bidderRequest) { utils.deepSetValue(payload, 'regs.ext.us_privacy', uspConsent); } - let userId; - for (const idModule of ['sharedId', 'pubcid', 'tdid']) { - if ((userId = utils.deepAccess(bidRequest, `userId.${idModule}`))) { - break; - } - } - - if (!userId) { - userId = utils.generateUUID(); - } - - utils.deepSetValue(payload, 'user.id', userId); - const eids = utils.deepAccess(bidRequest, 'userIdAsEids', []); if (eids.length > 0) { utils.deepSetValue(payload, 'user.eids', eids); @@ -625,6 +614,28 @@ function mapNativeImage(image, type) { return img; } +/** + * Get user id from bid request. if no user id module used, return a new uuid. + * + * @param {BidRequest} bidRequest + * @returns {String} userId + */ +function getUserId(bidRequest) { + let sharedId = utils.deepAccess(bidRequest, 'userId.sharedid.id'); + if (sharedId) { + return sharedId; + } + + for (const idModule of ['pubcid', 'tdid']) { + let userId = utils.deepAccess(bidRequest, `userId.${idModule}`); + if (userId) { + return userId; + } + } + + return utils.generateUUID(); +} + /** * Get publisher domain * From dea9535261a22543943a7b23031506fa82c55d34 Mon Sep 17 00:00:00 2001 From: Xingwang Liao Date: Fri, 9 Jul 2021 12:54:02 +0800 Subject: [PATCH 3/9] chore(operaads): remove user sync support --- modules/operaadsBidAdapter.js | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/modules/operaadsBidAdapter.js b/modules/operaadsBidAdapter.js index d30b2a43a0b..2d3f764fe43 100644 --- a/modules/operaadsBidAdapter.js +++ b/modules/operaadsBidAdapter.js @@ -8,7 +8,6 @@ import { OUTSTREAM } from '../src/video.js'; const BIDDER_CODE = 'operaads'; const ENDPOINT = 'https://s.adx.opera.com/ortb/v2/'; -const USER_SYNC_ENDPOINT = 'https://t.adx.opera.com/pbs/sync'; const OUTSTREAM_RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; @@ -138,28 +137,7 @@ export const spec = { * @return {UserSync[]} The user syncs which should be dropped. */ getUserSyncs: function (syncOptions, serverResponses, gdprConsent, uspConsent) { - const syncs = []; - - const params = []; - if (gdprConsent) { - if (typeof gdprConsent.gdprApplies === 'boolean') { - params.push(`gdpr=${Number(gdprConsent.gdprApplies)}`); - } - params.push(`gdpr_consent=${encodeURIComponent(gdprConsent.consentString)}`); - } - - if (uspConsent) { - params.push(`us_privacy=${encodeURIComponent(uspConsent)}`); - } - - if (syncOptions.pixelEnabled && serverResponses.length > 0) { - syncs.push({ - type: 'image', - url: USER_SYNC_ENDPOINT + '?' + params.join('&') - }); - } - - return syncs; + return []; }, /** From 820c23bc51e127ac77fc915e3ef0930757a3348d Mon Sep 17 00:00:00 2001 From: Xingwang Liao Date: Fri, 9 Jul 2021 13:22:45 +0800 Subject: [PATCH 4/9] feat(operaads): no need to set width and height when native ad is requested --- modules/operaadsBidAdapter.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/operaadsBidAdapter.js b/modules/operaadsBidAdapter.js index 2d3f764fe43..982488c71f3 100644 --- a/modules/operaadsBidAdapter.js +++ b/modules/operaadsBidAdapter.js @@ -265,9 +265,6 @@ function buildBidResponse(bid, bidRequest, responseBody) { requestId: bid.impid, cpm: (parseFloat(bid.price) || 0).toFixed(2), currency: responseBody.cur || DEFAULT_CURRENCY, - ad: bid.adm, - width: bid.w, - height: bid.h, mediaType: mediaType, ttl: 300, creativeId: bid.crid || bid.id, @@ -289,11 +286,11 @@ function buildBidResponse(bid, bidRequest, responseBody) { const playerSize = utils.deepAccess(bidRequest, 'mediaTypes.video.playerSize', VIDEO_DEFAULTS.SIZE); const size = canonicalizeSizesArray(playerSize)[0]; + bidResponse.vastXml = bid.adm; + bidResponse.width = bid.w || size[0]; bidResponse.height = bid.h || size[1]; - bidResponse.vastXml = bid.adm; - const context = utils.deepAccess(bidRequest, 'mediaTypes.video.context'); // if outstream video, add a default render for it. @@ -316,6 +313,9 @@ function buildBidResponse(bid, bidRequest, responseBody) { } default: { bidResponse.ad = bid.adm; + + bidResponse.width = bid.w; + bidResponse.height = bid.h; } } return bidResponse; From 5986a04b274d60aac354756017440416b0584ce4 Mon Sep 17 00:00:00 2001 From: Xingwang Liao Date: Fri, 9 Jul 2021 14:52:13 +0800 Subject: [PATCH 5/9] fix(operaads): decode native icon url --- modules/operaadsBidAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/operaadsBidAdapter.js b/modules/operaadsBidAdapter.js index 982488c71f3..3890be44e14 100644 --- a/modules/operaadsBidAdapter.js +++ b/modules/operaadsBidAdapter.js @@ -371,7 +371,7 @@ function interpretNativeAd(nativeResponse) { case NATIVE_DEFAULTS.ASSET_ID.ICON: { if (asset.img) { native.icon = { - url: encodeURIComponent(asset.img.url), + url: decodeURIComponent(asset.img.url), width: asset.img.w, height: asset.img.h } From 65704f240f9221ee5bfed528e242da1e136ea9fb Mon Sep 17 00:00:00 2001 From: Xingwang Liao Date: Fri, 9 Jul 2021 19:47:36 +0800 Subject: [PATCH 6/9] test(operaads): add more test cases --- test/spec/modules/operaadsBidAdapter_spec.js | 596 ++++++++++++++++--- 1 file changed, 515 insertions(+), 81 deletions(-) diff --git a/test/spec/modules/operaadsBidAdapter_spec.js b/test/spec/modules/operaadsBidAdapter_spec.js index de1d94b4c15..75e201bf168 100644 --- a/test/spec/modules/operaadsBidAdapter_spec.js +++ b/test/spec/modules/operaadsBidAdapter_spec.js @@ -24,13 +24,19 @@ describe('Opera Ads Bid Adapter', function () { it('bid.params.publisherId should be set', function () { expect(spec.isBidRequestValid({ - params: { placementId: 'ep12345678', endpointId: 'pub12345678' } + params: { placementId: 's12345678', endpointId: 'ep12345678' } + })).to.be.false; + }); + + it('bid.params.endpointId should be set', function () { + expect(spec.isBidRequestValid({ + params: { placementId: 's12345678', publisherId: 'pub12345678' } })).to.be.false; }); it('valid bid should return true', function () { expect(spec.isBidRequestValid({ - params: { placementId: 'ep12345678', endpointId: 'pub12345678', publisherId: 'pub12345678' } + params: { placementId: 's12345678', endpointId: 'ep12345678', publisherId: 'pub12345678' } })).to.be.true; }); }); @@ -45,9 +51,12 @@ describe('Opera Ads Bid Adapter', function () { referer: 'http://example.com', stack: ['http://example.com'] }, - userId: { - 'shareId': 'b06c5141-fe8f-4cdf-9d7d-54415490a917' - } + gdprConsent: { + gdprApplies: true, + consentString: 'IwuyYwpjmnsauyYasIUWwe' + }, + uspConsent: 'Oush3@jmUw82has', + timeout: 3000 }; it('build request object', function () { @@ -58,7 +67,134 @@ describe('Opera Ads Bid Adapter', function () { bidId: '22c4871113f461', bidder: 'operaads', bidderRequestId: '15246a574e859f', - mediaTypes: {banner: {sizes: [[300, 250]]}}, + mediaTypes: { + banner: { sizes: [[300, 250]] } + }, + params: { + placementId: 's12345678', + publisherId: 'pub12345678', + endpointId: 'ep12345678' + } + }, + { + adUnitCode: 'test-native', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: '22c4871113f4622', + bidder: 'operaads', + bidderRequestId: '15246a574e859f', + mediaTypes: { + native: { + title: { + required: true, + len: 20, + }, + image: { + required: true, + sizes: [300, 250], + aspect_ratios: [{ + ratio_width: 1, + ratio_height: 1 + }] + }, + icon: { + required: true, + sizes: [60, 60], + aspect_ratios: [{ + ratio_width: 1, + ratio_height: 1 + }] + }, + sponsoredBy: { + required: true, + len: 20 + }, + body: { + required: true, + len: 140 + }, + cta: { + required: true, + len: 20, + } + } + }, + params: { + placementId: 's12345678', + publisherId: 'pub12345678', + endpointId: 'ep12345678' + } + }, + { + adUnitCode: 'test-native2', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: '22c4871113f4632', + bidder: 'operaads', + bidderRequestId: '15246a574e859f', + mediaTypes: { + native: { + title: {}, + image: {}, + icon: {}, + sponsoredBy: {}, + body: {}, + cta: {} + } + }, + params: { + placementId: 's12345678', + publisherId: 'pub12345678', + endpointId: 'ep12345678' + } + }, + { + adUnitCode: 'test-native3', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: '22c4871113f4633', + bidder: 'operaads', + bidderRequestId: '15246a574e859f', + mediaTypes: { + native: {}, + }, + params: { + placementId: 's12345678', + publisherId: 'pub12345678', + endpointId: 'ep12345678' + } + }, + { + adUnitCode: 'test-video', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: '22c4871113f4623', + bidder: 'operaads', + bidderRequestId: '15246a574e859f', + mediaTypes: { + video: { + context: 'outstream', + playerSize: [[640, 480]], + mimes: ['video/mp4'], + protocols: [2, 3, 5, 6], + startdelay: 0, + skip: 1, + playbackmethod: [1, 2, 3, 4], + delivery: [1], + api: [1, 2, 5], + } + }, + params: { + placementId: 's12345678', + publisherId: 'pub12345678', + endpointId: 'ep12345678' + } + }, + { + adUnitCode: 'test-video', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: '22c4871113f4643', + bidder: 'operaads', + bidderRequestId: '15246a574e859f', + mediaTypes: { + video: {} + }, params: { placementId: 's12345678', publisherId: 'pub12345678', @@ -67,20 +203,65 @@ describe('Opera Ads Bid Adapter', function () { } ]; - const reqs = spec.buildRequests(bidRequests, bidderRequest); + let reqs; - expect(reqs).to.be.an('array'); + expect(function () { + reqs = spec.buildRequests(bidRequests, bidderRequest); + }).to.not.throw(); + + expect(reqs).to.be.an('array').that.have.lengthOf(bidRequests.length); + + for (let i = 0, len = reqs.length; i < len; i++) { + const req = reqs[i]; + const bidRequest = bidRequests[i]; - for (const req of reqs) { expect(req.method).to.equal('POST'); - expect(req.url).to.equal('https://s.adx.opera.com/ortb/v2/pub12345678?ep=ep12345678'); + expect(req.url).to.equal('https://s.adx.opera.com/ortb/v2/' + + bidRequest.params.publisherId + '?ep=' + bidRequest.params.endpointId); expect(req.options).to.be.an('object'); expect(req.options.contentType).to.contain('application/json'); expect(req.options.customHeaders).to.be.an('object'); expect(req.options.customHeaders['x-openrtb-version']).to.equal(2.5); + expect(req.originalBidRequest).to.equal(bidRequest); + expect(req.data).to.be.a('string'); + + let requestData; + expect(function () { + requestData = JSON.parse(req.data); + }).to.not.throw(); + + expect(requestData.id).to.equal(bidderRequest.auctionId); + expect(requestData.tmax).to.equal(bidderRequest.timeout); + expect(requestData.test).to.equal(0); + expect(requestData.imp).to.be.an('array').that.have.lengthOf(1); + expect(requestData.device).to.be.an('object'); + expect(requestData.site).to.be.an('object'); + expect(requestData.site.id).to.equal(bidRequest.params.publisherId); + expect(requestData.site.domain).to.not.be.empty; + expect(requestData.site.page).to.equal(bidderRequest.refererInfo.referer); + expect(requestData.at).to.equal(1); + expect(requestData.bcat).to.be.an('array').that.is.empty; + expect(requestData.cur).to.be.an('array').that.not.be.empty; + expect(requestData.user).to.be.an('object'); + + let impItem = requestData.imp[0]; + expect(impItem).to.be.an('object'); + expect(impItem.id).to.equal(bidRequest.bidId); + expect(impItem.tagid).to.equal(bidRequest.params.placementId); + expect(impItem.bidfloor).to.be.a('number'); + + if (bidRequest.mediaTypes.banner) { + expect(impItem.banner).to.be.an('object'); + } else if (bidRequest.mediaTypes.native) { + expect(impItem.native).to.be.an('object'); + } else if (bidRequest.mediaTypes.video) { + expect(impItem.video).to.be.an('object'); + } else { + expect.fail('should not happen'); + } } }); @@ -90,7 +271,7 @@ describe('Opera Ads Bid Adapter', function () { adUnitCode: 'test-div', auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', bidId: '22c4871113f461', - mediaTypes: {banner: {sizes: [[300, 250]]}}, + mediaTypes: { banner: { sizes: [[300, 250]] } }, params: { placementId: 's12345678', publisherId: 'pub12345678', @@ -102,27 +283,25 @@ describe('Opera Ads Bid Adapter', function () { const reqs = spec.buildRequests(bidRequests, bidderRequest); - expect(reqs).to.be.an('array'); + expect(reqs).to.be.an('array').that.have.lengthOf(1); for (const req of reqs) { - let data; - try { - data = JSON.parse(req.data); - } catch (e) { - data = {}; - } + let requestData; + expect(function () { + requestData = JSON.parse(req.data); + }).to.not.throw(); - expect(data.cur).to.be.an('array').that.includes('RMB'); + expect(requestData.cur).to.be.an('array').that.includes('RMB'); } }); - it('bcat in params should be used', function() { + it('bcat in params should be used', function () { const bidRequests = [ { adUnitCode: 'test-div', auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', bidId: '22c4871113f461', - mediaTypes: {banner: {sizes: [[300, 250]]}}, + mediaTypes: { banner: { sizes: [[300, 250]] } }, params: { placementId: 's12345678', publisherId: 'pub12345678', @@ -134,19 +313,120 @@ describe('Opera Ads Bid Adapter', function () { const reqs = spec.buildRequests(bidRequests, bidderRequest); - expect(reqs).to.be.an('array'); + expect(reqs).to.be.an('array').that.have.lengthOf(1); for (const req of reqs) { - let data; - try { - data = JSON.parse(req.data); - } catch (e) { - data = {}; - } + let requestData; + expect(function () { + requestData = JSON.parse(req.data); + }).to.not.throw(); - expect(data.bcat).to.be.an('array').that.includes('IAB1-1'); + expect(requestData.bcat).to.be.an('array').that.includes('IAB1-1'); } }); + + it('sharedid should be used', function () { + const bidRequests = [{ + adUnitCode: 'test-div', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: '22c4871113f461', + bidder: 'operaads', + bidderRequestId: '15246a574e859f', + mediaTypes: { + banner: { sizes: [[300, 250]] } + }, + params: { + placementId: 's12345678', + publisherId: 'pub12345678', + endpointId: 'ep12345678' + }, + userId: { + sharedid: { + id: '01F5DEQW731Q2VKT031KBKMW5W' + } + }, + userIdAsEids: [{ + source: 'pubcid.org', + uids: [{ + atype: 1, + id: '01F5DEQW731Q2VKT031KBKMW5W' + }] + }] + }]; + + const reqs = spec.buildRequests(bidRequests, bidderRequest); + + let requestData; + expect(function () { + requestData = JSON.parse(reqs[0].data); + }).to.not.throw(); + + expect(requestData.user.id).to.equal(bidRequests[0].userId.sharedid.id); + }); + + it('pubcid should be used when sharedid is empty', function () { + const bidRequests = [{ + adUnitCode: 'test-div', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: '22c4871113f461', + bidder: 'operaads', + bidderRequestId: '15246a574e859f', + mediaTypes: { + banner: { sizes: [[300, 250]] } + }, + params: { + placementId: 's12345678', + publisherId: 'pub12345678', + endpointId: 'ep12345678' + }, + userId: { + 'pubcid': '21F5DEQW731Q2VKT031KBKMW5W' + }, + userIdAsEids: [{ + source: 'pubcid.org', + uids: [{ + atype: 1, + id: '21F5DEQW731Q2VKT031KBKMW5W' + }] + }] + }]; + + const reqs = spec.buildRequests(bidRequests, bidderRequest); + + let requestData; + expect(function () { + requestData = JSON.parse(reqs[0].data); + }).to.not.throw(); + + expect(requestData.user.id).to.equal(bidRequests[0].userId.pubcid); + }); + + it('random uid will be generate when userId is empty', function () { + const bidRequests = [{ + adUnitCode: 'test-div', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: '22c4871113f461', + bidder: 'operaads', + bidderRequestId: '15246a574e859f', + mediaTypes: { + banner: { sizes: [[300, 250]] } + }, + params: { + placementId: 's12345678', + publisherId: 'pub12345678', + endpointId: 'ep12345678' + } + }]; + + const reqs = spec.buildRequests(bidRequests, bidderRequest); + + let requestData; + expect(function () { + requestData = JSON.parse(reqs[0].data); + }).to.not.throw(); + + expect(requestData.user.id).to.not.be.empty; + }) }); describe('Test adapter request', function () { @@ -158,63 +438,217 @@ describe('Opera Ads Bid Adapter', function () { }); describe('Test response interpretResponse', function () { - const serverResponse = { - body: { - 'id': 'b06c5141-fe8f-4cdf-9d7d-54415490a917', - 'seatbid': [ - { - 'bid': [ - { - 'id': '003004d9c05c6bc7fec0', - 'impid': '22c4871113f461', - 'price': 1.04, - 'nurl': 'https://s.adx.opera.com/win?a=a5311273992064&burl=aHR0cHM6Ly93d3cub3BlcmEuY29tL2J1cmw%3D&cc=HK&cm=1&crid=0.49379027&dvt=PHONE&ext=_6Ux3PNfxKD5lYt5CVWDTM-TRx6sr__qxRadWTKvNcOIzec2BXxScDVZDKJPkeCOCdUW-0I7YwEsmixrPTT4r1mGH8-plpXh3ws4p0JhEtuvrGK3LJOwhJfT2pBvrMSY&iabCat=IAB9-31%2CIAB8&m=m5311273992833&pubId=pub5311274436800&s=s5323636048704&se=003004d9c05c6bc7fec0&srid=b06c5141-fe8f-4cdf-9d7d-54415490a917&u=68a99bb21f5855f2&ac=${AUCTION_CURRENCY}&ap=${AUCTION_PRICE}', - 'lurl': 'https://s.adx.opera.com/loss?a=a5311273992064&burl=aHR0cHM6Ly93d3cub3BlcmEuY29tL2J1cmw%3D&cc=HK&cm=1&crid=0.49379027&dvt=PHONE&ext=_6Ux3PNfxKD5lYt5CVWDTM-TRx6sr__qxRadWTKvNcOIzec2BXxScDVZDKJPkeCOCdUW-0I7YwEsmixrPTT4r1mGH8-plpXh3ws4p0JhEtuvrGK3LJOwhJfT2pBvrMSY&iabCat=IAB9-31%2CIAB8&m=m5311273992833&pubId=pub5311274436800&s=s5323636048704&se=003004d9c05c6bc7fec0&srid=b06c5141-fe8f-4cdf-9d7d-54415490a917&u=68a99bb21f5855f2&al=${AUCTION_LOSS}', - 'adm': "
\"\"
", - 'adomain': [ - 'opera.com', - 'www.algorx.cn' - ], - 'bundle': 'com.opera.mini.beta', - 'cid': '0.49379027', - 'crid': '0.49379027', - 'cat': [ - 'IAB9-31', - 'IAB8' - ], - 'language': 'EN', - 'h': 300, - 'w': 250, - 'exp': 500, - 'ext': {} - } - ], - 'seat': 'adv4199760017536' - } - ], - 'bidid': '003004d9c05c6bc7fec0', - 'cur': 'USD' - } - }; + it('Test banner interpretResponse', function () { + const serverResponse = { + body: { + 'id': 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + 'seatbid': [ + { + 'bid': [ + { + 'id': '003004d9c05c6bc7fec0', + 'impid': '22c4871113f461', + 'price': 1.04, + 'nurl': 'https://s.adx.opera.com/win', + 'lurl': 'https://s.adx.opera.com/loss', + 'adm': '', + 'adomain': [ + 'opera.com', + ], + 'cid': '0.49379027', + 'crid': '0.49379027', + 'cat': [ + 'IAB9-31', + 'IAB8' + ], + 'language': 'EN', + 'h': 300, + 'w': 250, + 'exp': 500, + 'ext': {} + } + ], + 'seat': 'adv4199760017536' + } + ], + 'bidid': '003004d9c05c6bc7fec0', + 'cur': 'USD' + } + }; - it('interpretResponse', function () { const bidResponses = spec.interpretResponse(serverResponse, { - adUnitCode: 'test-div', - auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', - bidId: '22c4871113f461', - bidder: 'operaads', - bidderRequestId: '15246a574e859f', - mediaTypes: {banner: {sizes: [[300, 250]]}}, - params: { - placementId: 's12345678', - publisherId: 'pub123456', - endpointId: 'ep1234566' - }, - src: 'client', - transactionId: '4781e6ac-93c4-42ba-86fe-ab5f278863cf' + originalBidRequest: { + adUnitCode: 'test-div', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: '22c4871113f461', + bidder: 'operaads', + bidderRequestId: '15246a574e859f', + mediaTypes: { banner: { sizes: [[300, 250]] } }, + params: { + placementId: 's12345678', + publisherId: 'pub123456', + endpointId: 'ep1234566' + }, + src: 'client', + transactionId: '4781e6ac-93c4-42ba-86fe-ab5f278863cf' + } }); expect(bidResponses).to.be.an('array').that.is.not.empty; }); + + it('Test video interpretResponse', function () { + const serverResponse = { + body: { + 'id': 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + 'seatbid': [ + { + 'bid': [ + { + 'id': '003004d9c05c6bc7fec0', + 'impid': '22c4871113f461', + 'price': 1.04, + 'nurl': 'https://s.adx.opera.com/win', + 'lurl': 'https://s.adx.opera.com/loss', + 'adm': 'Static VAST TemplateStatic VAST Taghttp://example.com/pixel.gif?asi=[ADSERVINGID]00:00:08http://example.com/pixel.gifhttp://example.com/pixel.gifhttp://example.com/pixel.gifhttp://example.com/pixel.gifhttp://example.com/pixel.gifhttp://example.com/pixel.gifhttp://example.com/pixel.gifhttp://example.com/pixel.gifhttp://www.jwplayer.com/http://example.com/pixel.gif?r=[REGULATIONS]&gdpr=[GDPRCONSENT]&pu=[PAGEURL]&da=[DEVICEUA] http://example.com/uploads/myPrerollVideo.mp4 https://example.com/adchoices-sm.pnghttps://sample-url.com', + 'adomain': [ + 'opera.com', + ], + 'cid': '0.49379027', + 'crid': '0.49379027', + 'cat': [ + 'IAB9-31', + 'IAB8' + ], + 'language': 'EN', + 'h': 300, + 'w': 250, + 'exp': 500, + 'ext': {} + } + ], + 'seat': 'adv4199760017536' + } + ], + 'bidid': '003004d9c05c6bc7fec0', + 'cur': 'USD' + } + }; + + const bidResponses = spec.interpretResponse(serverResponse, { + originalBidRequest: { + adUnitCode: 'test-div', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: '22c4871113f461', + bidder: 'operaads', + bidderRequestId: '15246a574e859f', + mediaTypes: { video: { context: 'outstream' } }, + params: { + placementId: 's12345678', + publisherId: 'pub123456', + endpointId: 'ep1234566' + }, + src: 'client', + transactionId: '4781e6ac-93c4-42ba-86fe-ab5f278863cf' + } + }); + + expect(bidResponses).to.be.an('array').that.is.not.empty; + }); + + it('Test native interpretResponse', function () { + const serverResponse = { + body: { + 'id': 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + 'seatbid': [ + { + 'bid': [ + { + 'id': '003004d9c05c6bc7fec0', + 'impid': '22c4871113f461', + 'price': 1.04, + 'nurl': 'https://s.adx.opera.com/win', + 'lurl': 'https://s.adx.opera.com/loss', + 'adm': '{"native":{"ver":"1.1","assets":[{"id":1,"required":1,"title":{"text":"The first personal browser"}},{"id":2,"required":1,"img":{"url":"https://res.adx.opera.com/xxx.png","w":720,"h":1280}},{"id":5,"required":1,"data":{"value":"Opera","len":5}}],"link":{"url":"https://www.opera.com/mobile/opera","clicktrackers":["https://thirdpart-click.tracker.com","https://t-odx.op-mobile.opera.com/click"]}}}', + 'adomain': [ + 'opera.com', + ], + 'cid': '0.49379027', + 'crid': '0.49379027', + 'cat': [ + 'IAB9-31', + 'IAB8' + ], + 'language': 'EN', + 'h': 300, + 'w': 250, + 'exp': 500, + 'ext': {} + } + ], + 'seat': 'adv4199760017536' + } + ], + 'bidid': '003004d9c05c6bc7fec0', + 'cur': 'USD' + } + }; + + const bidResponses = spec.interpretResponse(serverResponse, { + originalBidRequest: { + adUnitCode: 'test-div', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: '22c4871113f461', + bidder: 'operaads', + bidderRequestId: '15246a574e859f', + mediaTypes: { native: { } }, + params: { + placementId: 's12345678', + publisherId: 'pub123456', + endpointId: 'ep1234566' + }, + src: 'client', + transactionId: '4781e6ac-93c4-42ba-86fe-ab5f278863cf' + } + }); + + expect(bidResponses).to.be.an('array').that.is.not.empty; + }); + + it('Test empty server response', function () { + const bidResponses = spec.interpretResponse({}, {}); + + expect(bidResponses).to.be.an('array').that.is.empty; + }); + + it('Test empty bid response', function () { + const bidResponses = spec.interpretResponse({ body: { seatbid: null } }, {}); + + expect(bidResponses).to.be.an('array').that.is.empty; + }); + }); + + describe('Test getUserSyncs', function () { + it('getUserSyncs should return empty array', function () { + expect(spec.getUserSyncs()).to.be.an('array').that.is.empty; + }); + }); + + describe('Test onTimeout', function () { + it('onTimeout should not throw', function () { + expect(spec.onTimeout()).to.not.throw; + }); + }); + + describe('Test onBidWon', function () { + it('onBidWon should not throw', function () { + expect(spec.onTimeout()).to.not.throw; + }); + }); + + describe('Test onSetTargeting', function () { + it('onSetTargeting should not throw', function () { + expect(spec.onTimeout()).to.not.throw; + }); }); }); From 0a5b893fbbe2685ce594709b9a778f6be473ea4d Mon Sep 17 00:00:00 2001 From: Xingwang Liao Date: Mon, 12 Jul 2021 18:33:57 +0800 Subject: [PATCH 7/9] fix(operaads): fix native response parse --- modules/operaadsBidAdapter.js | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/modules/operaadsBidAdapter.js b/modules/operaadsBidAdapter.js index 3890be44e14..30ac0057e31 100644 --- a/modules/operaadsBidAdapter.js +++ b/modules/operaadsBidAdapter.js @@ -248,11 +248,13 @@ function buildBidResponse(bid, bidRequest, responseBody) { } else { let markup; try { - markup = JSON.parse(bid.adm.replace(/\\/g, '')); + markup = JSON.parse(bid.adm); } catch (e) { markup = null; } + // OpenRtb Markup Response Object + // https://www.iab.com/wp-content/uploads/2016/03/OpenRTB-Native-Ads-Specification-1-1_2016.pdf#5.1 if (markup && utils.isPlainObject(markup.native)) { mediaType = NATIVE; nativeResponse = markup.native; @@ -330,21 +332,24 @@ function buildBidResponse(bid, bidRequest, responseBody) { function interpretNativeAd(nativeResponse) { const native = {}; + // OpenRtb Link Object + // https://www.iab.com/wp-content/uploads/2016/03/OpenRTB-Native-Ads-Specification-1-1_2016.pdf#5.7 const clickUrl = utils.deepAccess(nativeResponse, 'link.url'); - if (clickUrl) { + if (clickUrl && utils.isStr(clickUrl)) { native.clickUrl = decodeURIComponent(clickUrl); } - if (nativeResponse.imptrackers && utils.isArray(nativeResponse.imptrackers)) { - native.impressionTrackers = nativeResponse.imptrackers.map(url => decodeURIComponent(url)); + const clickTrackers = utils.deepAccess(nativeResponse, 'link.clicktrackers'); + if (clickTrackers && utils.isArray(clickTrackers)) { + native.clickTrackers = clickTrackers.map(url => decodeURIComponent(url)); } - if (nativeResponse.clicktrackers && utils.isArray(nativeResponse.clicktrackers)) { - native.clickTrackers = nativeResponse.clicktrackers.map(url => decodeURIComponent(url)); + if (nativeResponse.imptrackers && utils.isArray(nativeResponse.imptrackers)) { + native.impressionTrackers = nativeResponse.imptrackers.map(url => decodeURIComponent(url)); } - if (nativeResponse.jstracker && utils.isArray(nativeResponse.jstracker)) { - native.jstracker = nativeResponse.jstracker.map(url => decodeURIComponent(url)); + if (nativeResponse.jstracker && utils.isStr(nativeResponse.jstracker)) { + native.javascriptTrackers = [nativeResponse.jstracker]; } let assets; From efd1ae293769e38632288580a90ed4bee0f20d23 Mon Sep 17 00:00:00 2001 From: Xingwang Liao Date: Mon, 12 Jul 2021 19:21:56 +0800 Subject: [PATCH 8/9] feat(operaads): track bid won --- modules/operaadsBidAdapter.js | 52 ++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/modules/operaadsBidAdapter.js b/modules/operaadsBidAdapter.js index 30ac0057e31..85ba25c1404 100644 --- a/modules/operaadsBidAdapter.js +++ b/modules/operaadsBidAdapter.js @@ -152,7 +152,26 @@ export const spec = { * * @param {Bid} bid The bid that won the auction */ - onBidWon: function (bid) { }, + onBidWon: function (bid) { + if (!bid || !utils.isStr(bid.nurl)) { + return; + } + + let winCpm, winCurr; + if (Object.prototype.hasOwnProperty.call(bid, 'originalCpm')) { + winCpm = bid.originalCpm; + winCurr = bid.originalCurrency; + } else { + winCpm = bid.cpm; + winCurr = bid.currency; + } + + utils.triggerPixel( + bid.nurl + .replace(/\$\{AUCTION_PRICE\}/g, winCpm) + .replace(/\$\{AUCTION_CURRENCY\}/g, winCurr) + ); + }, /** * Register bidder specific code, which will execute when the adserver targeting has been set for a bid from this bidder @@ -261,16 +280,21 @@ function buildBidResponse(bid, bidRequest, responseBody) { } } + const currency = responseBody.cur || DEFAULT_CURRENCY; + const cpm = (parseFloat(bid.price) || 0).toFixed(2); + const categories = utils.deepAccess(bid, 'cat', []); const bidResponse = { requestId: bid.impid, - cpm: (parseFloat(bid.price) || 0).toFixed(2), - currency: responseBody.cur || DEFAULT_CURRENCY, + cpm: cpm, + currency: currency, mediaType: mediaType, ttl: 300, creativeId: bid.crid || bid.id, netRevenue: NET_REVENUE, + nurl: bid.nurl, + lurl: bid.lurl, meta: { mediaType: mediaType, primaryCatId: categories[0], @@ -310,7 +334,7 @@ function buildBidResponse(bid, bidRequest, responseBody) { break; } case NATIVE: { - bidResponse.native = interpretNativeAd(nativeResponse); + bidResponse.native = interpretNativeAd(nativeResponse, currency, cpm); break; } default: { @@ -327,9 +351,11 @@ function buildBidResponse(bid, bidRequest, responseBody) { * Convert OpenRtb native response to bid native object. * * @param {OpenRtbNativeResponse} nativeResponse + * @param {String} currency + * @param {String} cpm * @returns {BidNative} native */ -function interpretNativeAd(nativeResponse) { +function interpretNativeAd(nativeResponse, currency, cpm) { const native = {}; // OpenRtb Link Object @@ -341,11 +367,23 @@ function interpretNativeAd(nativeResponse) { const clickTrackers = utils.deepAccess(nativeResponse, 'link.clicktrackers'); if (clickTrackers && utils.isArray(clickTrackers)) { - native.clickTrackers = clickTrackers.map(url => decodeURIComponent(url)); + native.clickTrackers = clickTrackers + .filter(Boolean) + .map( + url => decodeURIComponent(url) + .replace(/\$\{AUCTION_PRICE\}/g, cpm) + .replace(/\$\{AUCTION_CURRENCY\}/g, currency) + ); } if (nativeResponse.imptrackers && utils.isArray(nativeResponse.imptrackers)) { - native.impressionTrackers = nativeResponse.imptrackers.map(url => decodeURIComponent(url)); + native.impressionTrackers = nativeResponse.imptrackers + .filter(Boolean) + .map( + url => decodeURIComponent(url) + .replace(/\$\{AUCTION_PRICE\}/g, cpm) + .replace(/\$\{AUCTION_CURRENCY\}/g, currency) + ); } if (nativeResponse.jstracker && utils.isStr(nativeResponse.jstracker)) { From 0295c6310015e9a5f7b6cf81fe42fa5c50ed584c Mon Sep 17 00:00:00 2001 From: Xingwang Liao Date: Mon, 12 Jul 2021 19:43:11 +0800 Subject: [PATCH 9/9] test(operaads): update test cases --- test/spec/modules/operaadsBidAdapter_spec.js | 50 ++++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/test/spec/modules/operaadsBidAdapter_spec.js b/test/spec/modules/operaadsBidAdapter_spec.js index 75e201bf168..92e51c5473d 100644 --- a/test/spec/modules/operaadsBidAdapter_spec.js +++ b/test/spec/modules/operaadsBidAdapter_spec.js @@ -1,6 +1,7 @@ import { expect } from 'chai'; import { spec } from 'modules/operaadsBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from 'src/mediaTypes.js'; describe('Opera Ads Bid Adapter', function () { describe('Test isBidRequestValid', function () { @@ -495,6 +496,29 @@ describe('Opera Ads Bid Adapter', function () { }); expect(bidResponses).to.be.an('array').that.is.not.empty; + + const bid = serverResponse.body.seatbid[0].bid[0]; + const bidResponse = bidResponses[0]; + + expect(bidResponse.mediaType).to.equal(BANNER); + expect(bidResponse.requestId).to.equal(bid.impid); + expect(bidResponse.cpm).to.equal(parseFloat(bid.price).toFixed(2)) + expect(bidResponse.currency).to.equal(serverResponse.body.cur); + expect(bidResponse.creativeId).to.equal(bid.crid || bid.id); + expect(bidResponse.netRevenue).to.be.true; + expect(bidResponse.nurl).to.equal(bid.nurl); + expect(bidResponse.lurl).to.equal(bid.lurl); + + expect(bidResponse.meta).to.be.an('object'); + expect(bidResponse.meta.mediaType).to.equal(BANNER); + expect(bidResponse.meta.primaryCatId).to.equal('IAB9-31'); + expect(bidResponse.meta.secondaryCatIds).to.deep.equal(['IAB8']); + expect(bidResponse.meta.advertiserDomains).to.deep.equal(bid.adomain); + expect(bidResponse.meta.clickUrl).to.equal(bid.adomain[0]); + + expect(bidResponse.ad).to.equal(bid.adm); + expect(bidResponse.width).to.equal(bid.w); + expect(bidResponse.height).to.equal(bid.h); }); it('Test video interpretResponse', function () { @@ -554,6 +578,17 @@ describe('Opera Ads Bid Adapter', function () { }); expect(bidResponses).to.be.an('array').that.is.not.empty; + + const bid = serverResponse.body.seatbid[0].bid[0]; + const bidResponse = bidResponses[0]; + + expect(bidResponse.mediaType).to.equal(VIDEO); + expect(bidResponse.vastXml).to.equal(bid.adm); + expect(bidResponse.width).to.equal(bid.w); + expect(bidResponse.height).to.equal(bid.h); + + expect(bidResponse.adResponse).to.be.an('object'); + expect(bidResponse.renderer).to.be.an('object'); }); it('Test native interpretResponse', function () { @@ -569,7 +604,7 @@ describe('Opera Ads Bid Adapter', function () { 'price': 1.04, 'nurl': 'https://s.adx.opera.com/win', 'lurl': 'https://s.adx.opera.com/loss', - 'adm': '{"native":{"ver":"1.1","assets":[{"id":1,"required":1,"title":{"text":"The first personal browser"}},{"id":2,"required":1,"img":{"url":"https://res.adx.opera.com/xxx.png","w":720,"h":1280}},{"id":5,"required":1,"data":{"value":"Opera","len":5}}],"link":{"url":"https://www.opera.com/mobile/opera","clicktrackers":["https://thirdpart-click.tracker.com","https://t-odx.op-mobile.opera.com/click"]}}}', + 'adm': '{"native":{"ver":"1.1","assets":[{"id":1,"required":1,"title":{"text":"The first personal browser"}},{"id":2,"required":1,"img":{"url":"https://res.adx.opera.com/xxx.png","w":720,"h":1280}},{"id":3,"required":1,"img":{"url":"https://res.adx.opera.com/xxx.png","w":60,"h":60}},{"id":4,"required":1,"data":{"value":"Download Opera","len":14}},{"id":5,"required":1,"data":{"value":"Opera","len":5}},{"id":6,"required":1,"data":{"value":"Download","len":8}}],"link":{"url":"https://www.opera.com/mobile/opera","clicktrackers":["https://thirdpart-click.tracker.com","https://t-odx.op-mobile.opera.com/click"]},"imptrackers":["https://thirdpart-imp.tracker.com","https://t-odx.op-mobile.opera.com/impr"],"jstracker":""}}', 'adomain': [ 'opera.com', ], @@ -613,6 +648,15 @@ describe('Opera Ads Bid Adapter', function () { }); expect(bidResponses).to.be.an('array').that.is.not.empty; + + const bidResponse = bidResponses[0]; + + expect(bidResponse.mediaType).to.equal(NATIVE) + expect(bidResponse.native).to.be.an('object'); + expect(bidResponse.native.clickUrl).is.not.empty; + expect(bidResponse.native.clickTrackers).to.have.lengthOf(2); + expect(bidResponse.native.impressionTrackers).to.have.lengthOf(2); + expect(bidResponse.native.javascriptTrackers).to.have.lengthOf(1); }); it('Test empty server response', function () { @@ -642,13 +686,13 @@ describe('Opera Ads Bid Adapter', function () { describe('Test onBidWon', function () { it('onBidWon should not throw', function () { - expect(spec.onTimeout()).to.not.throw; + expect(spec.onBidWon({nurl: '#', originalCpm: '1.04', currency: 'USD'})).to.not.throw; }); }); describe('Test onSetTargeting', function () { it('onSetTargeting should not throw', function () { - expect(spec.onTimeout()).to.not.throw; + expect(spec.onSetTargeting()).to.not.throw; }); }); });