From 7116c9cc05f001834483d01b481a23b2cc199510 Mon Sep 17 00:00:00 2001 From: mikiz <31058500+mikizi@users.noreply.github.com> Date: Thu, 26 May 2022 19:59:14 +0300 Subject: [PATCH] Taboola bid adapter: initial release (#8483) * create taboola adapter * create taboola adapter md * taboolaBidAdapter.js - small fixes taboolaBidAdapter_spec.js - new UT * taboolaBidAdapter.js - small fixes taboolaBidAdapter_spec.js - new UT * update the md * update the Maintainer email * * update MD page * refactor code for better readability * small fix in UT * * add privacy to the request builder * add relevant Ut * small fixes in UT * * code refactoring + add more accurate way to get page url and referer * add relevant Ut * small fixes in md * * code refactoring + gte user id * add relevant Ut * small fixes * * code refactoring + gte user id * add relevant Ut * small fixes * * update end point url * update UT * Update banner End point structure * small fixes + update epi url * remove the destruction from the bidResponse property * (update the unit tests) remove the destruction from the bidResponse property * fix tests * fix tests - run stubs on each test * rerun because of another adapter flaky test * rerun because of another adapter flaky test * fix cors issue, switch between height, width position * update badv, bcat to be based in the ortb2 to support prebid 7 new protocols + update Ut * retry run circleci * retry run circleci * pull from upstream update md (placement + pub ) * update badv, bcat UT * rerun build * rerun build * support storageAllowed restriction on unit tests for prebid 7 * add it also to the aftereach * add it also to the aftereach --- modules/taboolaBidAdapter.js | 265 ++++++++++++ modules/taboolaBidAdapter.md | 49 +++ test/spec/modules/taboolaBidAdapter_spec.js | 455 ++++++++++++++++++++ 3 files changed, 769 insertions(+) create mode 100644 modules/taboolaBidAdapter.js create mode 100644 modules/taboolaBidAdapter.md create mode 100644 test/spec/modules/taboolaBidAdapter_spec.js diff --git a/modules/taboolaBidAdapter.js b/modules/taboolaBidAdapter.js new file mode 100644 index 00000000000..f93dd39a484 --- /dev/null +++ b/modules/taboolaBidAdapter.js @@ -0,0 +1,265 @@ +'use strict'; + +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER} from '../src/mediaTypes.js'; +import {config} from '../src/config.js'; +import {getWindowSelf, getWindowTop} from '../src/utils.js' +import {getStorageManager} from '../src/storageManager.js'; + +const BIDDER_CODE = 'taboola'; +const GVLID = 42; +const CURRENCY = 'USD'; +export const END_POINT_URL = 'http://hb.bidder.taboola.com/TaboolaHBOpenRTBRequestHandlerServlet'; +const USER_ID = 'user-id'; +const STORAGE_KEY = `taboola global:${USER_ID}`; +const COOKIE_KEY = 'trc_cookie_storage'; + +/** + * extract User Id by that order: + * 1. local storage + * 2. first party cookie + * 3. rendered trc + * 4. new user set it to 0 + */ +export const userData = { + storageManager: getStorageManager({gvlid: GVLID, bidderCode: BIDDER_CODE}), + getUserId: () => { + const {getFromLocalStorage, getFromCookie, getFromTRC} = userData; + + try { + return getFromLocalStorage() || getFromCookie() || getFromTRC(); + } catch (ex) { + return 0; + } + }, + getFromCookie() { + const {cookiesAreEnabled, getCookie} = userData.storageManager; + if (cookiesAreEnabled()) { + const cookieData = getCookie(COOKIE_KEY); + const userId = userData.getCookieDataByKey(cookieData, USER_ID); + if (userId) { + return userId; + } + } + }, + getCookieDataByKey(cookieData, key) { + const [, value = ''] = cookieData.split(`${key}=`) + return value; + }, + getFromLocalStorage() { + const {hasLocalStorage, localStorageIsEnabled, getDataFromLocalStorage} = userData.storageManager; + + if (hasLocalStorage() && localStorageIsEnabled()) { + return getDataFromLocalStorage(STORAGE_KEY); + } + }, + getFromTRC() { + return window.TRC ? window.TRC.user_id : 0; + } +} + +export const internal = { + getPageUrl: (refererInfo = {}) => { + if (refererInfo.canonicalUrl) { + return refererInfo.canonicalUrl; + } + + if (config.getConfig('pageUrl')) { + return config.getConfig('pageUrl'); + } + + try { + return getWindowTop().location.href; + } catch (e) { + return getWindowSelf().location.href; + } + }, + getReferrer: (refererInfo = {}) => { + if (refererInfo.referer) { + return refererInfo.referer; + } + + try { + return getWindowTop().document.referrer; + } catch (e) { + return getWindowSelf().document.referrer; + } + } +} + +export const spec = { + supportedMediaTypes: [BANNER], + gvlid: GVLID, + code: BIDDER_CODE, + isBidRequestValid: (bidRequest) => { + return !!(bidRequest.sizes && + bidRequest.params && + bidRequest.params.publisherId && + bidRequest.params.tagId); + }, + buildRequests: (validBidRequests, bidderRequest) => { + const [bidRequest] = validBidRequests; + const {refererInfo, gdprConsent = {}, uspConsent} = bidderRequest; + const {publisherId} = bidRequest.params; + const site = getSiteProperties(bidRequest.params, refererInfo); + const device = {ua: navigator.userAgent}; + const imps = getImps(validBidRequests); + const user = { + buyeruid: userData.getUserId(gdprConsent, uspConsent), + ext: {} + }; + const regs = { + coppa: 0, + ext: {} + }; + + if (gdprConsent.gdprApplies) { + user.ext.consent = bidderRequest.gdprConsent.consentString; + regs.ext.gdpr = 1; + } + + if (uspConsent) { + regs.ext.us_privacy = uspConsent; + } + + if (config.getConfig('coppa')) { + regs.coppa = 1 + } + + const ortb2 = config.getConfig('ortb2') || { + badv: [], + bcat: [] + }; + + const request = { + id: bidderRequest.auctionId, + imp: imps, + site, + device, + source: {fd: 1}, + tmax: bidderRequest.timeout, + bcat: ortb2.bcat, + badv: ortb2.badv, + user, + regs + }; + + const url = [END_POINT_URL, publisherId].join('/'); + + return { + url, + method: 'POST', + data: JSON.stringify(request), + bids: validBidRequests, + options: { + withCredentials: false + }, + }; + }, + interpretResponse: (serverResponse, {bids}) => { + if (!bids) { + return []; + } + + const {bidResponses, cur: currency} = getBidResponses(serverResponse); + + if (!bidResponses) { + return []; + } + + return bids.map((bid, id) => getBid(bid.bidId, currency, bidResponses[id])).filter(Boolean); + }, +}; + +function getSiteProperties({publisherId, bcat = []}, refererInfo) { + const {getPageUrl, getReferrer} = internal; + return { + id: publisherId, + name: publisherId, + domain: window.location.host, + page: getPageUrl(refererInfo), + ref: getReferrer(refererInfo), + publisher: { + id: publisherId + }, + content: { + language: navigator.language + } + } +} + +function getImps(validBidRequests) { + return validBidRequests.map((bid, id) => { + const {tagId, bidfloor = null, bidfloorcur = CURRENCY} = bid.params; + + return { + id: id + 1, + banner: getBanners(bid), + tagid: tagId, + bidfloor, + bidfloorcur, + }; + }); +} + +function getBanners(bid) { + return getSizes(bid.sizes); +} + +function getSizes(sizes) { + return { + format: sizes.map(size => { + return { + w: size[0], + h: size[1] + } + }) + } +} + +function getBidResponses({body}) { + if (!body) { + return []; + } + + const {seatbid, cur} = body; + + if (!seatbid.length || !seatbid[0].bid) { + return []; + } + + return { + bidResponses: seatbid[0].bid, + cur + }; +} + +function getBid(requestId, currency, bidResponse) { + if (!bidResponse) { + return; + } + + const { + price: cpm, crid: creativeId, adm: ad, w: width, h: height, adomain: advertiserDomains, meta = {} + } = bidResponse; + + if (advertiserDomains && advertiserDomains.length > 0) { + meta.advertiserDomains = advertiserDomains + } + + return { + requestId, + ttl: 360, + mediaType: BANNER, + cpm, + creativeId, + currency, + ad, + width, + height, + meta, + netRevenue: false + }; +} + +registerBidder(spec); diff --git a/modules/taboolaBidAdapter.md b/modules/taboolaBidAdapter.md new file mode 100644 index 00000000000..a4213355049 --- /dev/null +++ b/modules/taboolaBidAdapter.md @@ -0,0 +1,49 @@ +# Overview + +``` +Module Name: Taboola Adapter +Module Type: Bidder Adapter +Maintainer: prebid@taboola.com +``` + +# Description + +Module that connects to Taboola bidder to fetch bids. +- Supports `display` format +- Uses `OpenRTB` standard + +The Taboola Bidding adapter requires setup before beginning. Please contact us on prebid@taboola.com + +# Test Display Parameters +``` javascript + var adUnits = [{ + code: 'your-unit-container-id', + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]] + } + }, + bids: [{ + bidder: 'taboola', + params: { + tagId: 'tester-placement', // Placement Name + publisherId: 'tester-pub', // your-publisher-id + bidfloor: 0.25, // Optional - default is null + bcat: ['IAB1-1'], // Optional - default is [] + badv: ['example.com'] // Optional - default is [] + } + }] +}]; +``` + +# Parameters + +| Name | Scope | Description | Example | Type | +|----------------|----------|---------------------------------------------------------|----------------------------|--------------| +| `tagId` | required | Tag ID / Placement Name
(as provided by Taboola) | `'Below The Article'` | `String` | +| `publisherId` | required | Alphabetic Publisher ID
(as provided by Taboola) | `'acme-publishing'` | `String` | +| `bcat` | optional | List of blocked advertiser categories (IAB) | `['IAB1-1']` | `Array` | +| `badv` | optional | Blocked Advertiser Domains | `'example.com'` | `String Url` | +| `bidfloor` | optional | CPM bid floor | `0.25` | `Integer` | + + diff --git a/test/spec/modules/taboolaBidAdapter_spec.js b/test/spec/modules/taboolaBidAdapter_spec.js new file mode 100644 index 00000000000..1c4f0fa4c32 --- /dev/null +++ b/test/spec/modules/taboolaBidAdapter_spec.js @@ -0,0 +1,455 @@ +import {expect} from 'chai'; +import {spec, internal, END_POINT_URL, userData} from 'modules/taboolaBidAdapter.js'; +import {config} from '../../../src/config' +import * as utils from '../../../src/utils' + +describe('Taboola Adapter', function () { + let hasLocalStorage, cookiesAreEnabled, getDataFromLocalStorage, localStorageIsEnabled, getCookie; + + beforeEach(() => { + hasLocalStorage = sinon.stub(userData.storageManager, 'hasLocalStorage'); + cookiesAreEnabled = sinon.stub(userData.storageManager, 'cookiesAreEnabled'); + getCookie = sinon.stub(userData.storageManager, 'getCookie'); + getDataFromLocalStorage = sinon.stub(userData.storageManager, 'getDataFromLocalStorage'); + localStorageIsEnabled = sinon.stub(userData.storageManager, 'localStorageIsEnabled'); + + $$PREBID_GLOBAL$$.bidderSettings = { + taboola: { + storageAllowed: true + } + }; + }); + + afterEach(() => { + hasLocalStorage.restore(); + cookiesAreEnabled.restore(); + getCookie.restore(); + getDataFromLocalStorage.restore(); + localStorageIsEnabled.restore(); + + $$PREBID_GLOBAL$$.bidderSettings = {}; + }) + + const commonBidRequest = { + bidder: 'taboola', + params: { + publisherId: 'publisherId', + tagId: 'placement name' + }, + bidId: 'aa43860a-4644-442a-b5e0-93f268cs4d19', + auctionId: '65746dca-26f3-4186-be13-dfa63469b1b7', + } + + const displayBidRequestParams = { + sizes: [[300, 250], [300, 600]] + } + + describe('isBidRequestValid', function () { + it('should fail when bid is invalid - tagId isn`t defined', function () { + const bid = { + bidder: 'taboola', + params: { + publisherId: 'publisherId' + }, + ...displayBidRequestParams + } + expect(spec.isBidRequestValid(bid)).to.equal(false) + }) + + it('should fail when bid is invalid - publisherId isn`t defined', function () { + const bid = { + bidder: 'taboola', + params: { + tagId: 'below the article' + }, + ...displayBidRequestParams + } + expect(spec.isBidRequestValid(bid)).to.equal(false) + }) + + it('should fail when bid is invalid - sizes isn`t defined', function () { + const bid = { + bidder: 'taboola', + params: { + publisherId: 'publisherId', + tagId: 'below the article' + }, + } + expect(spec.isBidRequestValid(bid)).to.equal(false) + }) + + it('should succeed when bid contains valid', function () { + const bid = { + bidder: 'taboola', + params: { + publisherId: 'publisherId', + tagId: 'below the article' + }, + ...displayBidRequestParams, + } + expect(spec.isBidRequestValid(bid)).to.equal(true) + }) + }) + + describe('buildRequests', function () { + const defaultBidRequest = { + ...commonBidRequest, + ...displayBidRequestParams, + } + + const commonBidderRequest = { + refererInfo: { + referer: 'https://example.com/ref', + canonicalUrl: 'https://example.com/' + } + } + + it('should build display request', function () { + const expectedData = { + 'imp': [{ + 'id': 1, + 'banner': { + format: [{ + w: displayBidRequestParams.sizes[0][0], + h: displayBidRequestParams.sizes[0][1] + }, + { + w: displayBidRequestParams.sizes[1][0], + h: displayBidRequestParams.sizes[1][1] + } + ] + }, + 'tagid': commonBidRequest.params.tagId, + 'bidfloor': null, + 'bidfloorcur': 'USD' + }], + 'site': { + 'id': commonBidRequest.params.publisherId, + 'name': commonBidRequest.params.publisherId, + 'domain': window.location.host, + 'page': commonBidderRequest.refererInfo.canonicalUrl, + 'ref': commonBidderRequest.refererInfo.referer, + 'publisher': {'id': commonBidRequest.params.publisherId}, + 'content': {'language': navigator.language} + }, + 'device': {'ua': navigator.userAgent}, + 'source': {'fd': 1}, + 'bcat': [], + 'badv': [], + 'user': { + 'buyeruid': 0, + 'ext': {}, + }, + 'regs': {'coppa': 0, 'ext': {}} + }; + + const res = spec.buildRequests([defaultBidRequest], commonBidderRequest); + + expect(res.url).to.equal(`${END_POINT_URL}/${commonBidRequest.params.publisherId}`); + expect(res.data).to.deep.equal(JSON.stringify(expectedData)); + }) + + it('should pass optional parameters in request', function () { + const optionalParams = { + bidfloor: 0.25, + bidfloorcur: 'EUR' + }; + + const bidRequest = { + ...defaultBidRequest, + params: {...commonBidRequest.params, ...optionalParams} + }; + + const res = spec.buildRequests([bidRequest], commonBidderRequest); + const resData = JSON.parse(res.data); + expect(resData.imp[0].bidfloor).to.deep.equal(0.25); + expect(resData.imp[0].bidfloorcur).to.deep.equal('EUR'); + }); + + it('should pass bidder timeout', function () { + const bidderRequest = { + ...commonBidderRequest, + timeout: 500 + } + const res = spec.buildRequests([defaultBidRequest], bidderRequest); + const resData = JSON.parse(res.data); + expect(resData.tmax).to.equal(500); + }); + + describe('handle privacy segments when building request', function () { + it('should pass GDPR consent', function () { + const bidderRequest = { + refererInfo: { + referer: 'https://example.com/' + }, + gdprConsent: { + gdprApplies: true, + consentString: 'consentString', + } + }; + + const res = spec.buildRequests([defaultBidRequest], bidderRequest) + const resData = JSON.parse(res.data) + expect(resData.user.ext.consent).to.equal('consentString') + expect(resData.regs.ext.gdpr).to.equal(1) + }); + + it('should pass us privacy consent', function () { + const bidderRequest = { + refererInfo: { + referer: 'https://example.com/' + }, + uspConsent: 'consentString' + } + const res = spec.buildRequests([defaultBidRequest], bidderRequest); + const resData = JSON.parse(res.data); + expect(resData.regs.ext.us_privacy).to.equal('consentString'); + }); + + it('should pass coppa consent', function () { + config.setConfig({coppa: true}) + + const res = spec.buildRequests([defaultBidRequest], commonBidderRequest) + const resData = JSON.parse(res.data); + expect(resData.regs.coppa).to.equal(1) + + config.resetConfig() + }); + }) + + describe('handle userid ', function () { + it('should get user id from local storage', function () { + getDataFromLocalStorage.returns(51525152); + hasLocalStorage.returns(true); + localStorageIsEnabled.returns(true); + + const bidderRequest = { + ...commonBidderRequest, + timeout: 500 + } + const res = spec.buildRequests([defaultBidRequest], bidderRequest); + const resData = JSON.parse(res.data); + expect(resData.user.buyeruid).to.equal(51525152); + }); + + it('should get user id from cookie if local storage isn`t defined', function () { + getDataFromLocalStorage.returns(51525152); + hasLocalStorage.returns(false); + localStorageIsEnabled.returns(false); + cookiesAreEnabled.returns(true); + getCookie.returns('taboola%20global%3Auser-id=12121212'); + + const bidderRequest = { + ...commonBidderRequest + }; + const res = spec.buildRequests([defaultBidRequest], bidderRequest); + const resData = JSON.parse(res.data); + + expect(resData.user.buyeruid).to.equal('12121212'); + }); + + it('should get user id from TRC if local storage and cookie isn`t defined', function () { + hasLocalStorage.returns(false); + cookiesAreEnabled.returns(false); + localStorageIsEnabled.returns(false); + + window.TRC = { + user_id: 31313132 + }; + + const bidderRequest = { + ...commonBidderRequest + } + const res = spec.buildRequests([defaultBidRequest], bidderRequest); + const resData = JSON.parse(res.data); + expect(resData.user.buyeruid).to.equal(window.TRC.user_id); + + delete window.TRC; + }); + + it('should get user id to be 0 if cookie, local storage, TRC isn`t defined', function () { + hasLocalStorage.returns(false); + cookiesAreEnabled.returns(false); + + const bidderRequest = { + ...commonBidderRequest + } + const res = spec.buildRequests([defaultBidRequest], bidderRequest); + const resData = JSON.parse(res.data); + expect(resData.user.buyeruid).to.equal(0); + }); + + it('should set buyeruid to be 0 if it`s a new user', function () { + const bidderRequest = { + ...commonBidderRequest + } + const res = spec.buildRequests([defaultBidRequest], bidderRequest); + const resData = JSON.parse(res.data); + expect(resData.user.buyeruid).to.equal(0); + }); + }); + }) + + describe('interpretResponse', function () { + const serverResponse = { + body: { + 'id': '49ffg4d58ef9a163a69fhgfghd4fad03621b9e036f24f7_15', + 'seatbid': [ + { + 'bid': [ + { + 'id': '0b3dd94348-134b-435f-8db5-6bf5afgfc39e86c', + 'impid': '1', + 'price': 0.342068, + 'adid': '2785119545551083381', + 'adm': '\u003chtml\u003e\n\u003chead\u003e\n\u003cmeta charset\u003d"UTF-8"\u003e\n\u003cmeta http-equiv\u003d"Content-Type" content\u003d"text/html; charset\u003dutf-8"/\u003e\u003c/head\u003e\n\u003cbody style\u003d"margin: 0px; overflow:hidden;"\u003e \n\u003cscript type\u003d"text/javascript"\u003e\nwindow.tbl_trc_domain \u003d \u0027us-trc.taboola.com\u0027;\nwindow._taboola \u003d window._taboola || [];\n_taboola.push({article:\u0027auto\u0027});\n!function (e, f, u, i) {\nif (!document.getElementById(i)){\ne.async \u003d 1;\ne.src \u003d u;\ne.id \u003d i;\nf.parentNode.insertBefore(e, f);\n}\n}(document.createElement(\u0027script\u0027),\ndocument.getElementsByTagName(\u0027script\u0027)[0],\n\u0027//cdn.taboola.com/libtrc/wattpad-placement-255/loader.js\u0027,\n\u0027tb_loader_script\u0027);\nif(window.performance \u0026\u0026 typeof window.performance.mark \u003d\u003d \u0027function\u0027)\n{window.performance.mark(\u0027tbl_ic\u0027);}\n\u003c/script\u003e\n\n\u003cdiv id\u003d"taboola-below-article-thumbnails" style\u003d"height: 250px; width: 300px;"\u003e\u003c/div\u003e\n\u003cscript type\u003d"text/javascript"\u003e\nwindow._taboola \u003d window._taboola || [];\n_taboola.push({\nmode: \u0027Rbox_300x250_1x1\u0027,\ncontainer: \u0027taboola-below-article-thumbnails\u0027,\nplacement: \u0027wattpad.com_P18694_S257846_W300_H250_N1_TB\u0027,\ntarget_type: \u0027mix\u0027,\n"rtb-win":{ \nbi:\u002749ff4d58ef9a163a696d4fad03621b9e036f24f7_15\u0027,\ncu:\u0027USD\u0027,\nwp:\u0027${AUCTION_PRICE:BF}\u0027,\nwcb:\u0027~!audex-display-impression!~\u0027,\nrt:\u00271643227025284\u0027,\nrdc:\u0027us.taboolasyndication.com\u0027,\nti:\u00274212\u0027,\nex:\u0027MagniteSCoD\u0027,\nbs:\u0027xapi:257846:lvvSm6Ak7_wE\u0027,\nbp:\u002718694\u0027,\nbd:\u0027wattpad.com\u0027,\nsi:\u00279964\u0027\n} \n,\nrec: {"trc":{"si":"a69c7df43b2334f0aa337c37e2d80c21","sd":"v2_a69c7df43b2334f0aa337c37e2d80c21_3c70f7c7d64a65b15e4a4175c9a2cfa51072f04bMagniteSCoD_1643227025_1643227025_CJS1tQEQ5NdWGPLA0d76xo-9ngEgASgEMCY4iegHQIroB0iB09kDUKPPB1gAYABop-G2i_Hl-eVucAA","ui":"3c70f7c7d64a65b15e4a4175c9a2cfa51072f04bMagniteSCoD","plc":"PHON","wi":"-643136642229425433","cc":"CA","route":"US:US:V","el2r":["bulk-metrics","debug","social","metrics","perf"],"uvpw":"1","pi":"1420260","cpb":"GNO629MGIJz__________wEqGXVzLnRhYm9vbGFzeW5kaWNhdGlvbi5jb20yC3RyYy1zY29kMTI5OIDwmrUMQInoB0iK6AdQgdPZA1ijzwdjCN3__________wEQ3f__________ARgjZGMI3AoQoBAYFmRjCNIDEOAGGAhkYwiWFBCcHBgYZGMI9AUQiwoYC2RjCNkUEPkcGB1kYwj0FBCeHRgfZGorNDlmZjRkNThlZjlhMTYzYTY5NmQ0ZmFkMDM2MjFiOWUwMzZmMjRmN18xNXgCgAHpbIgBrPvTxQE","dcga":{"pubConfigOverride":{"border-color":"black","font-weight":"bold","inherit-title-color":"true","module-name":"cta-lazy-module","enable-call-to-action-creative-component":"true","disable-cta-on-custom-module":"true"}},"tslt":{"p-video-overlay":{"cancel":"סגור","goto":"עבור לדף"},"read-more":{"DEFAULT_CAPTION":"%D7%A7%D7%A8%D7%90%20%D7%A2%D7%95%D7%93"},"next-up":{"BTN_TEXT":"לקריאת התוכן הבא"},"time-ago":{"now":"עכשיו","today":"היום","yesterday":"אתמול","minutes":"לפני {0} דקות","hour":"לפני שעה","hours":"לפני {0} שעות","days":"לפני {0} ימים"},"explore-more":{"TITLE_TEXT":"המשיכו לקרוא","POPUP_TEXT":"אל תפספסו הזדמנות לקרוא עוד תוכן מעולה, רגע לפני שתעזבו"}},"evh":"-1964913910","vl":[{"ri":"185db6d274ce94b27caaabd9eed7915b","uip":"wattpad.com_P18694_S257846_W300_H250_N1_TB","ppb":"COIF","estimation_method":"EcpmEstimationMethodType_ESTIMATION","baseline_variant":"false","original_ecpm":"0.4750949889421463","v":[{"thumbnail":"https://cdn.taboola.com/libtrc/static/thumbnails/a2b272be514ca3ebe3f97a4a32a41db5.jpg","all-thumbnails":"https://cdn.taboola.com/libtrc/static/thumbnails/a2b272be514ca3ebe3f97a4a32a41db5.jpg!-#@1600x1000","origin":"default","thumb-size":"1600x1000","title":"Get Roofing Services At Prices You Can Afford In Edmonton","type":"text","published-date":"1641997069","branding-text":"Roofing Services | Search Ads","url":"https://inneth-conded.xyz/9ad2e613-8777-4fe7-9a52-386c88879289?site\u003dwattpad-placement-255\u0026site_id\u003d1420260\u0026title\u003dGet+Roofing+Services+At+Prices+You+Can+Afford+In+Edmonton\u0026platform\u003dSmartphone\u0026campaign_id\u003d15573949\u0026campaign_item_id\u003d3108610633\u0026thumbnail\u003dhttp%3A%2F%2Fcdn.taboola.com%2Flibtrc%2Fstatic%2Fthumbnails%2Fa2b272be514ca3ebe3f97a4a32a41db5.jpg\u0026cpc\u003d{cpc}\u0026click_id\u003dGiCIypnAQogsMTFL3e_mPaVM2qLvK3KRU6LWzEMUgeB6piCit1Uox6CNr5v5n-x1\u0026tblci\u003dGiCIypnAQogsMTFL3e_mPaVM2qLvK3KRU6LWzEMUgeB6piCit1Uox6CNr5v5n-x1#tblciGiCIypnAQogsMTFL3e_mPaVM2qLvK3KRU6LWzEMUgeB6piCit1Uox6CNr5v5n-x1","duration":"0","sig":"328243c4127ff16e3fdcd7270bab908f6f3fc5b4c98d","item-id":"~~V1~~2785119550041083381~~PnBkfBE9JnQxpahv0adkcuIcmMhroRAHXwLZd-7zhunTxvAnL2wqac4MyzR7uD46gj3kUkbS3FhelBtnsiJV6MhkDZRZzzIqDobN6rWmCPA3hYz5D3PLat6nhIftiT1lwdxwdlxkeV_Mfb3eos_TQavImGhxk0e7psNAZxHJ9RKL2w3lppALGgQJoy2o6lkf-pOqODtX1VkgWpEEM4WsVoWOnUTAwdyGd-8yrze8CWNp752y28hl7lleicyO1vByRdbgwlJdnqyroTPEQNNEn1JRxBOSYSWt-Xm3vkPm-G4","uploader":"","is-syndicated":"true","publisher":"search","id":"~~V1~~2785119550041083381~~PnBkfBE9JnQxpahv0adkcuIcmMhroRAHXwLZd-7zhunTxvAnL2wqac4MyzR7uD46gj3kUkbS3FhelBtnsiJV6MhkDZRZzzIqDobN6rWmCPA3hYz5D3PLat6nhIftiT1lwdxwdlxkeV_Mfb3eos_TQavImGhxk0e7psNAZxHJ9RKL2w3lppALGgQJoy2o6lkf-pOqODtX1VkgWpEEM4WsVoWOnUTAwdyGd-8yrze8CWNp752y28hl7lleicyO1vByRdbgwlJdnqyroTPEQNNEn1JRxBOSYSWt-Xm3vkPm-G4","category":"home","views":"0","itp":[{"u":"https://trc.taboola.com/1326786/log/3/unip?en\u003dclickersusa","t":"c"}],"description":""}]}],"cpcud":{"upc":"0.0","upr":"0.0"}}}\n});\n\u003c/script\u003e\n\n\u003cscript type\u003d"text/javascript"\u003e\nwindow._taboola \u003d window._taboola || [];\n_taboola.push({flush: true});\n\u003c/script\u003e\n\n\u003c/body\u003e\n\u003c/html\u003e', + 'adomain': [ + 'example.xyz' + ], + 'cid': '15744349', + 'crid': '278195503434041083381', + 'w': 300, + 'h': 250, + 'lurl': 'http://us-trc.taboola.com/sample' + } + ], + 'seat': '14204545260' + } + ], + 'bidid': 'da43860a-4644-442a-b5e0-93f268cf8d19', + 'cur': 'USD' + } + }; + + const request = { + bids: [ + { + ...commonBidRequest, + ...displayBidRequestParams + } + ] + } + + it('should return empty array if no valid bids', function () { + const res = spec.interpretResponse(serverResponse, []) + expect(res).to.be.an('array').that.is.empty + }); + + it('should return empty array if no server response', function () { + const res = spec.interpretResponse({}, request) + expect(res).to.be.an('array').that.is.empty + }); + + it('should return empty array if server response without seatbid', function () { + const overriddenServerResponse = {...serverResponse}; + const seatbid = {...serverResponse.body.seatbid[0]}; + overriddenServerResponse.body.seatbid[0] = {}; + + const res = spec.interpretResponse(overriddenServerResponse, request) + expect(res).to.be.an('array').that.is.empty + + overriddenServerResponse.body.seatbid[0] = seatbid; + }); + + it('should return empty array if server response without bids', function () { + const overriddenServerResponse = {...serverResponse}; + const bid = [...serverResponse.body.seatbid[0].bid]; + overriddenServerResponse.body.seatbid[0].bid = {}; + + const res = spec.interpretResponse(overriddenServerResponse, request) + expect(res).to.be.an('array').that.is.empty + + overriddenServerResponse.body.seatbid[0].bid = bid; + }); + + it('should interpret display response', function () { + const [bid] = serverResponse.body.seatbid[0].bid; + const expectedRes = [ + { + requestId: request.bids[0].bidId, + cpm: bid.price, + creativeId: bid.crid, + ttl: 360, + netRevenue: false, + currency: serverResponse.body.cur, + mediaType: 'banner', + ad: bid.adm, + width: bid.w, + height: bid.h, + meta: { + 'advertiserDomains': bid.adomain + }, + } + ] + + const res = spec.interpretResponse(serverResponse, request) + expect(res).to.deep.equal(expectedRes) + }); + }) + + describe('userData', function () { + // todo: add UT for getUserSyncs + }) + + describe('internal functions', function () { + describe('getPageUrl', function () { + let origPageUrl; + const bidderRequest = { + refererInfo: { + canonicalUrl: 'http://canonical.url' + } + }; + + beforeEach(function () { + // remember original pageUrl in config + origPageUrl = config.getConfig('pageUrl'); + + // unset pageUrl in config + config.setConfig({pageUrl: null}); + }); + + afterEach(function () { + // set original pageUrl to config + config.setConfig({pageUrl: origPageUrl}); + }); + + it('should handle empty or missing data', function () { + expect(internal.getPageUrl(undefined)).to.equal(utils.getWindowTop().location.href); + expect(internal.getPageUrl('')).to.equal(utils.getWindowTop().location.href); + }); + + it('should use "pageUrl" from config', function () { + config.setConfig({pageUrl: 'http://page.url'}); + + expect(internal.getPageUrl(undefined)).to.equal(config.getConfig('pageUrl')); + }); + + it('should use bidderRequest.refererInfo.canonicalUrl', function () { + expect(internal.getPageUrl(bidderRequest.refererInfo)).to.equal(bidderRequest.refererInfo.canonicalUrl); + }); + + it('should prefer bidderRequest.refererInfo.canonicalUrl over "pageUrl" from config', () => { + config.setConfig({pageUrl: 'https://page.url'}); + + expect(internal.getPageUrl(bidderRequest.refererInfo)).to.equal(bidderRequest.refererInfo.canonicalUrl); + }); + }); + + describe('getReferrer', function () { + it('should handle empty or missing data', function () { + expect(internal.getReferrer(undefined)).to.equal(utils.getWindowTop().document.referrer); + expect(internal.getReferrer('')).to.equal(utils.getWindowTop().document.referrer); + }); + + it('should use bidderRequest.refererInfo.referer', () => { + const bidderRequest = { + refererInfo: { + referer: 'foobar' + } + }; + + expect(internal.getReferrer(bidderRequest.refererInfo)).to.equal(bidderRequest.refererInfo.referer); + }); + }); + }) +})