diff --git a/modules/beopBidAdapter.js b/modules/beopBidAdapter.js index f8a53a293de..f356a1e9959 100644 --- a/modules/beopBidAdapter.js +++ b/modules/beopBidAdapter.js @@ -1,153 +1,214 @@ -import { deepAccess, isArray, logWarn, triggerPixel, buildUrl, logInfo, getValue, getBidIdParameter } from '../src/utils.js'; -import { getRefererInfo } from '../src/refererDetection.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { config } from '../src/config.js'; -const BIDDER_CODE = 'beop'; -const ENDPOINT_URL = 'https://hb.beop.io/bid'; +import { + deepAccess, + isArray, + logWarn, + triggerPixel, + buildUrl, + logInfo, + getValue, + getBidIdParameter, +} from "../src/utils.js"; +import { getRefererInfo } from "../src/refererDetection.js"; +import { registerBidder } from "../src/adapters/bidderFactory.js"; +import { config } from "../src/config.js"; +const BIDDER_CODE = "beop"; +const ENDPOINT_URL = "https://hb.beop.io/bid"; const TCF_VENDOR_ID = 666; -const validIdRegExp = /^[0-9a-fA-F]{24}$/ +const validIdRegExp = /^[0-9a-fA-F]{24}$/; export const spec = { code: BIDDER_CODE, gvlid: TCF_VENDOR_ID, - aliases: ['bp'], + aliases: ["bp"], /** - * Test if the bid request is valid. - * - * @param {bid} : The Bid params - * @return boolean true if the bid request is valid (aka contains a valid accountId or networkId and is open for BANNER), false otherwise. - */ - isBidRequestValid: function(bid) { + * Test if the bid request is valid. + * + * @param {bid} : The Bid params + * @return boolean true if the bid request is valid (aka contains a valid accountId or networkId and is open for BANNER), false otherwise. + */ + isBidRequestValid: function (bid) { const id = bid.params.accountId || bid.params.networkId; - if (id === null || typeof id === 'undefined') { - return false + if (id === null || typeof id === "undefined") { + return false; } if (!validIdRegExp.test(id)) { - return false + return false; } - return bid.mediaTypes.banner !== null && typeof bid.mediaTypes.banner !== 'undefined'; + return ( + bid.mediaTypes.banner !== null && + typeof bid.mediaTypes.banner !== "undefined" + ); }, /** - * Create a BeOp server request from a list of BidRequest - * - * @param {validBidRequests[], ...} : The array of validated bidRequests - * @param {... , bidderRequest} : Common params for each bidRequests - * @return ServerRequest Info describing the request to the BeOp's server - */ - buildRequests: function(validBidRequests, bidderRequest) { + * Create a BeOp server request from a list of BidRequest + * + * @param {validBidRequests[], ...} : The array of validated bidRequests + * @param {... , bidderRequest} : Common params for each bidRequests + * @return ServerRequest Info describing the request to the BeOp's server + */ + buildRequests: function (validBidRequests, bidderRequest) { const slots = validBidRequests.map(beOpRequestSlotsMaker); const pageUrl = getPageUrl(bidderRequest.refererInfo, window); const gdpr = bidderRequest.gdprConsent; const firstSlot = slots[0]; + const kwdsFromRequest = bidderRequest.ortb2?.site?.keywords; + let keywords = []; + if (kwdsFromRequest) { + if (isArray(kwdsFromRequest)) { + keywords = kwdsFromRequest; + } else { + if (kwdsFromRequest.indexOf(",") != -1) { + keywords = kwdsFromRequest.split(","); + } else { + keywords.push(kwdsFromRequest); + } + } + } + const payloadObject = { at: new Date().toString(), nid: firstSlot.nid, nptnid: firstSlot.nptnid, pid: firstSlot.pid, url: pageUrl, - lang: (window.navigator.language || window.navigator.languages[0]), - kwds: bidderRequest.ortb2?.site?.keywords || [], + lang: window.navigator.language || window.navigator.languages[0], + kwds: keywords, dbg: false, slts: slots, - is_amp: deepAccess(bidderRequest, 'referrerInfo.isAmp'), - tc_string: (gdpr && gdpr.gdprApplies) ? gdpr.consentString : null, + is_amp: deepAccess(bidderRequest, "referrerInfo.isAmp"), + tc_string: gdpr && gdpr.gdprApplies ? gdpr.consentString : null, }; const payloadString = JSON.stringify(payloadObject); return { - method: 'POST', + method: "POST", url: ENDPOINT_URL, - data: payloadString - } + data: payloadString, + }; }, - interpretResponse: function(serverResponse, request) { - if (serverResponse && serverResponse.body && isArray(serverResponse.body.bids) && serverResponse.body.bids.length > 0) { + interpretResponse: function (serverResponse, request) { + if ( + serverResponse && + serverResponse.body && + isArray(serverResponse.body.bids) && + serverResponse.body.bids.length > 0 + ) { return serverResponse.body.bids; } return []; }, - onTimeout: function(timeoutData) { - if (timeoutData === null || typeof timeoutData === 'undefined' || Object.keys(timeoutData).length === 0) { + onTimeout: function (timeoutData) { + if ( + timeoutData === null || + typeof timeoutData === "undefined" || + Object.keys(timeoutData).length === 0 + ) { return; } - let trackingParams = buildTrackingParams(timeoutData, 'timeout', timeoutData.timeout); + let trackingParams = buildTrackingParams( + timeoutData, + "timeout", + timeoutData.timeout + ); - logWarn(BIDDER_CODE + ': timed out request'); - triggerPixel(buildUrl({ - protocol: 'https', - hostname: 't.beop.io', - pathname: '/bid', - search: trackingParams - })); + logWarn(BIDDER_CODE + ": timed out request"); + triggerPixel( + buildUrl({ + protocol: "https", + hostname: "t.beop.io", + pathname: "/bid", + search: trackingParams, + }) + ); }, - onBidWon: function(bid) { - if (bid === null || typeof bid === 'undefined' || Object.keys(bid).length === 0) { + onBidWon: function (bid) { + if ( + bid === null || + typeof bid === "undefined" || + Object.keys(bid).length === 0 + ) { return; } - let trackingParams = buildTrackingParams(bid, 'won', bid.cpm); + let trackingParams = buildTrackingParams(bid, "won", bid.cpm); - logInfo(BIDDER_CODE + ': won request'); - triggerPixel(buildUrl({ - protocol: 'https', - hostname: 't.beop.io', - pathname: '/bid', - search: trackingParams - })); + logInfo(BIDDER_CODE + ": won request"); + triggerPixel( + buildUrl({ + protocol: "https", + hostname: "t.beop.io", + pathname: "/bid", + search: trackingParams, + }) + ); }, - onSetTargeting: function(bid) {} -} + onSetTargeting: function (bid) {}, +}; function buildTrackingParams(data, info, value) { let params = Array.isArray(data.params) ? data.params[0] : data.params; const pageUrl = getPageUrl(null, window); return { - pid: params.accountId === undefined ? data.ad.match(/account: \“([a-f\d]{24})\“/)[1] : params.accountId, + pid: + params.accountId === undefined + ? data.ad.match(/account: \“([a-f\d]{24})\“/)[1] + : params.accountId, nid: params.networkId, nptnid: params.networkPartnerId, bid: data.bidId || data.requestId, sl_n: data.adUnitCode, aid: data.auctionId, - se_ca: 'bid', + se_ca: "bid", se_ac: info, se_va: value, - url: pageUrl + url: pageUrl, }; } function beOpRequestSlotsMaker(bid) { - const bannerSizes = deepAccess(bid, 'mediaTypes.banner.sizes'); - const publisherCurrency = config.getConfig('currency.adServerCurrency') || getValue(bid.params, 'currency') || 'EUR'; + const bannerSizes = deepAccess(bid, "mediaTypes.banner.sizes"); + const publisherCurrency = + config.getConfig("currency.adServerCurrency") || + getValue(bid.params, "currency") || + "EUR"; let floor; - if (typeof bid.getFloor === 'function') { - const floorInfo = bid.getFloor({currency: publisherCurrency, mediaType: 'banner', size: [1, 1]}); - if (typeof floorInfo === 'object' && floorInfo.currency === publisherCurrency && !isNaN(parseFloat(floorInfo.floor))) { + if (typeof bid.getFloor === "function") { + const floorInfo = bid.getFloor({ + currency: publisherCurrency, + mediaType: "banner", + size: [1, 1], + }); + if ( + typeof floorInfo === "object" && + floorInfo.currency === publisherCurrency && + !isNaN(parseFloat(floorInfo.floor)) + ) { floor = parseFloat(floorInfo.floor); } } return { sizes: isArray(bannerSizes) ? bannerSizes : bid.sizes, flr: floor, - pid: getValue(bid.params, 'accountId'), - nid: getValue(bid.params, 'networkId'), - nptnid: getValue(bid.params, 'networkPartnerId'), - bid: getBidIdParameter('bidId', bid), - brid: getBidIdParameter('bidderRequestId', bid), - name: getBidIdParameter('adUnitCode', bid), - aid: getBidIdParameter('auctionId', bid), - tid: getBidIdParameter('transactionId', bid), - brc: getBidIdParameter('bidRequestsCount', bid), - bdrc: getBidIdParameter('bidderRequestCount', bid), - bwc: getBidIdParameter('bidderWinsCount', bid), - } + pid: getValue(bid.params, "accountId"), + nid: getValue(bid.params, "networkId"), + nptnid: getValue(bid.params, "networkPartnerId"), + bid: getBidIdParameter("bidId", bid), + brid: getBidIdParameter("bidderRequestId", bid), + name: getBidIdParameter("adUnitCode", bid), + aid: getBidIdParameter("auctionId", bid), + tid: getBidIdParameter("transactionId", bid), + brc: getBidIdParameter("bidRequestsCount", bid), + bdrc: getBidIdParameter("bidderRequestCount", bid), + bwc: getBidIdParameter("bidderWinsCount", bid), + }; } -const protocolRelativeRegExp = /^\/\// +const protocolRelativeRegExp = /^\/\//; function isProtocolRelativeUrl(url) { return url && url.match(protocolRelativeRegExp) != null; } -const withProtocolRegExp = /[a-z]{1,}:\/\// +const withProtocolRegExp = /[a-z]{1,}:\/\//; function isNoProtocolUrl(url) { return url && url.match(withProtocolRegExp) == null; } @@ -168,7 +229,7 @@ function ensureProtocolInUrl(url, defaultProtocol) { */ function safeDeepAccess(obj, path) { try { - return deepAccess(obj, path) + return deepAccess(obj, path); } catch (_e) { return null; } @@ -176,10 +237,15 @@ function safeDeepAccess(obj, path) { function getPageUrl(refererInfo, window) { refererInfo = refererInfo || getRefererInfo(); - let pageUrl = refererInfo.canonicalUrl || safeDeepAccess(window, 'top.location.href') || deepAccess(window, 'location.href'); + let pageUrl = + refererInfo.canonicalUrl || + safeDeepAccess(window, "top.location.href") || + deepAccess(window, "location.href"); // Ensure the protocol is present (looks like sometimes the extracted pageUrl misses it) if (pageUrl != null) { - const defaultProtocol = safeDeepAccess(window, 'top.location.protocol') || deepAccess(window, 'location.protocol'); + const defaultProtocol = + safeDeepAccess(window, "top.location.protocol") || + deepAccess(window, "location.protocol"); pageUrl = ensureProtocolInUrl(pageUrl, defaultProtocol); } return pageUrl; diff --git a/test/spec/modules/beopBidAdapter_spec.js b/test/spec/modules/beopBidAdapter_spec.js index c6001c3ba2e..beb162a58bd 100644 --- a/test/spec/modules/beopBidAdapter_spec.js +++ b/test/spec/modules/beopBidAdapter_spec.js @@ -1,219 +1,278 @@ -import { expect } from 'chai'; -import { spec } from 'modules/beopBidAdapter.js'; -import { newBidder } from 'src/adapters/bidderFactory.js'; -import { config } from 'src/config.js'; -const utils = require('src/utils'); +import { expect } from "chai"; +import { spec } from "modules/beopBidAdapter.js"; +import { newBidder } from "src/adapters/bidderFactory.js"; +import { config } from "src/config.js"; +const utils = require("src/utils"); -const ENDPOINT = 'https://hb.beop.io/bid'; +const ENDPOINT = "https://hb.beop.io/bid"; let validBid = { - 'bidder': 'beop', - 'params': { - 'accountId': '5a8af500c9e77c00017e4cad' + bidder: "beop", + params: { + accountId: "5a8af500c9e77c00017e4cad", }, - 'adUnitCode': 'bellow-article', - 'mediaTypes': { - 'banner': { - 'sizes': [[1, 1]] - } + adUnitCode: "bellow-article", + mediaTypes: { + banner: { + sizes: [[1, 1]], + }, }, - 'getFloor': () => { + getFloor: () => { return { - currency: 'USD', + currency: "USD", floor: 10, - } + }; }, - 'bidId': '30b31c1838de1e', - 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475', - 'transactionId': '04f2659e-c005-4eb1-a57c-fa93145e3843', - 'creativeId': 'er2ee' + bidId: "30b31c1838de1e", + bidderRequestId: "22edbae2733bf6", + auctionId: "1d1a030790a475", + transactionId: "04f2659e-c005-4eb1-a57c-fa93145e3843", + creativeId: "er2ee", }; -describe('BeOp Bid Adapter tests', () => { +describe("BeOp Bid Adapter tests", () => { afterEach(function () { config.setConfig({}); }); const adapter = newBidder(spec); - describe('inherited functions', () => { - it('exists and is a function', () => { - expect(adapter.callBids).to.exist.and.to.be.a('function'); + describe("inherited functions", () => { + it("exists and is a function", () => { + expect(adapter.callBids).to.exist.and.to.be.a("function"); }); }); - describe('isBidRequestValid', function() { - it('should return true when accountId params found', function () { + describe("isBidRequestValid", function () { + it("should return true when accountId params found", function () { expect(spec.isBidRequestValid(validBid)).to.equal(true); }); - it('should return true if no accountId but networkId', function () { + it("should return true if no accountId but networkId", function () { let bid = Object.assign({}, validBid); delete bid.params; bid.params = { - 'networkId': '5a8af500c9e77c00017e4aaa' + networkId: "5a8af500c9e77c00017e4aaa", }; expect(spec.isBidRequestValid(bid)).to.equal(true); }); - it('should return false if neither account or network id param found', function () { + it("should return false if neither account or network id param found", function () { let bid = Object.assign({}, validBid); delete bid.params; bid.params = { - 'someId': '5a8af500c9e77c00017e4aaa' + someId: "5a8af500c9e77c00017e4aaa", }; expect(spec.isBidRequestValid(bid)).to.equal(false); }); - it('should return false if account Id param is not an ObjectId', function () { + it("should return false if account Id param is not an ObjectId", function () { let bid = Object.assign({}, validBid); delete bid.params; bid.params = { - 'someId': '12345' + someId: "12345", }; expect(spec.isBidRequestValid(bid)).to.equal(false); }); - it('should return false if there is no banner media type', function () { + it("should return false if there is no banner media type", function () { let bid = Object.assign({}, validBid); delete bid.mediaTypes; bid.mediaTypes = { - 'native': { - 'sizes': [[1, 1]] - } + native: { + sizes: [[1, 1]], + }, }; expect(spec.isBidRequestValid(bid)).to.equal(false); }); }); - describe('buildRequests', function () { + describe("buildRequests", function () { let bidRequests = []; bidRequests.push(validBid); - it('should build the request', function () { - config.setConfig({'currency': {'adServerCurrency': 'USD'}}); + it("should build the request", function () { + config.setConfig({ currency: { adServerCurrency: "USD" } }); const request = spec.buildRequests(bidRequests, {}); const payload = JSON.parse(request.data); const url = request.url; expect(url).to.equal(ENDPOINT); expect(payload.pid).to.exist; - expect(payload.pid).to.equal('5a8af500c9e77c00017e4cad'); + expect(payload.pid).to.equal("5a8af500c9e77c00017e4cad"); expect(payload.slts[0].name).to.exist; - expect(payload.slts[0].name).to.equal('bellow-article'); + expect(payload.slts[0].name).to.equal("bellow-article"); expect(payload.slts[0].flr).to.equal(10); }); - it('should call the endpoint with GDPR consent and pageURL info if found', function () { - let consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; - let bidderRequest = - { - 'gdprConsent': - { - 'gdprApplies': true, - 'consentString': consentString + it("should call the endpoint with GDPR consent and pageURL info if found", function () { + let consentString = "BOJ8RZsOJ8RZsABAB8AAAAAZ+A=="; + let bidderRequest = { + gdprConsent: { + gdprApplies: true, + consentString: consentString, + }, + refererInfo: { + canonicalUrl: "test.te", }, - 'refererInfo': - { - 'canonicalUrl': 'test.te' - } }; const request = spec.buildRequests(bidRequests, bidderRequest); const payload = JSON.parse(request.data); expect(payload.tc_string).to.exist; - expect(payload.tc_string).to.equal('BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='); + expect(payload.tc_string).to.equal("BOJ8RZsOJ8RZsABAB8AAAAAZ+A=="); expect(payload.url).to.exist; // check that the protocol is added correctly - expect(payload.url).to.equal('http://test.te'); + expect(payload.url).to.equal("http://test.te"); }); - it('should not prepend the protocol in page url if already present', function () { + it("should not prepend the protocol in page url if already present", function () { const bidderRequest = { - 'refererInfo': { - 'canonicalUrl': 'https://test.te' - } + refererInfo: { + canonicalUrl: "https://test.te", + }, }; const request = spec.buildRequests(bidRequests, bidderRequest); const payload = JSON.parse(request.data); expect(payload.url).to.exist; - expect(payload.url).to.equal('https://test.te'); + expect(payload.url).to.equal("https://test.te"); }); }); - describe('interpretResponse', function() { + describe("interpretResponse", function () { let serverResponse = { - 'body': { - 'bids': [ + body: { + bids: [ { - 'requestId': 'aaaa', - 'cpm': 1.0, - 'currency': 'EUR', - 'creativeId': '60f691be1515670a2a09aea2', - 'netRevenue': true, - 'width': 1, - 'height': 1, - 'ad': '
', - 'meta': { - 'advertiserId': '60f691be1515670a2a09aea1' - } - } - ] - } - } - it('should interpret the response by pushing it in the bids elem', function () { + requestId: "aaaa", + cpm: 1.0, + currency: "EUR", + creativeId: "60f691be1515670a2a09aea2", + netRevenue: true, + width: 1, + height: 1, + ad: '
', + meta: { + advertiserId: "60f691be1515670a2a09aea1", + }, + }, + ], + }, + }; + it("should interpret the response by pushing it in the bids elem", function () { const response = spec.interpretResponse(serverResponse, validBid); expect(response[0].ad).to.exist; expect(response[0].requestId).to.exist; - expect(response[0].requestId).to.equal('aaaa'); + expect(response[0].requestId).to.equal("aaaa"); }); }); - describe('timeout and bid won pixel trigger', function () { + describe("timeout and bid won pixel trigger", function () { let triggerPixelStub; beforeEach(function () { - triggerPixelStub = sinon.stub(utils, 'triggerPixel'); + triggerPixelStub = sinon.stub(utils, "triggerPixel"); }); afterEach(function () { utils.triggerPixel.restore(); }); - it('should call triggerPixel utils function when timed out is filled', function () { + it("should call triggerPixel utils function when timed out is filled", function () { spec.onTimeout({}); spec.onTimeout(); expect(triggerPixelStub.getCall(0)).to.be.null; - spec.onTimeout({params: {accountId: '5a8af500c9e77c00017e4cad'}, timeout: 2000}); + spec.onTimeout({ + params: { accountId: "5a8af500c9e77c00017e4cad" }, + timeout: 2000, + }); expect(triggerPixelStub.getCall(0)).to.not.be.null; - expect(triggerPixelStub.getCall(0).args[0]).to.exist.and.to.include('https://t.beop.io'); - expect(triggerPixelStub.getCall(0).args[0]).to.include('se_ca=bid'); - expect(triggerPixelStub.getCall(0).args[0]).to.include('se_ac=timeout'); - expect(triggerPixelStub.getCall(0).args[0]).to.include('pid=5a8af500c9e77c00017e4cad'); + expect(triggerPixelStub.getCall(0).args[0]).to.exist.and.to.include( + "https://t.beop.io" + ); + expect(triggerPixelStub.getCall(0).args[0]).to.include("se_ca=bid"); + expect(triggerPixelStub.getCall(0).args[0]).to.include("se_ac=timeout"); + expect(triggerPixelStub.getCall(0).args[0]).to.include( + "pid=5a8af500c9e77c00017e4cad" + ); }); - it('should call triggerPixel utils function on bid won', function () { + it("should call triggerPixel utils function on bid won", function () { spec.onBidWon({}); spec.onBidWon(); expect(triggerPixelStub.getCall(0)).to.be.null; - spec.onBidWon({params: {accountId: '5a8af500c9e77c00017e4cad'}, cpm: 1.2}); + spec.onBidWon({ + params: { accountId: "5a8af500c9e77c00017e4cad" }, + cpm: 1.2, + }); expect(triggerPixelStub.getCall(0)).to.not.be.null; - expect(triggerPixelStub.getCall(0).args[0]).to.exist.and.to.include('https://t.beop.io'); - expect(triggerPixelStub.getCall(0).args[0]).to.include('se_ca=bid'); - expect(triggerPixelStub.getCall(0).args[0]).to.include('se_ac=won'); - expect(triggerPixelStub.getCall(0).args[0]).to.exist.and.to.include('pid=5a8af500c9e77c00017e4cad'); + expect(triggerPixelStub.getCall(0).args[0]).to.exist.and.to.include( + "https://t.beop.io" + ); + expect(triggerPixelStub.getCall(0).args[0]).to.include("se_ca=bid"); + expect(triggerPixelStub.getCall(0).args[0]).to.include("se_ac=won"); + expect(triggerPixelStub.getCall(0).args[0]).to.exist.and.to.include( + "pid=5a8af500c9e77c00017e4cad" + ); }); - it('should call triggerPixel utils function on bid won and work even if params is an array', function () { + it("should call triggerPixel utils function on bid won and work even if params is an array", function () { spec.onBidWon({}); spec.onBidWon(); expect(triggerPixelStub.getCall(0)).to.be.null; - spec.onBidWon({params: [{accountId: '5a8af500c9e77c00017e4cad'}], cpm: 1.2}); + spec.onBidWon({ + params: [{ accountId: "5a8af500c9e77c00017e4cad" }], + cpm: 1.2, + }); expect(triggerPixelStub.getCall(0)).to.not.be.null; - expect(triggerPixelStub.getCall(0).args[0]).to.exist.and.to.include('https://t.beop.io'); - expect(triggerPixelStub.getCall(0).args[0]).to.include('se_ca=bid'); - expect(triggerPixelStub.getCall(0).args[0]).to.include('se_ac=won'); - expect(triggerPixelStub.getCall(0).args[0]).to.exist.and.to.include('pid=5a8af500c9e77c00017e4cad'); + expect(triggerPixelStub.getCall(0).args[0]).to.exist.and.to.include( + "https://t.beop.io" + ); + expect(triggerPixelStub.getCall(0).args[0]).to.include("se_ca=bid"); + expect(triggerPixelStub.getCall(0).args[0]).to.include("se_ac=won"); + expect(triggerPixelStub.getCall(0).args[0]).to.exist.and.to.include( + "pid=5a8af500c9e77c00017e4cad" + ); + }); + }); + + describe("Ensure keywords is always array of string", function () { + let bidRequests = []; + bidRequests.push(validBid); + + it("should work with keywords as an array", function () { + config.setConfig({ + currency: { adServerCurrency: "USD" }, + ortb: { site: { keywords: ["a", "b"] } }, + }); + const request = spec.buildRequests(bidRequests, {}); + const payload = JSON.parse(request.data); + const url = request.url; + expect(payload.kwd).to.exist; + expect(payload.kwd).to.equal(["a", "b"]); + }); + + it("should work with keywords as a string", function () { + config.setConfig({ + currency: { adServerCurrency: "USD" }, + ortb: { site: { keywords: "list of keywords" } }, + }); + const request = spec.buildRequests(bidRequests, {}); + const payload = JSON.parse(request.data); + const url = request.url; + expect(payload.kwd).to.exist; + expect(payload.kwd).to.equal(["list of keywords"]); + }); + + it("should work with keywords as a string containing a comma", function () { + config.setConfig({ + currency: { adServerCurrency: "USD" }, + ortb: { site: { keywords: "list, of, keywords" } }, + }); + const request = spec.buildRequests(bidRequests, {}); + const payload = JSON.parse(request.data); + const url = request.url; + expect(payload.kwd).to.exist; + expect(payload.kwd).to.equal(["list", "of", "keywords"]); }); }); });