diff --git a/libraries/pageInfosUtils/pageInfosUtils.js b/libraries/pageInfosUtils/pageInfosUtils.js new file mode 100644 index 00000000000..5e215ad3f3d --- /dev/null +++ b/libraries/pageInfosUtils/pageInfosUtils.js @@ -0,0 +1,65 @@ +/** + * Retrieves the referrer information from the bidder request. + * + * @param {Object} bidderRequest - The bidder request object. + * @param {Object} [bidderRequest.refererInfo] - The referer information object. + * @param {string} [bidderRequest.refererInfo.page] - The page URL of the referer. + * @returns {string} The referrer URL if available, otherwise an empty string. + */ +export function getReferrerInfo(bidderRequest) { + let ref = ''; + if (bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.page) { + ref = bidderRequest.refererInfo.page; + } + return ref; +} + +/** + * Retrieves the title of the current web page. + * + * This function attempts to get the title from the top-level window's document. + * If an error occurs (e.g., due to cross-origin restrictions), it falls back to the current document. + * It first tries to get the title from the `og:title` meta tag, and if that is not available, it uses the document's title. + * + * @returns {string} The title of the current web page, or an empty string if no title is found. + */ +export function getPageTitle() { + try { + const ogTitle = window.top.document.querySelector('meta[property="og:title"]'); + return window.top.document.title || (ogTitle && ogTitle.content) || ''; + } catch (e) { + const ogTitle = document.querySelector('meta[property="og:title"]'); + return document.title || (ogTitle && ogTitle.content) || ''; + } +} + +/** + * Retrieves the content of the page description meta tag. + * + * This function attempts to get the description from the top-level window's document. + * If it fails (e.g., due to cross-origin restrictions), it falls back to the current document. + * It looks for meta tags with either the name "description" or the property "og:description". + * + * @returns {string} The content of the description meta tag, or an empty string if not found. + */ +export function getPageDescription() { + try { + const element = window.top.document.querySelector('meta[name="description"]') || + window.top.document.querySelector('meta[property="og:description"]'); + return (element && element.content) || ''; + } catch (e) { + const element = document.querySelector('meta[name="description"]') || + document.querySelector('meta[property="og:description"]'); + return (element && element.content) || ''; + } +} + +/** + * Retrieves the downlink speed of the user's network connection. + * + * @param {object} nav - The navigator object, typically `window.navigator`. + * @returns {string} The downlink speed as a string if available, otherwise an empty string. + */ +export function getConnectionDownLink(nav) { + return nav && nav.connection && nav.connection.downlink >= 0 ? nav.connection.downlink.toString() : ''; +} diff --git a/libraries/timeToFirstBytesUtils/timeToFirstBytesUtils.js b/libraries/timeToFirstBytesUtils/timeToFirstBytesUtils.js new file mode 100644 index 00000000000..5d5330d8127 --- /dev/null +++ b/libraries/timeToFirstBytesUtils/timeToFirstBytesUtils.js @@ -0,0 +1,37 @@ +/** + * Calculates the Time to First Byte (TTFB) for the given window object. + * + * This function attempts to use the Navigation Timing Level 2 API first, and falls back to + * the Navigation Timing Level 1 API if the former is not available. + * + * @param {Window} win - The window object from which to retrieve performance timing information. + * @returns {string} The TTFB in milliseconds as a string, or an empty string if the TTFB cannot be determined. + */ +export function getTimeToFirstByte(win) { + const performance = win.performance || win.webkitPerformance || win.msPerformance || win.mozPerformance; + + const ttfbWithTimingV2 = performance && + typeof performance.getEntriesByType === 'function' && + Object.prototype.toString.call(performance.getEntriesByType) === '[object Function]' && + performance.getEntriesByType('navigation')[0] && + performance.getEntriesByType('navigation')[0].responseStart && + performance.getEntriesByType('navigation')[0].requestStart && + performance.getEntriesByType('navigation')[0].responseStart > 0 && + performance.getEntriesByType('navigation')[0].requestStart > 0 && + Math.round( + performance.getEntriesByType('navigation')[0].responseStart - performance.getEntriesByType('navigation')[0].requestStart + ); + + if (ttfbWithTimingV2) { + return ttfbWithTimingV2.toString(); + } + + const ttfbWithTimingV1 = performance && + performance.timing.responseStart && + performance.timing.requestStart && + performance.timing.responseStart > 0 && + performance.timing.requestStart > 0 && + performance.timing.responseStart - performance.timing.requestStart; + + return ttfbWithTimingV1 ? ttfbWithTimingV1.toString() : ''; +} diff --git a/modules/greenbidsBidAdapter.js b/modules/greenbidsBidAdapter.js new file mode 100644 index 00000000000..418cb850527 --- /dev/null +++ b/modules/greenbidsBidAdapter.js @@ -0,0 +1,238 @@ +import { getValue, logError, deepAccess, parseSizesInput, getBidIdParameter, logInfo } from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { getHLen } from '../libraries/navigatorData/navigatorData.js'; +import { getTimeToFirstByte } from '../libraries/timeToFirstBytesUtils/timeToFirstBytesUtils.js'; +import { getReferrerInfo, getPageTitle, getPageDescription, getConnectionDownLink } from '../libraries/pageInfosUtils/pageInfosUtils.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + +const BIDDER_CODE = 'greenbids'; +const GVL_ID = 1232; +const ENDPOINT_URL = 'https://hb.greenbids.ai'; +export const storage = getStorageManager({ bidderCode: BIDDER_CODE }); + +export const spec = { + code: BIDDER_CODE, + gvlid: GVL_ID, + supportedMediaTypes: ['banner', 'video'], + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (bid) { + if (typeof bid.params !== 'undefined' && parseInt(getValue(bid.params, 'placementId')) > 0) { + logInfo('Greenbids bidder adapter valid bid request'); + return true; + } else { + logError('Greenbids bidder adapter requires placementId to be defined and a positive number'); + return false; + } + }, + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} validBidRequests array of bids + * @param bidderRequest bidder request object + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function (validBidRequests, bidderRequest) { + const bids = validBidRequests.map(bids => { + const reqObj = {}; + let placementId = getValue(bids.params, 'placementId'); + const gpid = deepAccess(bids, 'ortb2Imp.ext.gpid'); + reqObj.sizes = getSizes(bids); + reqObj.bidId = getBidIdParameter('bidId', bids); + reqObj.bidderRequestId = getBidIdParameter('bidderRequestId', bids); + reqObj.placementId = parseInt(placementId, 10); + reqObj.adUnitCode = getBidIdParameter('adUnitCode', bids); + reqObj.transactionId = bids.ortb2Imp?.ext?.tid || ''; + if (gpid) { reqObj.gpid = gpid; } + }); + const topWindow = window.top; + + const payload = { + referrer: getReferrerInfo(bidderRequest), + pageReferrer: document.referrer, + pageTitle: getPageTitle().slice(0, 300), + pageDescription: getPageDescription().slice(0, 300), + networkBandwidth: getConnectionDownLink(window.navigator), + timeToFirstByte: getTimeToFirstByte(window), + data: bids, + device: bidderRequest?.ortb2?.device || {}, + deviceWidth: screen.width, + deviceHeight: screen.height, + devicePixelRatio: topWindow.devicePixelRatio, + screenOrientation: screen.orientation?.type, + historyLength: getHLen(), + viewportHeight: topWindow.visualViewport?.height, + viewportWidth: topWindow.visualViewport?.width, + prebid_version: '$prebid.version$', + }; + + const firstBidRequest = validBidRequests[0]; + + if (firstBidRequest.schain) { + payload.schain = firstBidRequest.schain; + } + + hydratePayloadWithGppConsentData(payload, bidderRequest.gppConsent); + hydratePayloadWithGdprConsentData(payload, bidderRequest.gdprConsent); + hydratePayloadWithUspConsentData(payload, bidderRequest.uspConsent); + + const userAgentClientHints = deepAccess(firstBidRequest, 'ortb2.device.sua'); + if (userAgentClientHints) { + payload.userAgentClientHints = userAgentClientHints; + } + + const dsa = deepAccess(bidderRequest, 'ortb2.regs.ext.dsa'); + if (dsa) { + payload.dsa = dsa; + } + + const payloadString = JSON.stringify(payload); + return { + method: 'POST', + url: ENDPOINT_URL, + data: payloadString, + }; + }, + /** + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server response. + */ + interpretResponse: function (serverResponse) { + serverResponse = serverResponse.body; + if (!serverResponse.responses) { + return []; + } + return serverResponse.responses.map((bid) => { + const bidResponse = { + cpm: bid.cpm, + width: bid.width, + height: bid.height, + currency: bid.currency, + netRevenue: true, + size: bid.size, + ttl: bid.ttl, + meta: { + advertiserDomains: bid && bid.adomain ? bid.adomain : [], + }, + ad: bid.ad, + requestId: bid.bidId, + creativeId: bid.creativeId, + placementId: bid.placementId, + }; + if (bid.dealId) { + bidResponse.dealId = bid.dealId + } + if (bid?.ext?.dsa) { + bidResponse.meta.dsa = bid.ext.dsa; + } + return bidResponse; + }); + } +}; + +registerBidder(spec); + +/** + * Converts the sizes from the bid object to the required format. + * + * @param {Object} bid - The bid object containing size information. + * @param {Array} bid.sizes - The sizes array from the bid object. + * @returns {Array} - The parsed sizes in the required format. + */ +function getSizes(bid) { + return parseSizesInput(bid.sizes); +} + +// Privacy handling + +/** + * Hydrates the given payload with GPP consent data if available. + * + * @param {Object} payload - The payload object to be hydrated. + * @param {Object} gppData - The GPP consent data object. + * @param {string} gppData.gppString - The GPP consent string. + * @param {number[]} gppData.applicableSections - An array of applicable section IDs. + */ +function hydratePayloadWithGppConsentData(payload, gppData) { + if (!gppData) { return; } + let isValidConsentString = typeof gppData.gppString === 'string'; + let validateApplicableSections = + Array.isArray(gppData.applicableSections) && + gppData.applicableSections.every((section) => typeof (section) === 'number') + payload.gpp = { + consentString: isValidConsentString ? gppData.gppString : '', + applicableSectionIds: validateApplicableSections ? gppData.applicableSections : [], + }; +} + +/** + * Hydrates the given payload with GDPR consent data if available. + * + * @param {Object} payload - The payload object to be hydrated with GDPR consent data. + * @param {Object} gdprData - The GDPR data object containing consent information. + * @param {boolean} gdprData.gdprApplies - Indicates if GDPR applies. + * @param {string} gdprData.consentString - The GDPR consent string. + * @param {number} gdprData.apiVersion - The version of the GDPR API being used. + * @param {Object} gdprData.vendorData - Additional vendor data related to GDPR. + */ +function hydratePayloadWithGdprConsentData(payload, gdprData) { + if (!gdprData) { return; } + let isCmp = typeof gdprData.gdprApplies === 'boolean'; + let isConsentString = typeof gdprData.consentString === 'string'; + let status = isCmp + ? findGdprStatus(gdprData.gdprApplies, gdprData.vendorData) + : gdprStatus.CMP_NOT_FOUND_OR_ERROR; + payload.gdpr_iab = { + consent: isConsentString ? gdprData.consentString : '', + status: status, + apiVersion: gdprData.apiVersion + }; +} + +/** + * Adds USP (CCPA) consent data to the payload if available. + * + * @param {Object} payload - The payload object to be hydrated with USP consent data. + * @param {string} uspConsentData - The USP consent string to be added to the payload. + */ +function hydratePayloadWithUspConsentData(payload, uspConsentData) { + if (!uspConsentData) { return; } + payload.us_privacy = uspConsentData; +} + +const gdprStatus = { + GDPR_APPLIES_PUBLISHER: 12, + GDPR_APPLIES_GLOBAL: 11, + GDPR_DOESNT_APPLY: 0, + CMP_NOT_FOUND_OR_ERROR: 22 +}; + +/** + * Determines the GDPR status based on whether GDPR applies and the provided GDPR data. + * + * @param {boolean} gdprApplies - Indicates if GDPR applies. + * @param {Object} gdprData - The GDPR data object. + * @param {boolean} gdprData.isServiceSpecific - Indicates if the GDPR data is service-specific. + * @returns {string} The GDPR status. + */ +function findGdprStatus(gdprApplies, gdprData) { + let status = gdprStatus.GDPR_APPLIES_PUBLISHER; + if (gdprApplies) { + if (gdprData && !gdprData.isServiceSpecific) { + status = gdprStatus.GDPR_APPLIES_GLOBAL; + } + } else { + status = gdprStatus.GDPR_DOESNT_APPLY; + } + return status; +} diff --git a/modules/greenbidsBidAdapter.md b/modules/greenbidsBidAdapter.md new file mode 100644 index 00000000000..df536294f47 --- /dev/null +++ b/modules/greenbidsBidAdapter.md @@ -0,0 +1,32 @@ +# Overview + +**Module Name**: Greenbids Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: tech@greenbids.ai + +# Description + +Use `greenbids` as bidder. + +## AdUnits configuration example +``` + var adUnits = [{ + code: 'your-slot_1-div', //use exactly the same code as your slot div id. + sizes: [[300, 250]], + bids: [{ + bidder: 'greenbids', + params: { + placementId: 12345, + } + }] + },{ + code: 'your-slot_2-div', //use exactly the same code as your slot div id. + sizes: [[600, 800]], + bids: [{ + bidder: 'greenbids', + params: { + placementId: 12345, + } + }] + }]; +``` diff --git a/test/spec/libraries/pageInfosUtils.js b/test/spec/libraries/pageInfosUtils.js new file mode 100644 index 00000000000..59d62abc74c --- /dev/null +++ b/test/spec/libraries/pageInfosUtils.js @@ -0,0 +1,102 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { getReferrerInfo, getPageTitle, getPageDescription, getConnectionDownLink } from './pageInfosUtils'; + +describe('pageInfosUtils', () => { + describe('getReferrerInfo', () => { + it('should return the referrer URL if available', () => { + const bidderRequest = { + refererInfo: { + page: 'http://example.com' + } + }; + const result = getReferrerInfo(bidderRequest); + expect(result).to.equal('http://example.com'); + }); + + it('should return an empty string if referrer URL is not available', () => { + const bidderRequest = {}; + const result = getReferrerInfo(bidderRequest); + expect(result).to.equal(''); + }); + }); + + describe('getPageTitle', () => { + let topDocumentStub, documentStub; + + beforeEach(() => { + topDocumentStub = sinon.stub(window.top, 'document').value({ + title: 'Top Document Title', + querySelector: sinon.stub().returns(null) + }); + documentStub = sinon.stub(document, 'querySelector').returns(null); + }); + + afterEach(() => { + topDocumentStub.restore(); + documentStub.restore(); + }); + + it('should return the title from the top-level document', () => { + const result = getPageTitle(); + expect(result).to.equal('Top Document Title'); + }); + + it('should return the title from the current document if top-level document access fails', () => { + topDocumentStub.value({ + title: '', + querySelector: sinon.stub().throws(new Error('Cross-origin restriction')) + }); + documentStub.returns({ content: 'Current Document Title' }); + const result = getPageTitle(); + expect(result).to.equal('Current Document Title'); + }); + }); + + describe('getPageDescription', () => { + let topDocumentStub, documentStub; + + beforeEach(() => { + topDocumentStub = sinon.stub(window.top, 'document').value({ + querySelector: sinon.stub().returns(null) + }); + documentStub = sinon.stub(document, 'querySelector').returns(null); + }); + + afterEach(() => { + topDocumentStub.restore(); + documentStub.restore(); + }); + + it('should return the description from the top-level document', () => { + topDocumentStub.querySelector.withArgs('meta[name="description"]').returns({ content: 'Top Document Description' }); + const result = getPageDescription(); + expect(result).to.equal('Top Document Description'); + }); + + it('should return the description from the current document if top-level document access fails', () => { + topDocumentStub.querySelector.throws(new Error('Cross-origin restriction')); + documentStub.withArgs('meta[name="description"]').returns({ content: 'Current Document Description' }); + const result = getPageDescription(); + expect(result).to.equal('Current Document Description'); + }); + }); + + describe('getConnectionDownLink', () => { + it('should return the downlink speed if available', () => { + const nav = { + connection: { + downlink: 10 + } + }; + const result = getConnectionDownLink(nav); + expect(result).to.equal('10'); + }); + + it('should return an empty string if downlink speed is not available', () => { + const nav = {}; + const result = getConnectionDownLink(nav); + expect(result).to.equal(''); + }); + }); +}); diff --git a/test/spec/libraries/timeToFirstBytesUtils.js b/test/spec/libraries/timeToFirstBytesUtils.js new file mode 100644 index 00000000000..ea737e925c4 --- /dev/null +++ b/test/spec/libraries/timeToFirstBytesUtils.js @@ -0,0 +1,54 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { getTimeToFirstByte } from './timeToFirstBytesUtils'; + +describe('getTimeToFirstByte', () => { + let win; + + beforeEach(() => { + win = { + performance: { + getEntriesByType: sinon.stub(), + timing: { + responseStart: 0, + requestStart: 0 + } + } + }; + }); + + it('should return TTFB using Navigation Timing Level 2 API', () => { + win.performance.getEntriesByType.withArgs('navigation').returns([{ + responseStart: 100, + requestStart: 50 + }]); + + const ttfb = getTimeToFirstByte(win); + expect(ttfb).to.equal('50'); + }); + + it('should return TTFB using Navigation Timing Level 1 API', () => { + win.performance.getEntriesByType.returns([]); + win.performance.timing.responseStart = 100; + win.performance.timing.requestStart = 50; + + const ttfb = getTimeToFirstByte(win); + expect(ttfb).to.equal('50'); + }); + + it('should return an empty string if TTFB cannot be determined', () => { + win.performance.getEntriesByType.returns([]); + win.performance.timing.responseStart = 0; + win.performance.timing.requestStart = 0; + + const ttfb = getTimeToFirstByte(win); + expect(ttfb).to.equal(''); + }); + + it('should return an empty string if performance object is not available', () => { + win.performance = null; + + const ttfb = getTimeToFirstByte(win); + expect(ttfb).to.equal(''); + }); +}); diff --git a/test/spec/modules/greenbidsBidAdapter_specs.js b/test/spec/modules/greenbidsBidAdapter_specs.js new file mode 100644 index 00000000000..014c3545cad --- /dev/null +++ b/test/spec/modules/greenbidsBidAdapter_specs.js @@ -0,0 +1,1051 @@ +import { expect } from 'chai'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import { spec } from 'modules/greenbidsBidAdapter.js'; +const ENDPOINT = 'https://d.greenbids.ai/hb/bid-request'; +const AD_SCRIPT = '"'; + +describe('greenbidsBidAdapter', () => { + const adapter = newBidder(spec); + let sandbox; + + beforeEach(function () { + sandbox = sinon.sandbox.create(); + }); + + afterEach(function () { + sandbox.restore(); + }); + + describe('inherited functions', () => { + it('exists and is a function', () => { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + let bid = { + 'bidder': 'greenbids', + 'params': { + 'placementId': 4242 + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'creativeId': 'er2ee', + }; + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not found', function () { + let bidNonGbCompatible = { + 'bidder': 'greenbids', + }; + expect(spec.isBidRequestValid(bidNonGbCompatible)).to.equal(false); + }); + + it('should return false when the placement is not a number', function () { + let bidNonGbCompatible = { + 'bidder': 'greenbids', + 'params': { + 'placementId': 'toto' + }, + }; + expect(spec.isBidRequestValid(bidNonGbCompatible)).to.equal(false); + }); + }) + describe('buildRequests', function () { + it('should send bid request to ENDPOINT via POST', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + + expect(request.url).to.equal(ENDPOINT); + expect(request.method).to.equal('POST'); + }); + + it('should not send auctionId in bid request ', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.data[0].auctionId).to.not.exist + }); + + it('should send US Privacy to endpoint', function () { + let usPrivacy = 'OHHHFCP1' + let bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'uspConsent': usPrivacy + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.us_privacy).to.exist; + expect(payload.us_privacy).to.equal(usPrivacy); + }); + + it('should send GPP values to endpoint when available and valid', function () { + let consentString = 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN'; + let applicableSectionIds = [7, 8]; + let bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gppConsent': { + 'gppString': consentString, + 'applicableSections': applicableSectionIds + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gpp).to.exist; + expect(payload.gpp.consentString).to.equal(consentString); + expect(payload.gpp.applicableSectionIds).to.have.members(applicableSectionIds); + }); + + it('should send default GPP values to endpoint when available but invalid', function () { + let bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gppConsent': { + 'gppString': undefined, + 'applicableSections': ['a'] + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gpp).to.exist; + expect(payload.gpp.consentString).to.equal(''); + expect(payload.gpp.applicableSectionIds).to.have.members([]); + }); + + it('should not set the GPP object in the request sent to the endpoint when not present', function () { + let bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000 + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gpp).to.not.exist; + }); + + it('should send GDPR to endpoint', function () { + let consentString = 'JRJ8RKfDeBNsERRDCSAAZ+A=='; + let bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + 'consentString': consentString, + 'gdprApplies': true, + 'vendorData': { + 'isServiceSpecific': true + }, + 'apiVersion': 2 + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gdpr_iab).to.exist; + expect(payload.gdpr_iab.consent).to.equal(consentString); + expect(payload.gdpr_iab.status).to.equal(12); + expect(payload.gdpr_iab.apiVersion).to.equal(2); + }); + + it('should add referer info to payload', function () { + const bidRequest = Object.assign({}, bidRequests[0]) + const bidderRequest = { + refererInfo: { + page: 'https://example.com/page.html', + reachedTop: true, + numIframes: 2 + } + } + const request = spec.buildRequests([bidRequest], bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.referrer).to.exist; + expect(payload.referrer).to.deep.equal('https://example.com/page.html') + }); + + const originalConnection = window.navigator.connection; + const mockConnection = { downlink: 10 }; + + const setNavigatorConnection = (connection) => { + Object.defineProperty(window.navigator, 'connection', { + value: connection, + configurable: true, + }); + }; + + try { + setNavigatorConnection(mockConnection); + + const requestWithConnection = spec.buildRequests(bidRequests, bidderRequestDefault); + const payloadWithConnection = JSON.parse(requestWithConnection.data); + + expect(payloadWithConnection.networkBandwidth).to.exist; + expect(payloadWithConnection.networkBandwidth).to.deep.equal(mockConnection.downlink.toString()); + + setNavigatorConnection(undefined); + + const requestWithoutConnection = spec.buildRequests(bidRequests, bidderRequestDefault); + const payloadWithoutConnection = JSON.parse(requestWithoutConnection.data); + + expect(payloadWithoutConnection.networkBandwidth).to.exist; + expect(payloadWithoutConnection.networkBandwidth).to.deep.equal(''); + } finally { + setNavigatorConnection(originalConnection); + } + + it('should add pageReferrer info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageReferrer).to.exist; + expect(payload.pageReferrer).to.deep.equal(document.referrer); + }); + + it('should add width info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + const deviceWidth = screen.width + + expect(payload.deviceWidth).to.exist; + expect(payload.deviceWidth).to.deep.equal(deviceWidth); + }); + + it('should add height info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + const deviceHeight = screen.height + + expect(payload.deviceHeight).to.exist; + expect(payload.deviceHeight).to.deep.equal(deviceHeight); + }); + + it('should add pixelRatio info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + const pixelRatio = window.top.devicePixelRatio + + expect(payload.devicePixelRatio).to.exist; + expect(payload.devicePixelRatio).to.deep.equal(pixelRatio); + }); + + it('should add screenOrientation info to payload', function () { + const originalScreenOrientation = window.top.screen.orientation; + + const mockScreenOrientation = (type) => { + Object.defineProperty(window.top.screen, 'orientation', { + value: { type }, + configurable: true, + }); + }; + + try { + const mockType = 'landscape-primary'; + mockScreenOrientation(mockType); + + const requestWithOrientation = spec.buildRequests(bidRequests, bidderRequestDefault); + const payloadWithOrientation = JSON.parse(requestWithOrientation.data); + + expect(payloadWithOrientation.screenOrientation).to.exist; + expect(payloadWithOrientation.screenOrientation).to.deep.equal(mockType); + + mockScreenOrientation(undefined); + + const requestWithoutOrientation = spec.buildRequests(bidRequests, bidderRequestDefault); + const payloadWithoutOrientation = JSON.parse(requestWithoutOrientation.data); + + expect(payloadWithoutOrientation.screenOrientation).to.not.exist; + } finally { + Object.defineProperty(window.top.screen, 'orientation', { + value: originalScreenOrientation, + configurable: true, + }); + } + }); + + it('should add historyLength info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.historyLength).to.exist; + expect(payload.historyLength).to.deep.equal(window.top.history.length); + }); + + it('should add viewportHeight info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.viewportHeight).to.exist; + expect(payload.viewportHeight).to.deep.equal(window.top.visualViewport.height); + }); + + it('should add viewportWidth info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.viewportWidth).to.exist; + expect(payload.viewportWidth).to.deep.equal(window.top.visualViewport.width); + }); + + it('should add viewportHeight info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.viewportHeight).to.exist; + expect(payload.viewportHeight).to.deep.equal(window.top.visualViewport.height); + }); + + it('should add ortb2 device data to payload', function () { + const ortb2DeviceBidderRequest = { + ...bidderRequestDefault, + ...{ + ortb2: { + device: { + w: 980, + h: 1720, + dnt: 0, + ua: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/125.0.6422.80 Mobile/15E148 Safari/604.1', + language: 'en', + devicetype: 1, + make: 'Apple', + model: 'iPhone 12 Pro Max', + os: 'iOS', + osv: '17.4', + ext: { fiftyonedegrees_deviceId: '17595-133085-133468-18092' }, + }, + }, + }, + }; + const request = spec.buildRequests(bidRequests, ortb2DeviceBidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.device).to.deep.equal(ortb2DeviceBidderRequest.ortb2.device); + }); + + it('should add hardwareConcurrency info to payload', function () { + const originalHardwareConcurrency = window.top.navigator.hardwareConcurrency; + + const mockHardwareConcurrency = (value) => { + Object.defineProperty(window.top.navigator, 'hardwareConcurrency', { + value, + configurable: true, + }); + }; + + try { + const mockValue = 8; + mockHardwareConcurrency(mockValue); + + const requestWithHardwareConcurrency = spec.buildRequests(bidRequests, bidderRequestDefault); + const payloadWithHardwareConcurrency = JSON.parse(requestWithHardwareConcurrency.data); + + expect(payloadWithHardwareConcurrency.hardwareConcurrency).to.exist; + expect(payloadWithHardwareConcurrency.hardwareConcurrency).to.deep.equal(mockValue); + + mockHardwareConcurrency(undefined); + + const requestWithoutHardwareConcurrency = spec.buildRequests(bidRequests, bidderRequestDefault); + const payloadWithoutHardwareConcurrency = JSON.parse(requestWithoutHardwareConcurrency.data); + + expect(payloadWithoutHardwareConcurrency.hardwareConcurrency).to.not.exist; + } finally { + Object.defineProperty(window.top.navigator, 'hardwareConcurrency', { + value: originalHardwareConcurrency, + configurable: true, + }); + } + }); + + it('should add deviceMemory info to payload', function () { + const originalDeviceMemory = window.top.navigator.deviceMemory; + + const mockDeviceMemory = (value) => { + Object.defineProperty(window.top.navigator, 'deviceMemory', { + value, + configurable: true, + }); + }; + + try { + const mockValue = 4; + mockDeviceMemory(mockValue); + + const requestWithDeviceMemory = spec.buildRequests(bidRequests, bidderRequestDefault); + const payloadWithDeviceMemory = JSON.parse(requestWithDeviceMemory.data); + + expect(payloadWithDeviceMemory.deviceMemory).to.exist; + expect(payloadWithDeviceMemory.deviceMemory).to.deep.equal(mockValue); + + mockDeviceMemory(undefined); + + const requestWithoutDeviceMemory = spec.buildRequests(bidRequests, bidderRequestDefault); + const payloadWithoutDeviceMemory = JSON.parse(requestWithoutDeviceMemory.data); + + expect(payloadWithoutDeviceMemory.deviceMemory).to.not.exist; + } finally { + Object.defineProperty(window.top.navigator, 'deviceMemory', { + value: originalDeviceMemory, + configurable: true, + }); + } + }); + }); + + describe('pageTitle', function () { + it('should add pageTitle info to payload based on document title', function () { + const testText = 'This is a title'; + sandbox.stub(window.top.document, 'title').value(testText); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageTitle).to.exist; + expect(payload.pageTitle).to.deep.equal(testText); + }); + + it('should add pageTitle info to payload based on open-graph title', function () { + const testText = 'This is a title from open-graph'; + sandbox.stub(window.top.document, 'title').value(''); + sandbox.stub(window.top.document, 'querySelector').withArgs('meta[property="og:title"]').returns({ content: testText }); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageTitle).to.exist; + expect(payload.pageTitle).to.deep.equal(testText); + }); + + it('should add pageTitle info to payload sliced on 300 first characters', function () { + const testText = Array(500).join('a'); + sandbox.stub(window.top.document, 'title').value(testText); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageTitle).to.exist; + expect(payload.pageTitle).to.have.length(300); + }); + + it('should add pageTitle info to payload when fallbacking from window.top', function () { + const testText = 'This is a fallback title'; + sandbox.stub(window.top.document, 'querySelector').throws(); + sandbox.stub(document, 'title').value(testText); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageTitle).to.exist; + expect(payload.pageTitle).to.deep.equal(testText); + }); + }); + + describe('pageDescription', function () { + it('should add pageDescription info to payload based on open-graph description', function () { + const testText = 'This is a description'; + sandbox.stub(window.top.document, 'querySelector').withArgs('meta[name="description"]').returns({ content: testText }); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageDescription).to.exist; + expect(payload.pageDescription).to.deep.equal(testText); + }); + + it('should add pageDescription info to payload based on open-graph description', function () { + const testText = 'This is a description from open-graph'; + sandbox.stub(window.top.document, 'querySelector').withArgs('meta[property="og:description"]').returns({ content: testText }); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageDescription).to.exist; + expect(payload.pageDescription).to.deep.equal(testText); + }); + + it('should add pageDescription info to payload sliced on 300 first characters', function () { + const testText = Array(500).join('a'); + sandbox.stub(window.top.document, 'querySelector').withArgs('meta[name="description"]').returns({ content: testText }); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageDescription).to.exist; + expect(payload.pageDescription).to.have.length(300); + }); + + it('should add pageDescription info to payload when fallbacking from window.top', function () { + const testText = 'This is a fallback description'; + sandbox.stub(window.top.document, 'querySelector').throws(); + sandbox.stub(document, 'querySelector').withArgs('meta[name="description"]').returns({ content: testText }); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageDescription).to.exist; + expect(payload.pageDescription).to.deep.equal(testText); + }); + + it('should add timeToFirstByte info to payload for Navigation Timing V2', function () { + // Mock `performance` object with Navigation Timing V2 data + const mockPerformance = { + getEntriesByType: () => [ + { requestStart: 100, responseStart: 150 }, + ], + }; + + // Override the global performance object + const originalPerformance = window.performance; + window.performance = mockPerformance; + + // Execute the code + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + // Calculate expected TTFB for V2 + const ttfbExpected = Math.round( + mockPerformance.getEntriesByType('navigation')[0].responseStart - + mockPerformance.getEntriesByType('navigation')[0].requestStart + ).toString(); + + // Assertions + expect(payload.timeToFirstByte).to.exist; + expect(payload.timeToFirstByte).to.deep.equal(ttfbExpected); + + // Restore the original performance object + window.performance = originalPerformance; + }); + + it('should add timeToFirstByte info to payload for Navigation Timing V1', function () { + // Mock `performance` object with Navigation Timing V1 data + const mockPerformance = { + timing: { + requestStart: 100, + responseStart: 150, + }, + getEntriesByType: () => [], + }; + + // Override the global performance object + const originalPerformance = window.performance; + window.performance = mockPerformance; + + // Execute the code + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + // Calculate expected TTFB for V1 + const ttfbExpected = ( + mockPerformance.timing.responseStart - mockPerformance.timing.requestStart + ).toString(); + + // Assertions + expect(payload.timeToFirstByte).to.exist; + expect(payload.timeToFirstByte).to.deep.equal(ttfbExpected); + + // Restore the original performance object + window.performance = originalPerformance; + }); + + it('should send GDPR to endpoint with 11 status', function () { + let consentString = 'JRJ8RKfDeBNsERRDCSAAZ+A=='; + let bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + 'consentString': consentString, + 'gdprApplies': true, + 'vendorData': { + 'isServiceSpecific': false, + }, + 'apiVersion': 2 + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gdpr_iab).to.exist; + expect(payload.gdpr_iab.consent).to.equal(consentString); + expect(payload.gdpr_iab.status).to.equal(11); + expect(payload.gdpr_iab.apiVersion).to.equal(2); + }); + + it('should send GDPR TCF2 to endpoint with 12 status', function () { + let consentString = 'JRJ8RKfDeBNsERRDCSAAZ+A=='; + let bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + 'consentString': consentString, + 'gdprApplies': true, + 'vendorData': { + 'isServiceSpecific': true + }, + 'apiVersion': 2 + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gdpr_iab).to.exist; + expect(payload.gdpr_iab.consent).to.equal(consentString); + expect(payload.gdpr_iab.status).to.equal(12); + expect(payload.gdpr_iab.apiVersion).to.equal(2); + }); + + it('should send GDPR to endpoint with 22 status', function () { + let bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + 'consentString': undefined, + 'gdprApplies': undefined, + 'vendorData': undefined, + 'apiVersion': 2 + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gdpr_iab).to.exist; + expect(payload.gdpr_iab.consent).to.equal(''); + expect(payload.gdpr_iab.status).to.equal(22); + expect(payload.gdpr_iab.apiVersion).to.equal(2); + }); + + it('should send GDPR to endpoint with 0 status', function () { + let consentString = 'JRJ8RKfDeBNsERRDCSAAZ+A=='; + let bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + 'consentString': consentString, + 'gdprApplies': false, + 'vendorData': { + 'hasGlobalScope': false + }, + 'apiVersion': 2 + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gdpr_iab).to.exist; + expect(payload.gdpr_iab.consent).to.equal(consentString); + expect(payload.gdpr_iab.status).to.equal(0); + expect(payload.gdpr_iab.apiVersion).to.equal(2); + }); + + it('should send GDPR to endpoint with 0 status when gdprApplies = false (vendorData = undefined)', function () { + let bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + 'consentString': undefined, + 'gdprApplies': false, + 'vendorData': undefined, + 'apiVersion': 2 + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gdpr_iab).to.exist; + expect(payload.gdpr_iab.consent).to.equal(''); + expect(payload.gdpr_iab.status).to.equal(0); + expect(payload.gdpr_iab.apiVersion).to.equal(2); + }); + + it('should send GDPR to endpoint with 12 status when apiVersion = 0', function () { + let consentString = 'JRJ8RKfDeBNsERRDCSAAZ+A=='; + let bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + 'consentString': consentString, + 'gdprApplies': true, + 'vendorData': { + 'isServiceSpecific': true + }, + 'apiVersion': 0 + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gdpr_iab).to.exist; + expect(payload.gdpr_iab.consent).to.equal(consentString); + expect(payload.gdpr_iab.status).to.equal(12); + expect(payload.gdpr_iab.apiVersion).to.equal(0); + }); + + it('should add schain info to payload if available', function () { + const bidRequest = Object.assign({}, bidRequests[0], { + schain: { + ver: '1.0', + complete: 1, + nodes: [{ + asi: 'example.com', + sid: '00001', + hp: 1 + }] + } + }); + + const request = spec.buildRequests([bidRequest], bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.schain).to.exist; + expect(payload.schain).to.deep.equal({ + ver: '1.0', + complete: 1, + nodes: [{ + asi: 'example.com', + sid: '00001', + hp: 1 + }] + }); + }); + + it('should add userAgentClientHints info to payload if available', function () { + const sua = { + source: 2, + platform: { + brand: 'macOS', + version: ['12', '4', '0'] + }, + browsers: [ + { + brand: 'Chromium', + version: ['106', '0', '5249', '119'] + }, + { + brand: 'Google Chrome', + version: ['106', '0', '5249', '119'] + }, + { + brand: 'Not;A=Brand', + version: ['99', '0', '0', '0'] + } + ], + mobile: 0, + model: '', + bitness: '64', + architecture: 'x86' + } + + const bidRequest = Object.assign({}, bidRequests[0], { + ortb2: { + device: { + sua: sua + } + } + }); + + const requestWithUserAgentClientHints = spec.buildRequests([bidRequest], bidderRequestDefault); + const payload = JSON.parse(requestWithUserAgentClientHints.data); + + expect(payload.userAgentClientHints).to.exist; + expect(payload.userAgentClientHints).to.deep.equal(sua); + + const defaultRequest = spec.buildRequests(bidRequests, bidderRequestDefault); + expect(JSON.parse(defaultRequest.data).userAgentClientHints).to.not.exist; + }); + + it('should use good mediaTypes banner sizes', function () { + const mediaTypesBannerSize = { + 'mediaTypes': { + 'banner': { + 'sizes': [300, 250] + } + } + }; + checkMediaTypesSizes(mediaTypesBannerSize, '300x250'); + }); + }); + + describe('Global Placement Id', function () { + let bidRequests = [ + { + 'bidder': 'greenbids', + 'params': { + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'creativeId': 'er2ee', + 'deviceWidth': 1680 + }, + { + 'bidder': 'greenbids', + 'params': { + }, + 'adUnitCode': 'adunit-code-2', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1f', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'creativeId': 'er2ef', + 'deviceWidth': 1680 + } + ]; + + it('should add gpid if ortb2Imp.ext.gpid is present and is non empty', function () { + const updatedBidRequests = bidRequests.map(function (bidRequest, index) { + return { + ...bidRequest, + ortb2Imp: { + ext: { + gpid: '1111/home-left-' + index + } + } + }; + } + ); + const request = spec.buildRequests(updatedBidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.data[0].gpid).to.equal('1111/home-left-0'); + expect(payload.data[1].gpid).to.equal('1111/home-left-1'); + }); + + it('should not add gpid if ortb2Imp.ext.gpid is present but empty', function () { + const updatedBidRequests = bidRequests.map(bidRequest => ({ + ...bidRequest, + ortb2Imp: { + ext: { + gpid: '' + } + } + })); + + const request = spec.buildRequests(updatedBidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + payload.data.forEach(bid => { + expect(bid).not.to.have.property('gpid'); + }); + }); + + it('should not add gpid if ortb2Imp.ext.gpid is not present', function () { + const updatedBidRequests = bidRequests.map(bidRequest => ({ + ...bidRequest, + ortb2Imp: { + ext: { + } + } + })); + + const request = spec.buildRequests(updatedBidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + payload.data.forEach(bid => { + expect(bid).not.to.have.property('gpid'); + }); + }); + + it('should add dsa info to payload if available', function () { + const bidRequestWithDsa = Object.assign({}, bidderRequestDefault, { + ortb2: { + regs: { + ext: { + dsa: { + dsarequired: '1', + pubrender: '2', + datatopub: '3', + transparency: [{ + domain: 'test.com', + dsaparams: [1, 2, 3] + }] + } + } + } + } + }); + + const requestWithDsa = spec.buildRequests(bidRequests, bidRequestWithDsa); + const payload = JSON.parse(requestWithDsa.data); + + expect(payload.dsa).to.exist; + expect(payload.dsa).to.deep.equal( + { + dsarequired: '1', + pubrender: '2', + datatopub: '3', + transparency: [{ + domain: 'test.com', + dsaparams: [1, 2, 3] + }] + } + ); + + const defaultRequest = spec.buildRequests(bidRequests, bidderRequestDefault); + expect(JSON.parse(defaultRequest.data).dsa).to.not.exist; + }); + }); + + describe('interpretResponse', function () { + it('should get correct bid responses', function () { + let bids = { + 'body': { + 'responses': [{ + 'ad': AD_SCRIPT, + 'cpm': 0.5, + 'currency': 'USD', + 'height': 250, + 'bidId': '3ede2a3fa0db94', + 'ttl': 360, + 'width': 300, + 'creativeId': 'er2ee', + 'placementId': 4242 + }, { + 'ad': AD_SCRIPT, + 'cpm': 0.5, + 'currency': 'USD', + 'height': 200, + 'bidId': '4fef3b4gb1ec15', + 'ttl': 360, + 'width': 350, + 'creativeId': 'fs3ff', + 'placementId': 4242, + 'dealId': 'ABC_123', + 'ext': { + 'dsa': { + 'behalf': 'some-behalf', + 'paid': 'some-paid', + 'transparency': [{ + 'domain': 'test.com', + 'dsaparams': [1, 2, 3] + }], + 'adrender': 1 + } + } + }] + } + }; + let expectedResponse = [ + { + 'cpm': 0.5, + 'width': 300, + 'height': 250, + 'currency': 'USD', + 'netRevenue': true, + 'meta': { + advertiserDomains: [] + }, + 'ttl': 360, + 'ad': AD_SCRIPT, + 'requestId': '3ede2a3fa0db94', + 'creativeId': 'er2ee', + 'placementId': 4242 + }, { + 'cpm': 0.5, + 'width': 350, + 'height': 200, + 'currency': 'USD', + 'netRevenue': true, + 'meta': { + advertiserDomains: [], + dsa: { + behalf: 'some-behalf', + paid: 'some-paid', + transparency: [{ + domain: 'test.com', + dsaparams: [1, 2, 3] + }], + adrender: 1 + } + }, + 'ttl': 360, + 'ad': AD_SCRIPT, + 'requestId': '4fef3b4gb1ec15', + 'creativeId': 'fs3ff', + 'placementId': 4242, + 'dealId': 'ABC_123' + } + ] + ; + + let result = spec.interpretResponse(bids); + expect(result).to.eql(expectedResponse); + }); + + it('handles nobid responses', function () { + let bids = { + 'body': { + 'responses': [] + } + }; + + let result = spec.interpretResponse(bids); + expect(result.length).to.equal(0); + }); + }); +}); + +let bidderRequestDefault = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000 +}; + +let bidRequests = [ + { + 'bidder': 'greenbids', + 'params': { + 'placementId': 4242 + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'creativeId': 'er2ee', + 'deviceWidth': 1680 + } +]; + +function checkMediaTypesSizes(mediaTypes, expectedSizes) { + const bidRequestWithBannerSizes = Object.assign(bidRequests[0], mediaTypes); + const requestWithBannerSizes = spec.buildRequests([bidRequestWithBannerSizes], bidderRequestDefault); + const payloadWithBannerSizes = JSON.parse(requestWithBannerSizes.data); + + return payloadWithBannerSizes.data.forEach(bid => { + if (Array.isArray(expectedSizes)) { + expect(JSON.stringify(bid.sizes)).to.equal(JSON.stringify(expectedSizes)); + } else { + expect(bid.sizes[0]).to.equal(expectedSizes); + } + }); +}