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);
+ });
+ });
+ })
+})