From 41c53f0805dde6609cadfd352e44ed0265f9fdac Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Tue, 8 Aug 2023 06:58:48 -0700 Subject: [PATCH 1/4] Convert ajax to fetch --- modules/geoedgeRtdProvider.js | 8 +- src/ajax.js | 208 ++++++---- test/mocks/xhr.js | 252 +++++++++++- test/spec/modules/adlooxAdServerVideo_spec.js | 6 +- test/spec/modules/adlooxRtdProvider_spec.js | 6 +- test/spec/modules/adqueryIdSystem_spec.js | 2 +- test/spec/modules/categoryTranslation_spec.js | 16 +- .../conversantAnalyticsAdapter_spec.js | 8 +- test/spec/modules/currency_spec.js | 4 +- test/spec/modules/dmdIdSystem_spec.js | 4 +- test/spec/modules/euidIdSystem_spec.js | 19 +- test/spec/modules/feedadBidAdapter_spec.js | 2 +- test/spec/modules/geoedgeRtdProvider_spec.js | 29 +- .../spec/modules/greenbidsRtdProvider_spec.js | 15 +- test/spec/modules/hadronIdSystem_spec.js | 2 +- test/spec/modules/id5AnalyticsAdapter_spec.js | 8 +- test/spec/modules/id5IdSystem_spec.js | 50 +-- .../spec/modules/identityLinkIdSystem_spec.js | 2 - test/spec/modules/intentIqIdSystem_spec.js | 2 - .../modules/invisiblyAnalyticsAdapter_spec.js | 8 +- test/spec/modules/jwplayerRtdProvider_spec.js | 54 ++- .../modules/magniteAnalyticsAdapter_spec.js | 4 +- test/spec/modules/mgidRtdProvider_spec.js | 6 +- test/spec/modules/oguryBidAdapter_spec.js | 24 +- .../spec/modules/ooloAnalyticsAdapter_spec.js | 2 +- test/spec/modules/priceFloors_spec.js | 70 ++-- test/spec/modules/publinkIdSystem_spec.js | 2 +- .../modules/pubmaticAnalyticsAdapter_spec.js | 16 +- .../modules/pubwiseAnalyticsAdapter_spec.js | 10 +- test/spec/modules/teadsIdSystem_spec.js | 2 +- test/spec/modules/uid2IdSystem_helpers.js | 10 +- test/spec/modules/uid2IdSystem_spec.js | 48 +-- test/spec/modules/userId_spec.js | 2 +- .../zeta_global_sspAnalyticsAdapter_spec.js | 7 +- test/spec/unit/core/ajax_spec.js | 376 ++++++++++++++++++ test/spec/videoCache_spec.js | 8 +- 36 files changed, 936 insertions(+), 356 deletions(-) create mode 100644 test/spec/unit/core/ajax_spec.js diff --git a/modules/geoedgeRtdProvider.js b/modules/geoedgeRtdProvider.js index 6f910632fbc..c6d6daa39b5 100644 --- a/modules/geoedgeRtdProvider.js +++ b/modules/geoedgeRtdProvider.js @@ -227,9 +227,13 @@ export const geoedgeSubmodule = { onBidResponseEvent: conditionallyWrap }; -export function beforeInit() { +export function beforeInit(submod = submodule) { fetchWrapper(setWrapper); - submodule('realTimeData', geoedgeSubmodule); + submod('realTimeData', geoedgeSubmodule); } beforeInit(); + +export function reset() { + wrapperReady = false; +} diff --git a/src/ajax.js b/src/ajax.js index 5e926f3210d..bf847a3d712 100644 --- a/src/ajax.js +++ b/src/ajax.js @@ -1,99 +1,133 @@ -import { config } from './config.js'; -import { logMessage, logError, parseUrl, buildUrl, _each } from './utils.js'; +import {config} from './config.js'; +import {buildUrl, logError, parseUrl} from './utils.js'; -const XHR_DONE = 4; +export const dep = { + fetch: window.fetch, + makeRequest: (r, o) => new Request(r, o), + timeout(timeout, resource) { + const ctl = new AbortController(); + let cancelTimer = setTimeout(() => { + ctl.abort(); + logError(`Request timeout after ${timeout}ms`, resource); + cancelTimer = null; + }, timeout); + return { + signal: ctl.signal, + done() { + cancelTimer && clearTimeout(cancelTimer) + } + } + } +} + +const GET = 'GET'; +const POST = 'POST'; +const CTYPE = 'Content-Type'; /** - * Simple IE9+ and cross-browser ajax request function - * Note: x-domain requests in IE9 do not support the use of cookies - * - * @param url string url - * @param callback {object | function} callback - * @param data mixed data - * @param options object + * transform legacy `ajax` parameters into a fetch request. + * @returns {Request} */ -export const ajax = ajaxBuilder(); - -export function ajaxBuilder(timeout = 3000, {request, done} = {}) { - return function(url, callback, data, options = {}) { - try { - let x; - let method = options.method || (data ? 'POST' : 'GET'); - let parser = document.createElement('a'); - parser.href = url; - - let callbacks = typeof callback === 'object' && callback !== null ? callback : { - success: function() { - logMessage('xhr success'); - }, - error: function(e) { - logError('xhr error', null, e); - } - }; +export function toFetchRequest(url, data, options = {}) { + const method = options.method || (data ? POST : GET); + if (method === GET && data) { + const urlInfo = parseUrl(url, options); + Object.assign(urlInfo.search, data); + url = buildUrl(urlInfo); + } + const headers = new Headers(options.customHeaders); + headers.set(CTYPE, options.contentType || 'text/plain'); + const opts = { + method, + headers + } + if (method !== GET && data) { + opts.body = data; + } + if (options.withCredentials) { + opts.credentials = 'include'; + } + return dep.makeRequest(url, opts); +} - if (typeof callback === 'function') { - callbacks.success = callback; - } +/** + * Return a version of `fetch` that automatically cancels requests after `timeout` milliseconds. + * + * If provided, `request` and `done` should be functions accepting a single argument. + * `request` is invoked at the beginning of each request, and `done` at the end; both are passed its origin. + * + * @returns {function(*, {}?): Promise} + */ +export function fetcherFactory(timeout = 3000, {request, done} = {}) { + let fetcher = (resource, options) => { + let to; + if (timeout != null && options?.signal == null && !config.getConfig('disableAjaxTimeout')) { + to = dep.timeout(timeout, resource); + options = Object.assign({signal: to.signal}, options); + } + let pm = dep.fetch(resource, options); + if (to?.done != null) pm = pm.finally(to.done); + return pm; + }; - x = new window.XMLHttpRequest(); + if (request != null || done != null) { + fetcher = ((fetch) => function (resource, options) { + const origin = new URL(resource?.url == null ? resource : resource.url, document.location).origin; + let req = fetch(resource, options); + request && request(origin); + if (done) req = req.finally(() => done(origin)); + return req; + })(fetcher); + } + return fetcher; +} - x.onreadystatechange = function () { - if (x.readyState === XHR_DONE) { - if (typeof done === 'function') { - done(parser.origin); - } - let status = x.status; - if ((status >= 200 && status < 300) || status === 304) { - callbacks.success(x.responseText, x); - } else { - callbacks.error(x.statusText, x); - } +function toXHR({status, statusText = '', headers, url}, responseText) { + let xml = 0; + return { + readyState: XMLHttpRequest.DONE, + status, + statusText, + responseText, + response: responseText, + responseType: '', + responseURL: url, + get responseXML() { + if (xml === 0) { + try { + xml = new DOMParser().parseFromString(responseText, headers?.get(CTYPE)?.split(';')?.[0]) + } catch (e) { + xml = null; + logError(e); } - }; - - // Disabled timeout temporarily to avoid xhr failed requests. https://github.com/prebid/Prebid.js/issues/2648 - if (!config.getConfig('disableAjaxTimeout')) { - x.ontimeout = function () { - logError(' xhr timeout after ', x.timeout, 'ms'); - }; } + return xml; + }, + getResponseHeader: (header) => headers?.has(header) ? headers.get(header) : null, + } +} - if (method === 'GET' && data) { - let urlInfo = parseUrl(url, options); - Object.assign(urlInfo.search, data); - url = buildUrl(urlInfo); - } - - x.open(method, url, true); - // IE needs timeout to be set after open - see #1410 - // Disabled timeout temporarily to avoid xhr failed requests. https://github.com/prebid/Prebid.js/issues/2648 - if (!config.getConfig('disableAjaxTimeout')) { - x.timeout = timeout; - } - - if (options.withCredentials) { - x.withCredentials = true; - } - _each(options.customHeaders, (value, header) => { - x.setRequestHeader(header, value); - }); - if (options.preflight) { - x.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); - } - x.setRequestHeader('Content-Type', options.contentType || 'text/plain'); - - if (typeof request === 'function') { - request(parser.origin); - } +/** + * attach legacy `ajax` callbacks to a fetch promise. + */ +export function attachCallbacks(fetchPm, callback) { + const {success, error} = typeof callback === 'object' && callback != null ? callback : { + success: typeof callback === 'function' ? callback : () => null, + error: (e, x) => logError('Network error', e, x) + }; + fetchPm.then(response => response.text().then((responseText) => [response, responseText])) + .then(([response, responseText]) => { + const xhr = toXHR(response, responseText); + response.ok || response.status === 304 ? success(responseText, xhr) : error(response.statusText, xhr); + }, () => error('', toXHR({status: 0}, ''))); +} - if (method === 'POST' && data) { - x.send(data); - } else { - x.send(); - } - } catch (error) { - logError('xhr construction', error); - typeof callback === 'object' && callback !== null && callback.error(error); - } - } +export function ajaxBuilder(timeout = 3000, {request, done} = {}) { + const fetcher = fetcherFactory(timeout, {request, done}); + return function (url, callback, data, options = {}) { + attachCallbacks(fetcher(toFetchRequest(url, data, options)), callback); + }; } + +export const ajax = ajaxBuilder(); +export const fetch = fetcherFactory(); diff --git a/test/mocks/xhr.js b/test/mocks/xhr.js index 424100f870c..c92b842d4ab 100644 --- a/test/mocks/xhr.js +++ b/test/mocks/xhr.js @@ -1,12 +1,223 @@ import {getUniqueIdentifierStr} from '../../src/utils.js'; +import {GreedyPromise} from '../../src/utils/promise.js'; +import {fakeXhr} from 'nise'; +import {dep} from 'src/ajax.js'; -export let server = sinon.createFakeServer(); -export let xhr = global.XMLHttpRequest; +export const xhr = sinon.useFakeXMLHttpRequest(); +export const server = mockFetchServer(); -beforeEach(function() { - server.restore(); - server = sinon.createFakeServer(); - xhr = global.XMLHttpRequest; +/** + * An (incomplete) replica of nise's fakeServer, but backing fetch used in ajax.js (rather than XHR). + */ +function mockFetchServer() { + const sandbox = sinon.createSandbox(); + const bodies = new WeakMap(); + const requests = []; + const {DONE, UNSENT} = XMLHttpRequest; + + function makeRequest(resource, options) { + const requestBody = options?.body || bodies.get(resource); + const request = new Request(resource, options); + bodies.set(request, requestBody); + return request; + } + + function mockXHR(resource, options) { + let resolve, reject; + const promise = new GreedyPromise((res, rej) => { + resolve = res; + reject = rej; + }); + + function error(reason = new TypeError('Failed to fetch')) { + mockReq.status = 0; + reject(reason); + } + + const request = makeRequest(resource, options); + request.signal.onabort = () => error(new DOMException('The user aborted a request')); + let responseHeaders; + + const mockReq = { + fetch: { + request, + requestBody: bodies.get(request), + promise, + }, + readyState: UNSENT, + url: request.url, + method: request.method, + requestBody: bodies.get(request), + status: 0, + statusText: '', + requestHeaders: new Proxy(request.headers, { + get(target, prop) { + return target[prop] != null ? target[prop] : target.get(prop); + }, + has(target, prop) { + return target.has(prop); + } + }), + withCredentials: request.credentials === 'include', + setStatus(status) { + // nise replaces invalid status with 200 + status = typeof status === 'number' ? status : 200; + mockReq.status = status; + mockReq.statusText = fakeXhr.FakeXMLHttpRequest.statusCodes[status] || ''; + }, + setResponseHeaders(headers) { + responseHeaders = headers; + }, + setResponseBody(body) { + if (mockReq.status === 0) { + error(); + return; + } + const resp = Object.defineProperties(new Response(body, { + status: mockReq.status, + statusText: mockReq.statusText, + headers: responseHeaders || {}, + }), { + url: { + get: () => mockReq.fetch.request.url, + } + }); + mockReq.readyState = DONE; + // tests expect respond() to run everything immediately, + // so make body available syncronously + resp.text = () => GreedyPromise.resolve(body || ''); + Object.assign(mockReq.fetch, { + response: resp, + responseBody: body || '' + }) + resolve(resp); + }, + respond(status = 200, headers, body) { + mockReq.setStatus(status); + mockReq.setResponseHeaders(headers); + mockReq.setResponseBody(body); + }, + error + }; + return mockReq; + } + + let enabled = false; + let timeoutsEnabled = false; + + function enable() { + if (!enabled) { + sandbox.stub(dep, 'fetch').callsFake((resource, options) => { + const req = mockXHR(resource, options); + requests.push(req); + return req.fetch.promise; + }); + sandbox.stub(dep, 'makeRequest').callsFake(makeRequest); + const timeout = dep.timeout; + sandbox.stub(dep, 'timeout').callsFake(function () { + if (timeoutsEnabled) { + return timeout.apply(null, arguments); + } else { + return {}; + } + }); + enabled = true; + } + } + + enable(); + + const responders = []; + + function respondWith() { + let response, urlMatcher, methodMatcher; + urlMatcher = methodMatcher = () => true; + switch (arguments.length) { + case 1: + ([response] = arguments); + break; + case 2: + ([urlMatcher, response] = arguments); + break; + case 3: + ([methodMatcher, urlMatcher, response] = arguments); + methodMatcher = ((toMatch) => (method) => method === toMatch)(methodMatcher); + break; + default: + throw new Error('Invalid respondWith invocation'); + } + if (typeof urlMatcher.exec === 'function') { + urlMatcher = ((rx) => (url) => rx.exec(url)?.slice(1))(urlMatcher); + } else if (typeof urlMatcher === 'string') { + urlMatcher = ((toMatch) => (url) => url === toMatch)(urlMatcher); + } + responders.push((req) => { + if (req.readyState !== DONE && methodMatcher(req.method)) { + const arg = urlMatcher(req.url); + if (arg) { + if (typeof response === 'function') { + response(req, ...(Array.isArray(arg) ? arg : [])); + } else if (typeof response === 'string') { + req.respond(200, null, response); + } else { + req.respond.apply(req, response); + } + } + } + }); + } + + function resetState() { + requests.length = 0; + responders.length = 0; + timeoutsEnabled = false; + } + + return { + requests, + enable, + restore() { + resetState(); + sandbox.restore(); + enabled = false; + }, + reset() { + sandbox.resetHistory(); + resetState(); + }, + respondWith, + respond() { + if (arguments.length > 0) { + respondWith.apply(null, arguments); + } + requests.forEach(req => { + for (let i = responders.length - 1; i >= 0; i--) { + responders[i](req); + if (req.readyState === DONE) break; + } + if (req.readyState !== DONE) { + req.respond(404, {}, ''); + } + }); + }, + /** + * the timeout mechanism is quite different between XHR and fetch + * by default, mocked fetch does not time out - to reflect fakeServer XHRs + * note that many tests will fire requests without caring or waiting for their response - + * if they are timed out later, during unrelated tests, the log messages might interfere with their + * assertions + */ + get autoTimeout() { + return timeoutsEnabled; + }, + set autoTimeout(val) { + timeoutsEnabled = !!val; + } + }; +} + +beforeEach(function () { + server.reset(); }); const bid = getUniqueIdentifierStr().substring(4); @@ -20,12 +231,35 @@ afterEach(function () { return (s) => s.split('\n').map(s => `${preamble} ${s}`).join('\n'); })(); + function format(obj, body = null) { + if (obj == null) return obj; + const fmt = {}; + let node = obj; + while (node != null) { + Object.keys(node).forEach((k) => { + const val = obj[k]; + if (typeof val !== 'function' && !fmt.hasOwnProperty(k)) { + fmt[k] = val; + } + }); + node = Object.getPrototypeOf(node); + } + if (obj.headers != null) { + fmt.headers = Object.fromEntries(obj.headers.entries()) + } + fmt.body = body; + return fmt; + } + - console.log(prepend(`XHR mock state after failure (for test '${this.currentTest.fullTitle()}'): ${server.requests.length} requests`)) + console.log(prepend(`XHR mock state after failure (for test '${this.currentTest.fullTitle()}'): ${server.requests.length} requests`)); server.requests.forEach((req, i) => { console.log(prepend(`Request #${i}:`)); - console.log(prepend(JSON.stringify(req, null, 2))); - }) + console.log(prepend(JSON.stringify({ + request: format(req.fetch.request, req.fetch.requestBody), + response: format(req.fetch.response, req.fetch.responseBody) + }, null, 2))); + }); } }); /* eslint-enable */ diff --git a/test/spec/modules/adlooxAdServerVideo_spec.js b/test/spec/modules/adlooxAdServerVideo_spec.js index a071c6bbe3f..58277bc830d 100644 --- a/test/spec/modules/adlooxAdServerVideo_spec.js +++ b/test/spec/modules/adlooxAdServerVideo_spec.js @@ -1,11 +1,11 @@ import adapterManager from 'src/adapterManager.js'; import analyticsAdapter from 'modules/adlooxAnalyticsAdapter.js'; -import { ajax } from 'src/ajax.js'; import { buildVideoUrl } from 'modules/adlooxAdServerVideo.js'; import { expect } from 'chai'; import * as events from 'src/events.js'; import { targeting } from 'src/targeting.js'; import * as utils from 'src/utils.js'; +import {server} from '../../mocks/xhr.js'; const analyticsAdapterName = 'adloox'; @@ -199,11 +199,9 @@ describe('Adloox Ad Server Video', function () { }); describe('process VAST', function () { - let server = null; let BID = null; let getWinningBidsStub; beforeEach(function () { - server = sinon.createFakeServer(); BID = utils.deepClone(bid); getWinningBidsStub = sinon.stub(targeting, 'getWinningBids') getWinningBidsStub.withArgs(adUnit.code).returns([ BID ]); @@ -212,8 +210,6 @@ describe('Adloox Ad Server Video', function () { getWinningBidsStub.restore(); getWinningBidsStub = undefined; BID = null; - server.restore(); - server = null; }); it('should return URL unchanged for non-VAST', function (done) { diff --git a/test/spec/modules/adlooxRtdProvider_spec.js b/test/spec/modules/adlooxRtdProvider_spec.js index 5b99789981f..0e26ef1afdb 100644 --- a/test/spec/modules/adlooxRtdProvider_spec.js +++ b/test/spec/modules/adlooxRtdProvider_spec.js @@ -1,12 +1,12 @@ import adapterManager from 'src/adapterManager.js'; import analyticsAdapter from 'modules/adlooxAnalyticsAdapter.js'; import {auctionManager} from 'src/auctionManager.js'; -import { config as _config } from 'src/config.js'; import { expect } from 'chai'; import * as events from 'src/events.js'; import * as prebidGlobal from 'src/prebidGlobal.js'; import { subModuleObj as rtdProvider } from 'modules/adlooxRtdProvider.js'; import * as utils from 'src/utils.js'; +import {server} from '../../mocks/xhr.js'; const analyticsAdapterName = 'adloox'; @@ -139,16 +139,12 @@ describe('Adloox RTD Provider', function () { expect(analyticsAdapter.context).is.null; }); - let server = null; let CONFIG = null; beforeEach(function () { - server = sinon.createFakeServer(); CONFIG = utils.deepClone(config); }); afterEach(function () { CONFIG = null; - server.restore(); - server = null; }); it('should fetch segments', function (done) { diff --git a/test/spec/modules/adqueryIdSystem_spec.js b/test/spec/modules/adqueryIdSystem_spec.js index a6b4e9d1529..4a58a90af55 100644 --- a/test/spec/modules/adqueryIdSystem_spec.js +++ b/test/spec/modules/adqueryIdSystem_spec.js @@ -64,7 +64,7 @@ describe('AdqueryIdSystem', function () { const callback = adqueryIdSubmodule.getId(config).callback; callback(callbackSpy); const request = server.requests[0]; - expect(request.url).to.eq('https://bidder.adquery.io'); + expect(request.url).to.eq('https://bidder.adquery.io/'); request.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ qid: 'testqid' })); expect(callbackSpy.lastCall.lastArg).to.deep.equal({qid: 'testqid'}); }); diff --git a/test/spec/modules/categoryTranslation_spec.js b/test/spec/modules/categoryTranslation_spec.js index 2301d6aab1b..d4f6aa66c7d 100644 --- a/test/spec/modules/categoryTranslation_spec.js +++ b/test/spec/modules/categoryTranslation_spec.js @@ -2,18 +2,16 @@ import { getAdserverCategoryHook, initTranslation, storage } from 'modules/categ import { config } from 'src/config.js'; import * as utils from 'src/utils.js'; import { expect } from 'chai'; +import {server} from '../../mocks/xhr.js'; describe('category translation', function () { - let fakeTranslationServer; let getLocalStorageStub; beforeEach(function () { - fakeTranslationServer = sinon.fakeServer.create(); getLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); }); afterEach(function() { - fakeTranslationServer.reset(); getLocalStorageStub.restore(); config.resetConfig(); }); @@ -73,7 +71,7 @@ describe('category translation', function () { } })); initTranslation(); - expect(fakeTranslationServer.requests.length).to.equal(0); + expect(server.requests.length).to.equal(0); clock.restore(); }); @@ -86,15 +84,15 @@ describe('category translation', function () { } })); initTranslation(); - expect(fakeTranslationServer.requests.length).to.equal(1); + expect(server.requests.length).to.equal(1); clock.restore(); }); it('should use default mapping file if publisher has not defined in config', function () { getLocalStorageStub.returns(null); initTranslation('http://sample.com', 'somekey'); - expect(fakeTranslationServer.requests.length).to.equal(1); - expect(fakeTranslationServer.requests[0].url).to.equal('http://sample.com'); + expect(server.requests.length).to.equal(1); + expect(server.requests[0].url).to.equal('http://sample.com/'); }); it('should use publisher defined mapping file', function () { @@ -105,7 +103,7 @@ describe('category translation', function () { }); getLocalStorageStub.returns(null); initTranslation('http://sample.com', 'somekey'); - expect(fakeTranslationServer.requests.length).to.equal(2); - expect(fakeTranslationServer.requests[0].url).to.equal('http://sample.com'); + expect(server.requests.length).to.equal(2); + expect(server.requests[0].url).to.equal('http://sample.com/'); }); }); diff --git a/test/spec/modules/conversantAnalyticsAdapter_spec.js b/test/spec/modules/conversantAnalyticsAdapter_spec.js index ce134f7f6af..f425535ce73 100644 --- a/test/spec/modules/conversantAnalyticsAdapter_spec.js +++ b/test/spec/modules/conversantAnalyticsAdapter_spec.js @@ -3,6 +3,7 @@ import {expect} from 'chai'; import {default as conversantAnalytics, CNVR_CONSTANTS, cnvrHelper} from 'modules/conversantAnalyticsAdapter'; import * as utils from 'src/utils.js'; import * as prebidGlobal from 'src/prebidGlobal'; +import {server} from '../../mocks/xhr.js'; import constants from 'src/constants.json' @@ -10,14 +11,13 @@ let events = require('src/events'); describe('Conversant analytics adapter tests', function() { let sandbox; // sinon sandbox to make restoring all stubbed objects easier - let xhr; // xhr stub from sinon for capturing data sent via ajax let clock; // clock stub from sinon to mock our cache cleanup interval let logInfoStub; const PREBID_VERSION = '1.2'; const SITE_ID = 108060; - let requests = []; + let requests; const DATESTAMP = Date.now(); const VALID_CONFIGURATION = { @@ -36,10 +36,9 @@ describe('Conversant analytics adapter tests', function() { }; beforeEach(function () { + requests = server.requests; sandbox = sinon.sandbox.create(); sandbox.stub(events, 'getEvents').returns([]); // need to stub this otherwise unwanted events seem to get fired during testing - xhr = sandbox.useFakeXMLHttpRequest(); // allows us to capture ajax requests - xhr.onCreate = function (req) { requests.push(req); }; // save ajax requests in a private array for testing purposes let getGlobalStub = { version: PREBID_VERSION, getUserIds: function() { // userIdTargeting.js init() gets called on AUCTION_END so we need to mock this function. @@ -60,7 +59,6 @@ describe('Conversant analytics adapter tests', function() { afterEach(function () { sandbox.restore(); - requests = []; // clean up any requests in our ajax request capture array. conversantAnalytics.disableAnalytics(); }); diff --git a/test/spec/modules/currency_spec.js b/test/spec/modules/currency_spec.js index 88c640e38cc..f7c2580f3f3 100644 --- a/test/spec/modules/currency_spec.js +++ b/test/spec/modules/currency_spec.js @@ -14,6 +14,7 @@ import { } from 'modules/currency.js'; import {createBid} from '../../../src/bidfactory.js'; import CONSTANTS from '../../../src/constants.json'; +import {server} from '../../mocks/xhr.js'; var assert = require('chai').assert; var expect = require('chai').expect; @@ -30,12 +31,11 @@ describe('currency', function () { } beforeEach(function () { - fakeCurrencyFileServer = sinon.fakeServer.create(); + fakeCurrencyFileServer = server; ready.reset(); }); afterEach(function () { - fakeCurrencyFileServer.restore(); setConfig({}); }); diff --git a/test/spec/modules/dmdIdSystem_spec.js b/test/spec/modules/dmdIdSystem_spec.js index 3096a8e55f5..16c32f184a3 100644 --- a/test/spec/modules/dmdIdSystem_spec.js +++ b/test/spec/modules/dmdIdSystem_spec.js @@ -60,7 +60,7 @@ describe('Dmd ID System', function () { it('Should invoke callback with response from API call', function () { const callbackSpy = sinon.spy(); - const domain = utils.getWindowLocation() + const domain = utils.getWindowLocation().href; const callback = dmdIdSubmodule.getId(config).callback; callback(callbackSpy); const request = server.requests[0]; @@ -73,7 +73,7 @@ describe('Dmd ID System', function () { it('Should log error if API response is not valid', function () { const callbackSpy = sinon.spy(); - const domain = utils.getWindowLocation() + const domain = utils.getWindowLocation().href; const callback = dmdIdSubmodule.getId(config).callback; callback(callbackSpy); const request = server.requests[0]; diff --git a/test/spec/modules/euidIdSystem_spec.js b/test/spec/modules/euidIdSystem_spec.js index 9a016b0facd..4f6bacebe6a 100644 --- a/test/spec/modules/euidIdSystem_spec.js +++ b/test/spec/modules/euidIdSystem_spec.js @@ -1,13 +1,9 @@ -import {coreStorage, init, setSubmoduleRegistry, requestBidsHook} from 'modules/userId/index.js'; +import {coreStorage, init, setSubmoduleRegistry} from 'modules/userId/index.js'; import {config} from 'src/config.js'; -import * as utils from 'src/utils.js'; -import { euidIdSubmodule } from 'modules/euidIdSystem.js'; +import {euidIdSubmodule} from 'modules/euidIdSystem.js'; import 'modules/consentManagement.js'; import 'src/prebid.js'; -import { getGlobal } from 'src/prebidGlobal.js'; -import { server } from 'test/mocks/xhr.js'; -import { configureTimerInterceptors } from 'test/mocks/timers.js'; -import { cookieHelpers, runAuction, apiHelpers, setGdprApplies } from './uid2IdSystem_helpers.js'; +import {apiHelpers, cookieHelpers, runAuction, setGdprApplies} from './uid2IdSystem_helpers.js'; import {hook} from 'src/hook.js'; import {uninstall as uninstallGdprEnforcement} from 'modules/gdprEnforcement.js'; @@ -32,12 +28,15 @@ const makePrebidConfig = (params = null, extraSettings = {}, debug = false) => ( const apiUrl = 'https://prod.euid.eu/v2/token/refresh'; const headers = { 'Content-Type': 'application/json' }; const makeSuccessResponseBody = () => btoa(JSON.stringify({ status: 'success', body: { ...apiHelpers.makeTokenResponse(initialToken), advertising_token: refreshedToken } })); -const configureEuidResponse = (httpStatus, response) => server.respondWith('POST', apiUrl, (xhr) => xhr.respond(httpStatus, headers, response)); const expectToken = (bid, token) => expect(bid?.userId ?? {}).to.deep.include(makeEuidIdentityContainer(token)); const expectNoIdentity = (bid) => expect(bid).to.not.haveOwnProperty('userId'); describe('EUID module', function() { let suiteSandbox, testSandbox, timerSpy, fullTestTitle, restoreSubtleToUndefined = false; + let server; + + const configureEuidResponse = (httpStatus, response) => server.respondWith('POST', apiUrl, (xhr) => xhr.respond(httpStatus, headers, response)); + before(function() { uninstallGdprEnforcement(); hook.ready(); @@ -54,10 +53,12 @@ describe('EUID module', function() { if (restoreSubtleToUndefined) window.crypto.subtle = undefined; }); beforeEach(function() { + server = sinon.createFakeServer(); init(config); setSubmoduleRegistry([euidIdSubmodule]); }); afterEach(function() { + server.restore(); $$PREBID_GLOBAL$$.requestBids.removeAll(); config.resetConfig(); cookieHelpers.clearCookies(moduleCookieName, publisherCookieName); @@ -116,7 +117,7 @@ describe('EUID module', function() { const euidToken = apiHelpers.makeTokenResponse(initialToken, true, true); configureEuidResponse(200, makeSuccessResponseBody()); config.setConfig(makePrebidConfig({euidToken})); - apiHelpers.respondAfterDelay(1); + apiHelpers.respondAfterDelay(1, server); const bid = await runAuction(); expectToken(bid, refreshedToken); }); diff --git a/test/spec/modules/feedadBidAdapter_spec.js b/test/spec/modules/feedadBidAdapter_spec.js index 152adba9d00..cb81c6f06de 100644 --- a/test/spec/modules/feedadBidAdapter_spec.js +++ b/test/spec/modules/feedadBidAdapter_spec.js @@ -621,7 +621,7 @@ describe('FeedAdAdapter', function () { expect(call.url).to.equal('https://api.feedad.com/1/prebid/web/events'); expect(JSON.parse(call.requestBody)).to.deep.equal(expectedData); expect(call.method).to.equal('POST'); - expect(call.requestHeaders).to.include({'Content-Type': 'application/json;charset=utf-8'}); + expect(call.requestHeaders).to.include({'Content-Type': 'application/json'}); }) }); }); diff --git a/test/spec/modules/geoedgeRtdProvider_spec.js b/test/spec/modules/geoedgeRtdProvider_spec.js index eec1feff87a..e93031b1db9 100644 --- a/test/spec/modules/geoedgeRtdProvider_spec.js +++ b/test/spec/modules/geoedgeRtdProvider_spec.js @@ -1,6 +1,15 @@ import * as utils from '../../../src/utils.js'; -import * as hook from '../../../src/hook.js' -import { beforeInit, geoedgeSubmodule, setWrapper, wrapper, htmlPlaceholder, WRAPPER_URL, getClientUrl } from '../../../modules/geoedgeRtdProvider.js'; + +import { + reset, + beforeInit, + geoedgeSubmodule, + setWrapper, + wrapper, + htmlPlaceholder, + WRAPPER_URL, + getClientUrl +} from '../../../modules/geoedgeRtdProvider.js'; import { server } from '../../../test/mocks/xhr.js'; import * as events from '../../../src/events.js'; import CONSTANTS from '../../../src/constants.json'; @@ -39,21 +48,15 @@ let mockWrapper = `${htmlPlaceholder}`; describe('Geoedge RTD module', function () { describe('beforeInit', function () { - let submoduleStub; - - before(function () { - submoduleStub = sinon.stub(hook, 'submodule'); - }); - after(function () { - submoduleStub.restore(); - }); it('should fetch the wrapper', function () { - beforeInit(); + reset(); + beforeInit(() => null); let request = server.requests[0]; - let isWrapperRequest = request && request.url && request.url && request.url === WRAPPER_URL; - expect(isWrapperRequest).to.equal(true); + expect(request.url).to.equal(WRAPPER_URL); }); it('should register RTD submodule provider', function () { + const submoduleStub = sinon.stub(); + beforeInit(submoduleStub); expect(submoduleStub.calledWith('realTimeData', geoedgeSubmodule)).to.equal(true); }); }); diff --git a/test/spec/modules/greenbidsRtdProvider_spec.js b/test/spec/modules/greenbidsRtdProvider_spec.js index 7cb6c10ce48..cd93e9013c0 100644 --- a/test/spec/modules/greenbidsRtdProvider_spec.js +++ b/test/spec/modules/greenbidsRtdProvider_spec.js @@ -6,18 +6,9 @@ import { import { greenbidsSubmodule } from 'modules/greenbidsRtdProvider.js'; +import {server} from '../../mocks/xhr.js'; describe('greenbidsRtdProvider', () => { - let server; - - beforeEach(() => { - server = sinon.createFakeServer(); - }); - - afterEach(() => { - server.restore(); - }); - const endPoint = 't.greenbids.ai'; const SAMPLE_MODULE_CONFIG = { @@ -137,7 +128,6 @@ describe('greenbidsRtdProvider', () => { {'Content-Type': 'application/json'}, JSON.stringify(SAMPLE_RESPONSE_ADUNITS) ); - done(); }, 50); setTimeout(() => { @@ -152,6 +142,7 @@ describe('greenbidsRtdProvider', () => { expect(requestBids.adUnits[1].bids.map((bid) => bid.bidder)).to.include('rubicon'); expect(requestBids.adUnits[1].bids.map((bid) => bid.bidder)).to.include('openx'); expect(callback.calledOnce).to.be.true; + done(); }, 60); }); }); @@ -195,7 +186,6 @@ describe('greenbidsRtdProvider', () => { {'Content-Type': 'application/json'}, JSON.stringify({'failure': 'fail'}) ); - done(); }, 50); setTimeout(() => { @@ -204,6 +194,7 @@ describe('greenbidsRtdProvider', () => { expect(requestBids.adUnits[0].bids).to.have.length(3); expect(requestBids.adUnits[1].bids).to.have.length(3); expect(callback.calledOnce).to.be.true; + done(); }, 60); }); }); diff --git a/test/spec/modules/hadronIdSystem_spec.js b/test/spec/modules/hadronIdSystem_spec.js index c998ef2cf14..cc0118d4659 100644 --- a/test/spec/modules/hadronIdSystem_spec.js +++ b/test/spec/modules/hadronIdSystem_spec.js @@ -47,7 +47,7 @@ describe('HadronIdSystem', function () { const callback = hadronIdSubmodule.getId(config).callback; callback(callbackSpy); const request = server.requests[0]; - expect(request.url).to.eq('https://hadronid.publync.com?partner_id=0&_it=prebid'); + expect(request.url).to.eq('https://hadronid.publync.com/?partner_id=0&_it=prebid'); request.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ hadronId: 'testHadronId1' })); expect(callbackSpy.lastCall.lastArg).to.deep.equal({ id: { hadronId: 'testHadronId1' } }); }); diff --git a/test/spec/modules/id5AnalyticsAdapter_spec.js b/test/spec/modules/id5AnalyticsAdapter_spec.js index 83951c3a6e9..9cb7233ce7c 100644 --- a/test/spec/modules/id5AnalyticsAdapter_spec.js +++ b/test/spec/modules/id5AnalyticsAdapter_spec.js @@ -1,20 +1,18 @@ import adapterManager from '../../../src/adapterManager.js'; import id5AnalyticsAdapter from '../../../modules/id5AnalyticsAdapter.js'; import { expect } from 'chai'; -import sinon from 'sinon'; import * as events from '../../../src/events.js'; import constants from '../../../src/constants.json'; import { generateUUID } from '../../../src/utils.js'; +import {server} from '../../mocks/xhr.js'; const CONFIG_URL = 'https://api.id5-sync.com/analytics/12349/pbjs'; const INGEST_URL = 'https://test.me/ingest'; describe('ID5 analytics adapter', () => { - let server; let config; beforeEach(() => { - server = sinon.createFakeServer(); config = { options: { partnerId: 12349, @@ -22,10 +20,6 @@ describe('ID5 analytics adapter', () => { }; }); - afterEach(() => { - server.restore(); - }); - it('registers itself with the adapter manager', () => { const adapter = adapterManager.getAnalyticsAdapter('id5Analytics'); expect(adapter).to.exist; diff --git a/test/spec/modules/id5IdSystem_spec.js b/test/spec/modules/id5IdSystem_spec.js index 2d81c9b7b8d..dd284357abe 100644 --- a/test/spec/modules/id5IdSystem_spec.js +++ b/test/spec/modules/id5IdSystem_spec.js @@ -19,8 +19,8 @@ import {uspDataHandler} from 'src/adapterManager.js'; import 'src/prebid.js'; import {hook} from '../../../src/hook.js'; import {mockGdprConsent} from '../../helpers/consentData.js'; - -let expect = require('chai').expect; +import {server} from '../../mocks/xhr.js'; +import {expect} from 'chai'; describe('ID5 ID System', function () { const ID5_MODULE_NAME = 'id5Id'; @@ -268,7 +268,7 @@ describe('ID5 ID System', function () { }); it('should call the ID5 server and handle a valid response', function () { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) let config = getId5FetchConfig(); let submoduleResponse = callSubmoduleGetId(config, undefined, undefined); @@ -297,7 +297,7 @@ describe('ID5 ID System', function () { }); it('should call the ID5 server with gdpr data ', function () { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) let consentData = { gdprApplies: true, consentString: 'consentString', @@ -322,7 +322,7 @@ describe('ID5 ID System', function () { }); it('should call the ID5 server without gdpr data when gdpr not applies ', function () { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) let consentData = { gdprApplies: false, consentString: 'consentString' @@ -347,7 +347,7 @@ describe('ID5 ID System', function () { it('should call the ID5 server with us privacy consent', function () { let usPrivacyString = '1YN-'; uspDataHandler.setConsentData(usPrivacyString) - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) let consentData = { gdprApplies: true, consentString: 'consentString', @@ -371,7 +371,7 @@ describe('ID5 ID System', function () { }); it('should call the ID5 server with no signature field when no stored object', function () { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), undefined, undefined); return xhrServerMock.expectFetchRequest() @@ -384,7 +384,7 @@ describe('ID5 ID System', function () { }); it('should call the ID5 server for config with submodule config object', function () { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) let id5FetchConfig = getId5FetchConfig(); id5FetchConfig.params.extraParam = { x: 'X', @@ -408,7 +408,7 @@ describe('ID5 ID System', function () { }); it('should call the ID5 server for config with partner id being a string', function () { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) let id5FetchConfig = getId5FetchConfig(); id5FetchConfig.params.partner = '173'; let submoduleResponse = callSubmoduleGetId(id5FetchConfig, undefined, undefined); @@ -426,7 +426,7 @@ describe('ID5 ID System', function () { }); it('should call the ID5 server for config with overridden url', function () { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) let id5FetchConfig = getId5FetchConfig(); id5FetchConfig.params.configUrl = 'http://localhost/x/y/z' @@ -444,7 +444,7 @@ describe('ID5 ID System', function () { }); it('should call the ID5 server with additional data when provided', function () { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), undefined, undefined); return xhrServerMock.expectConfigRequest() @@ -478,7 +478,7 @@ describe('ID5 ID System', function () { }); it('should call the ID5 server with extensions', function () { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), undefined, undefined); return xhrServerMock.expectConfigRequest() @@ -515,7 +515,7 @@ describe('ID5 ID System', function () { }); it('should call the ID5 server with extensions fetched with POST', function () { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), undefined, undefined); return xhrServerMock.expectConfigRequest() @@ -561,7 +561,7 @@ describe('ID5 ID System', function () { }); it('should call the ID5 server with signature field from stored object', function () { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), undefined, ID5_STORED_OBJ); return xhrServerMock.expectFetchRequest() @@ -574,7 +574,7 @@ describe('ID5 ID System', function () { }); it('should call the ID5 server with pd field when pd config is set', function () { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) const pubData = 'b50ca08271795a8e7e4012813f23d505193d75c0f2e2bb99baa63aa822f66ed3'; let id5Config = getId5FetchConfig(); @@ -592,7 +592,7 @@ describe('ID5 ID System', function () { }); it('should call the ID5 server with no pd field when pd config is not set', function () { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) let id5Config = getId5FetchConfig(); id5Config.params.pd = undefined; @@ -608,7 +608,7 @@ describe('ID5 ID System', function () { }); it('should call the ID5 server with nb=1 when no stored value exists and reset after', function () { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) coreStorage.removeDataFromLocalStorage(ID5_NB_STORAGE_NAME); let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), undefined, ID5_STORED_OBJ); @@ -626,7 +626,7 @@ describe('ID5 ID System', function () { }); it('should call the ID5 server with incremented nb when stored value exists and reset after', function () { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) storeNbInCache(ID5_TEST_PARTNER_ID, 1); let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), undefined, ID5_STORED_OBJ); @@ -644,7 +644,7 @@ describe('ID5 ID System', function () { }); it('should call the ID5 server with ab_testing object when abTesting is turned on', function () { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) let id5Config = getId5FetchConfig(); id5Config.params.abTesting = {enabled: true, controlGroupPct: 0.234} @@ -661,7 +661,7 @@ describe('ID5 ID System', function () { }); it('should call the ID5 server without ab_testing object when abTesting is turned off', function () { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) let id5Config = getId5FetchConfig(); id5Config.params.abTesting = {enabled: false, controlGroupPct: 0.55} @@ -677,7 +677,7 @@ describe('ID5 ID System', function () { }); it('should call the ID5 server without ab_testing when when abTesting is not set', function () { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) let id5Config = getId5FetchConfig(); let submoduleResponse = callSubmoduleGetId(id5Config, undefined, ID5_STORED_OBJ); @@ -692,7 +692,7 @@ describe('ID5 ID System', function () { }); it('should store the privacy object from the ID5 server response', function () { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), undefined, ID5_STORED_OBJ); const privacy = { @@ -714,7 +714,7 @@ describe('ID5 ID System', function () { }); it('should not store a privacy object if not part of ID5 server response', function () { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) coreStorage.removeDataFromLocalStorage(ID5_PRIVACY_STORAGE_NAME); let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), undefined, ID5_STORED_OBJ); @@ -760,7 +760,7 @@ describe('ID5 ID System', function () { [false, 0] ].forEach(function ([isEnabled, expectedValue]) { it(`should check localStorage availability and log in request. Available=${isEnabled}`, () => { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) let config = getId5FetchConfig(); let submoduleResponse = callSubmoduleGetId(config, undefined, undefined); storage.localStorageIsEnabled.callsFake(() => isEnabled) @@ -878,7 +878,7 @@ describe('ID5 ID System', function () { }); it('should call ID5 servers with signature and incremented nb post auction if refresh needed', function () { - let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let xhrServerMock = new XhrServerMock(server) let initialLocalStorageValue = JSON.stringify(ID5_STORED_OBJ); storeInLocalStorage(ID5_STORAGE_NAME, initialLocalStorageValue, 1); storeInLocalStorage(`${ID5_STORAGE_NAME}_last`, expDaysStr(-1), 1); diff --git a/test/spec/modules/identityLinkIdSystem_spec.js b/test/spec/modules/identityLinkIdSystem_spec.js index 52e9f9171d6..9777ebe5501 100644 --- a/test/spec/modules/identityLinkIdSystem_spec.js +++ b/test/spec/modules/identityLinkIdSystem_spec.js @@ -111,10 +111,8 @@ describe('IdentityLinkId tests', function () { request.respond( 204, responseHeader, - '' ); expect(callBackSpy.calledOnce).to.be.true; - expect(request.response).to.equal(''); expect(logErrorStub.calledOnce).to.not.be.true; }); diff --git a/test/spec/modules/intentIqIdSystem_spec.js b/test/spec/modules/intentIqIdSystem_spec.js index d5ffbf92d68..ef174af416b 100644 --- a/test/spec/modules/intentIqIdSystem_spec.js +++ b/test/spec/modules/intentIqIdSystem_spec.js @@ -169,10 +169,8 @@ describe('IntentIQ tests', function () { request.respond( 204, responseHeader, - '' ); expect(callBackSpy.calledOnce).to.be.true; - expect(request.response).to.equal(''); }); it('should log an error and continue to callback if ajax request errors', function () { diff --git a/test/spec/modules/invisiblyAnalyticsAdapter_spec.js b/test/spec/modules/invisiblyAnalyticsAdapter_spec.js index 2c460156318..a8828515ffd 100644 --- a/test/spec/modules/invisiblyAnalyticsAdapter_spec.js +++ b/test/spec/modules/invisiblyAnalyticsAdapter_spec.js @@ -1,6 +1,7 @@ import invisiblyAdapter from 'modules/invisiblyAnalyticsAdapter.js'; import { expect } from 'chai'; import {expectEvents} from '../../helpers/analytics.js'; +import {server} from '../../mocks/xhr.js'; let events = require('src/events'); let constants = require('src/constants.json'); @@ -169,11 +170,7 @@ describe('Invisibly Analytics Adapter test suite', function () { describe('Invisibly Analytic tests specs', function () { beforeEach(function () { - xhr = sinon.useFakeXMLHttpRequest(); - requests = []; - xhr.onCreate = (xhr) => { - requests.push(xhr); - }; + requests = server.requests; sinon.stub(events, 'getEvents').returns([]); sinon.spy(invisiblyAdapter, 'track'); }); @@ -182,7 +179,6 @@ describe('Invisibly Analytics Adapter test suite', function () { invisiblyAdapter.disableAnalytics(); events.getEvents.restore(); invisiblyAdapter.track.restore(); - xhr.restore(); }); describe('Send all events as & when they are captured', function () { diff --git a/test/spec/modules/jwplayerRtdProvider_spec.js b/test/spec/modules/jwplayerRtdProvider_spec.js index 5a38a971e09..4638595e0d6 100644 --- a/test/spec/modules/jwplayerRtdProvider_spec.js +++ b/test/spec/modules/jwplayerRtdProvider_spec.js @@ -1,8 +1,19 @@ -import { fetchTargetingForMediaId, getVatFromCache, extractPublisherParams, - formatTargetingResponse, getVatFromPlayer, enrichAdUnits, addTargetingToBid, - fetchTargetingInformation, jwplayerSubmodule, getContentId, getContentSegments, getContentData, addOrtbSiteContent } from 'modules/jwplayerRtdProvider.js'; -import { server } from 'test/mocks/xhr.js'; -import { config as prebidConfig } from 'src/config.js'; +import { + addOrtbSiteContent, + addTargetingToBid, + enrichAdUnits, + extractPublisherParams, + fetchTargetingForMediaId, + fetchTargetingInformation, + formatTargetingResponse, + getContentData, + getContentId, + getContentSegments, + getVatFromCache, + getVatFromPlayer, + jwplayerSubmodule +} from 'modules/jwplayerRtdProvider.js'; +import {server} from 'test/mocks/xhr.js'; import {deepClone} from '../../../src/utils.js'; describe('jwplayerRtdProvider', function() { @@ -223,9 +234,6 @@ describe('jwplayerRtdProvider', function() { describe('Get Bid Request Data', function () { it('executes immediately while request is active if player has item', function () { const bidRequestSpy = sinon.spy(); - const fakeServer = sinon.createFakeServer(); - fakeServer.respondImmediately = false; - fakeServer.autoRespond = false; fetchTargetingForMediaId(mediaIdWithSegment); @@ -255,7 +263,7 @@ describe('jwplayerRtdProvider', function() { jwplayerSubmodule.getBidRequestData({ adUnits: [adUnit] }, bidRequestSpy); expect(bidRequestSpy.calledOnce).to.be.true; expect(bid.rtd.jwplayer).to.have.deep.property('targeting', expectedTargeting); - fakeServer.respond(); + server.respond(); expect(bidRequestSpy.calledOnce).to.be.true; }); }); @@ -271,22 +279,17 @@ describe('jwplayerRtdProvider', function() { } }; let bidRequestSpy; - let fakeServer; let clock; beforeEach(function () { bidRequestSpy = sinon.spy(); - fakeServer = sinon.createFakeServer(); - fakeServer.respondImmediately = false; - fakeServer.autoRespond = false; - clock = sinon.useFakeTimers(); }); afterEach(function () { clock.restore(); - fakeServer.respond(); + server.respond(); }); it('adds targeting when pending request succeeds', function () { @@ -318,7 +321,7 @@ describe('jwplayerRtdProvider', function() { expect(bid1).to.not.have.property('rtd'); expect(bid2).to.not.have.property('rtd'); - const request = fakeServer.requests[0]; + const request = server.requests[0]; request.respond( 200, responseHeader, @@ -358,7 +361,7 @@ describe('jwplayerRtdProvider', function() { }, bids }; - const request = fakeServer.requests[0]; + const request = server.requests[0]; request.respond( 200, responseHeader, @@ -443,7 +446,7 @@ describe('jwplayerRtdProvider', function() { expect(bid1).to.not.have.property('rtd'); expect(bid2).to.not.have.property('rtd'); - const request = fakeServer.requests[0]; + const request = server.requests[0]; request.respond( 200, responseHeader, @@ -518,7 +521,7 @@ describe('jwplayerRtdProvider', function() { const bid1 = bids[0]; expect(bid1).to.not.have.property('rtd'); - const request = fakeServer.requests[0]; + const request = server.requests[0]; request.respond( 200, responseHeader, @@ -931,7 +934,6 @@ describe('jwplayerRtdProvider', function() { describe('Get Bid Request Data', function () { const validMediaIDs = ['media_ID_1', 'media_ID_2', 'media_ID_3']; let bidRequestSpy; - let fakeServer; let clock; let bidReqConfig; @@ -971,16 +973,12 @@ describe('jwplayerRtdProvider', function() { bidRequestSpy = sinon.spy(); - fakeServer = sinon.createFakeServer(); - fakeServer.respondImmediately = false; - fakeServer.autoRespond = false; - clock = sinon.useFakeTimers(); }); afterEach(function () { clock.restore(); - fakeServer.respond(); + server.respond(); }); it('executes callback immediately when ad units are missing', function () { @@ -1003,9 +1001,9 @@ describe('jwplayerRtdProvider', function() { jwplayerSubmodule.getBidRequestData(bidReqConfig, bidRequestSpy); expect(bidRequestSpy.notCalled).to.be.true; - const req1 = fakeServer.requests[0]; - const req2 = fakeServer.requests[1]; - const req3 = fakeServer.requests[2]; + const req1 = server.requests[0]; + const req2 = server.requests[1]; + const req3 = server.requests[2]; req1.respond(); expect(bidRequestSpy.notCalled).to.be.true; diff --git a/test/spec/modules/magniteAnalyticsAdapter_spec.js b/test/spec/modules/magniteAnalyticsAdapter_spec.js index ae63f19f46b..304ce2ed7a5 100644 --- a/test/spec/modules/magniteAnalyticsAdapter_spec.js +++ b/test/spec/modules/magniteAnalyticsAdapter_spec.js @@ -550,7 +550,7 @@ describe('magnite analytics adapter', function () { expect(server.requests.length).to.equal(1); let request = server.requests[0]; - expect(request.url).to.equal('//localhost:9999/event'); + expect(request.url).to.match(/\/\/localhost:9999\/event/); let message = JSON.parse(request.requestBody); @@ -724,7 +724,7 @@ describe('magnite analytics adapter', function () { expect(server.requests.length).to.equal(1); let request = server.requests[0]; - expect(request.url).to.equal('//localhost:9999/event'); + expect(request.url).to.match(/\/\/localhost:9999\/event/); let message = JSON.parse(request.requestBody); diff --git a/test/spec/modules/mgidRtdProvider_spec.js b/test/spec/modules/mgidRtdProvider_spec.js index 4f70b4d8b7c..996875649b6 100644 --- a/test/spec/modules/mgidRtdProvider_spec.js +++ b/test/spec/modules/mgidRtdProvider_spec.js @@ -1,16 +1,14 @@ import { mgidSubmodule, storage } from '../../../modules/mgidRtdProvider.js'; import {expect} from 'chai'; import * as refererDetection from '../../../src/refererDetection'; +import {server} from '../../mocks/xhr.js'; describe('Mgid RTD submodule', () => { - let server; let clock; let getRefererInfoStub; let getDataFromLocalStorageStub; beforeEach(() => { - server = sinon.fakeServer.create(); - clock = sinon.useFakeTimers(); getRefererInfoStub = sinon.stub(refererDetection, 'getRefererInfo'); @@ -22,7 +20,6 @@ describe('Mgid RTD submodule', () => { }); afterEach(() => { - server.restore(); clock.restore(); getRefererInfoStub.restore(); getDataFromLocalStorageStub.restore(); @@ -309,7 +306,6 @@ describe('Mgid RTD submodule', () => { server.requests[0].respond( 204, {'Content-Type': 'application/json'}, - '{}' ); assert.deepEqual(reqBidsConfigObj.ortb2Fragments.global, {}); diff --git a/test/spec/modules/oguryBidAdapter_spec.js b/test/spec/modules/oguryBidAdapter_spec.js index bbe53855094..ed358af19b6 100644 --- a/test/spec/modules/oguryBidAdapter_spec.js +++ b/test/spec/modules/oguryBidAdapter_spec.js @@ -1,6 +1,7 @@ import { expect } from 'chai'; import { spec } from 'modules/oguryBidAdapter'; import * as utils from 'src/utils.js'; +import {server} from '../../mocks/xhr.js'; const BID_URL = 'https://mweb-hb.presage.io/api/header-bidding-request'; const TIMEOUT_URL = 'https://ms-ads-monitoring-events.presage.io/bid_timeout' @@ -851,20 +852,11 @@ describe('OguryBidAdapter', function () { }); describe('onBidWon', function() { - const nurl = 'https://fakewinurl.test'; - let xhr; + const nurl = 'https://fakewinurl.test/'; let requests; beforeEach(function() { - xhr = sinon.useFakeXMLHttpRequest(); - requests = []; - xhr.onCreate = (xhr) => { - requests.push(xhr); - }; - }) - - afterEach(function() { - xhr.restore() + requests = server.requests; }) it('Should not create nurl request if bid is undefined', function() { @@ -932,21 +924,15 @@ describe('OguryBidAdapter', function () { }) describe('onTimeout', function () { - let xhr; let requests; beforeEach(function() { - xhr = sinon.useFakeXMLHttpRequest(); - requests = []; - xhr.onCreate = (xhr) => { + requests = server.requests; + server.onCreate = (xhr) => { requests.push(xhr); }; }) - afterEach(function() { - xhr.restore() - }) - it('should send on bid timeout notification', function() { const bid = { ad: 'cookies', diff --git a/test/spec/modules/ooloAnalyticsAdapter_spec.js b/test/spec/modules/ooloAnalyticsAdapter_spec.js index 2515c713b14..1224c3f0740 100644 --- a/test/spec/modules/ooloAnalyticsAdapter_spec.js +++ b/test/spec/modules/ooloAnalyticsAdapter_spec.js @@ -663,7 +663,7 @@ describe('oolo Prebid Analytic', () => { events.emit(constants.EVENTS.AUCTION_INIT, { ...auctionInit }); - expect(server.requests[3].url).to.equal('https://pbjs.com') + expect(server.requests[3].url).to.equal('https://pbjs.com/') }) it('should send raw events based on server configuration', () => { diff --git a/test/spec/modules/priceFloors_spec.js b/test/spec/modules/priceFloors_spec.js index 64c871308a9..e2b8ca38792 100644 --- a/test/spec/modules/priceFloors_spec.js +++ b/test/spec/modules/priceFloors_spec.js @@ -22,6 +22,7 @@ import {auctionManager} from '../../../src/auctionManager.js'; import {stubAuctionIndex} from '../../helpers/indexStub.js'; import {guardTids} from '../../../src/adapters/bidderFactory.js'; import * as activities from '../../../src/activities/rules.js'; +import {server} from '../../mocks/xhr.js'; describe('the price floors module', function () { let logErrorSpy; @@ -593,16 +594,11 @@ describe('the price floors module', function () { adUnits, }); }; - let fakeFloorProvider; let actualAllowedFields = allowedFields; let actualFieldMatchingFunctions = fieldMatchingFunctions; const defaultAllowedFields = [...allowedFields]; const defaultMatchingFunctions = {...fieldMatchingFunctions}; - beforeEach(function() { - fakeFloorProvider = sinon.fakeServer.create(); - }); afterEach(function() { - fakeFloorProvider.restore(); exposedAdUnits = undefined; actualAllowedFields = [...defaultAllowedFields]; actualFieldMatchingFunctions = {...defaultMatchingFunctions}; @@ -986,7 +982,7 @@ describe('the price floors module', function () { }); }); it('Should continue auction of delay is hit without a response from floor provider', function () { - handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakefloorprovider.json//'}}); // start the auction it should delay and not immediately call `continueAuction` runStandardAuction(); @@ -1013,7 +1009,7 @@ describe('the price floors module', function () { fetchStatus: 'timeout', floorProvider: undefined }); - fakeFloorProvider.respond(); + server.respond(); }); it('It should fetch if config has url and bidRequests have fetch level flooring meta data', function () { // init the fake server with response stuff @@ -1021,14 +1017,14 @@ describe('the price floors module', function () { ...basicFloorData, modelVersion: 'fetch model name', // change the model name }; - fakeFloorProvider.respondWith(JSON.stringify(fetchFloorData)); + server.respondWith(JSON.stringify(fetchFloorData)); // run setConfig indicating fetch - handleSetFloorsConfig({...basicFloorConfig, floorProvider: 'floorprovider', auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + handleSetFloorsConfig({...basicFloorConfig, floorProvider: 'floorprovider', auctionDelay: 250, endpoint: {url: 'http://www.fakefloorprovider.json/'}}); // floor provider should be called - expect(fakeFloorProvider.requests.length).to.equal(1); - expect(fakeFloorProvider.requests[0].url).to.equal('http://www.fakeFloorProvider.json'); + expect(server.requests.length).to.equal(1); + expect(server.requests[0].url).to.equal('http://www.fakefloorprovider.json/'); // start the auction it should delay and not immediately call `continueAuction` runStandardAuction(); @@ -1037,7 +1033,7 @@ describe('the price floors module', function () { expect(exposedAdUnits).to.be.undefined; // make the fetch respond - fakeFloorProvider.respond(); + server.respond(); expect(exposedAdUnits).to.not.be.undefined; // the exposedAdUnits should be from the fetch not setConfig level data @@ -1061,14 +1057,14 @@ describe('the price floors module', function () { floorProvider: 'floorProviderD', // change the floor provider modelVersion: 'fetch model name', // change the model name }; - fakeFloorProvider.respondWith(JSON.stringify(fetchFloorData)); + server.respondWith(JSON.stringify(fetchFloorData)); // run setConfig indicating fetch - handleSetFloorsConfig({...basicFloorConfig, floorProvider: 'floorproviderC', auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + handleSetFloorsConfig({...basicFloorConfig, floorProvider: 'floorproviderC', auctionDelay: 250, endpoint: {url: 'http://www.fakefloorprovider.json/'}}); // floor provider should be called - expect(fakeFloorProvider.requests.length).to.equal(1); - expect(fakeFloorProvider.requests[0].url).to.equal('http://www.fakeFloorProvider.json'); + expect(server.requests.length).to.equal(1); + expect(server.requests[0].url).to.equal('http://www.fakefloorprovider.json/'); // start the auction it should delay and not immediately call `continueAuction` runStandardAuction(); @@ -1077,7 +1073,7 @@ describe('the price floors module', function () { expect(exposedAdUnits).to.be.undefined; // make the fetch respond - fakeFloorProvider.respond(); + server.respond(); // the exposedAdUnits should be from the fetch not setConfig level data // and fetchStatus is success since fetch worked @@ -1102,14 +1098,14 @@ describe('the price floors module', function () { modelVersion: 'fetch model name', // change the model name }; fetchFloorData.skipRate = 95; - fakeFloorProvider.respondWith(JSON.stringify(fetchFloorData)); + server.respondWith(JSON.stringify(fetchFloorData)); // run setConfig indicating fetch - handleSetFloorsConfig({...basicFloorConfig, floorProvider: 'floorprovider', auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + handleSetFloorsConfig({...basicFloorConfig, floorProvider: 'floorprovider', auctionDelay: 250, endpoint: {url: 'http://www.fakefloorprovider.json/'}}); // floor provider should be called - expect(fakeFloorProvider.requests.length).to.equal(1); - expect(fakeFloorProvider.requests[0].url).to.equal('http://www.fakeFloorProvider.json'); + expect(server.requests.length).to.equal(1); + expect(server.requests[0].url).to.equal('http://www.fakefloorprovider.json/'); // start the auction it should delay and not immediately call `continueAuction` runStandardAuction(); @@ -1118,7 +1114,7 @@ describe('the price floors module', function () { expect(exposedAdUnits).to.be.undefined; // make the fetch respond - fakeFloorProvider.respond(); + server.respond(); expect(exposedAdUnits).to.not.be.undefined; // the exposedAdUnits should be from the fetch not setConfig level data @@ -1137,10 +1133,10 @@ describe('the price floors module', function () { }); it('Should not break if floor provider returns 404', function () { // run setConfig indicating fetch - handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakefloorprovider.json/'}}); // run the auction and make server respond with 404 - fakeFloorProvider.respond(); + server.respond(); runStandardAuction(); // error should have been called for fetch error @@ -1160,13 +1156,13 @@ describe('the price floors module', function () { }); }); it('Should not break if floor provider returns non json', function () { - fakeFloorProvider.respondWith('Not valid response'); + server.respondWith('Not valid response'); // run setConfig indicating fetch - handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakefloorprovider.json/'}}); // run the auction and make server respond - fakeFloorProvider.respond(); + server.respond(); runStandardAuction(); // error should have been called for response floor data not being valid @@ -1187,27 +1183,27 @@ describe('the price floors module', function () { }); it('should handle not using fetch correctly', function () { // run setConfig twice indicating fetch - fakeFloorProvider.respondWith(JSON.stringify(basicFloorData)); - handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); - handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + server.respondWith(JSON.stringify(basicFloorData)); + handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakefloorprovider.json/'}}); + handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakefloorprovider.json/'}}); // log warn should be called and server only should have one request expect(logWarnSpy.calledOnce).to.equal(true); - expect(fakeFloorProvider.requests.length).to.equal(1); - expect(fakeFloorProvider.requests[0].url).to.equal('http://www.fakeFloorProvider.json'); + expect(server.requests.length).to.equal(1); + expect(server.requests[0].url).to.equal('http://www.fakefloorprovider.json/'); // now we respond and then run again it should work and make another request - fakeFloorProvider.respond(); - handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); - fakeFloorProvider.respond(); + server.respond(); + handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakefloorprovider.json/'}}); + server.respond(); // now warn still only called once and server called twice expect(logWarnSpy.calledOnce).to.equal(true); - expect(fakeFloorProvider.requests.length).to.equal(2); + expect(server.requests.length).to.equal(2); // should log error if method is not GET for now expect(logErrorSpy.calledOnce).to.equal(false); - handleSetFloorsConfig({...basicFloorConfig, endpoint: {url: 'http://www.fakeFloorProvider.json', method: 'POST'}}); + handleSetFloorsConfig({...basicFloorConfig, endpoint: {url: 'http://www.fakefloorprovider.json/', method: 'POST'}}); expect(logErrorSpy.calledOnce).to.equal(true); }); describe('isFloorsDataValid', function () { diff --git a/test/spec/modules/publinkIdSystem_spec.js b/test/spec/modules/publinkIdSystem_spec.js index 7d98b724bd8..f35a7453403 100644 --- a/test/spec/modules/publinkIdSystem_spec.js +++ b/test/spec/modules/publinkIdSystem_spec.js @@ -120,7 +120,7 @@ describe('PublinkIdSystem', () => { expect(parsed.search.mpn).to.equal('Prebid.js'); expect(parsed.search.mpv).to.equal('$prebid.version$'); - request.respond(204, {}, JSON.stringify(serverResponse)); + request.respond(204); expect(callbackSpy.called).to.be.false; }); diff --git a/test/spec/modules/pubmaticAnalyticsAdapter_spec.js b/test/spec/modules/pubmaticAnalyticsAdapter_spec.js index c56ed565c43..a676f3e3497 100755 --- a/test/spec/modules/pubmaticAnalyticsAdapter_spec.js +++ b/test/spec/modules/pubmaticAnalyticsAdapter_spec.js @@ -1,11 +1,10 @@ -import pubmaticAnalyticsAdapter, { getMetadata } from 'modules/pubmaticAnalyticsAdapter.js'; +import pubmaticAnalyticsAdapter, {getMetadata} from 'modules/pubmaticAnalyticsAdapter.js'; import adapterManager from 'src/adapterManager.js'; import CONSTANTS from 'src/constants.json'; -import { config } from 'src/config.js'; -import { - setConfig, - addBidResponseHook, -} from 'modules/currency.js'; +import {config} from 'src/config.js'; +import {setConfig} from 'modules/currency.js'; +import {server} from '../../mocks/xhr.js'; +import 'src/prebid.js'; let events = require('src/events'); let ajax = require('src/ajax'); @@ -273,7 +272,6 @@ function getLoggerJsonFromRequest(requestBody) { describe('pubmatic analytics adapter', function () { let sandbox; - let xhr; let requests; let oldScreen; let clock; @@ -282,9 +280,7 @@ describe('pubmatic analytics adapter', function () { setUADefault(); sandbox = sinon.sandbox.create(); - xhr = sandbox.useFakeXMLHttpRequest(); - requests = []; - xhr.onCreate = request => requests.push(request); + requests = server.requests; sandbox.stub(events, 'getEvents').returns([]); diff --git a/test/spec/modules/pubwiseAnalyticsAdapter_spec.js b/test/spec/modules/pubwiseAnalyticsAdapter_spec.js index e14582edc39..92d5972cc13 100644 --- a/test/spec/modules/pubwiseAnalyticsAdapter_spec.js +++ b/test/spec/modules/pubwiseAnalyticsAdapter_spec.js @@ -1,6 +1,7 @@ import {expect} from 'chai'; import pubwiseAnalytics from 'modules/pubwiseAnalyticsAdapter.js'; import {expectEvents} from '../../helpers/analytics.js'; +import {server} from '../../mocks/xhr.js'; let events = require('src/events'); let adapterManager = require('src/adapterManager').default; @@ -9,7 +10,6 @@ let constants = require('src/constants.json'); describe('PubWise Prebid Analytics', function () { let requests; let sandbox; - let xhr; let clock; let mock = {}; @@ -38,9 +38,7 @@ describe('PubWise Prebid Analytics', function () { clock = sandbox.useFakeTimers(); sandbox.stub(events, 'getEvents').returns([]); - xhr = sandbox.useFakeXMLHttpRequest(); - requests = []; - xhr.onCreate = request => requests.push(request); + requests = server.requests; }); afterEach(function () { @@ -50,10 +48,6 @@ describe('PubWise Prebid Analytics', function () { }); describe('enableAnalytics', function () { - beforeEach(function () { - requests = []; - }); - it('should catch all events', function () { pubwiseAnalytics.enableAnalytics(mock.DEFAULT_PW_CONFIG); diff --git a/test/spec/modules/teadsIdSystem_spec.js b/test/spec/modules/teadsIdSystem_spec.js index 7b977e2fb2b..1959b990957 100644 --- a/test/spec/modules/teadsIdSystem_spec.js +++ b/test/spec/modules/teadsIdSystem_spec.js @@ -218,7 +218,7 @@ describe('TeadsIdSystem', function () { callback(callbackSpy); const request = server.requests[0]; expect(request.url).to.include(teadsUrl); - request.respond(204, null, 'Unavailable'); + request.respond(204); expect(logInfoStub.calledOnce).to.be.true; }); diff --git a/test/spec/modules/uid2IdSystem_helpers.js b/test/spec/modules/uid2IdSystem_helpers.js index 65d52c1d7c3..5006a50dedd 100644 --- a/test/spec/modules/uid2IdSystem_helpers.js +++ b/test/spec/modules/uid2IdSystem_helpers.js @@ -1,6 +1,6 @@ -import { setConsentConfig } from 'modules/consentManagement.js'; -import { server } from 'test/mocks/xhr.js'; -import {coreStorage, init, setSubmoduleRegistry, requestBidsHook} from 'modules/userId/index.js'; +import {setConsentConfig} from 'modules/consentManagement.js'; +import {server} from 'test/mocks/xhr.js'; +import {coreStorage, requestBidsHook} from 'modules/userId/index.js'; const msIn12Hours = 60 * 60 * 12 * 1000; const expireCookieDate = 'Thu, 01 Jan 1970 00:00:01 GMT'; @@ -34,8 +34,8 @@ export const apiHelpers = { refresh_expires: Date.now() + 24 * 60 * 60 * 1000, // 24 hours refresh_response_key: 'wR5t6HKMfJ2r4J7fEGX9Gw==', // Fake data }), - respondAfterDelay: (delay) => new Promise((resolve) => setTimeout(() => { - server.respond(); + respondAfterDelay: (delay, srv = server) => new Promise((resolve) => setTimeout(() => { + srv.respond(); setTimeout(() => resolve()); }, delay)), } diff --git a/test/spec/modules/uid2IdSystem_spec.js b/test/spec/modules/uid2IdSystem_spec.js index 20a38a292bb..f33060869df 100644 --- a/test/spec/modules/uid2IdSystem_spec.js +++ b/test/spec/modules/uid2IdSystem_spec.js @@ -7,7 +7,6 @@ import { uid2IdSubmodule } from 'modules/uid2IdSystem.js'; import 'src/prebid.js'; import 'modules/consentManagement.js'; import { getGlobal } from 'src/prebidGlobal.js'; -import { server } from 'test/mocks/xhr.js'; import { configureTimerInterceptors } from 'test/mocks/timers.js'; import { cookieHelpers, runAuction, apiHelpers, setGdprApplies } from './uid2IdSystem_helpers.js'; import {hook} from 'src/hook.js'; @@ -56,22 +55,6 @@ const expectModuleStorageToContain = (initialIdentity, latestIdentity) => { const apiUrl = 'https://prod.uidapi.com/v2/token/refresh'; const headers = { 'Content-Type': 'application/json' }; const makeSuccessResponseBody = () => btoa(JSON.stringify({ status: 'success', body: { ...apiHelpers.makeTokenResponse(initialToken), advertising_token: refreshedToken } })); -const configureUid2Response = (httpStatus, response) => server.respondWith('POST', apiUrl, (xhr) => xhr.respond(httpStatus, headers, response)); -const configureUid2ApiSuccessResponse = () => configureUid2Response(200, makeSuccessResponseBody()); -const configureUid2ApiFailResponse = () => configureUid2Response(500, 'Error'); - -// Runs the provided test twice - once with a successful API mock, once with one which returns a server error -const testApiSuccessAndFailure = (act, testDescription, failTestDescription, only = false) => { - const testFn = only ? it.only : it; - testFn(`API responds successfully: ${testDescription}`, async function() { - configureUid2ApiSuccessResponse(); - await act(true); - }); - testFn(`API responds with an error: ${failTestDescription ?? testDescription}`, async function() { - configureUid2ApiFailResponse(); - await act(false); - }); -} const testCookieAndLocalStorage = (description, test, only = false) => { const describeFn = only ? describe.only : describe; @@ -93,7 +76,7 @@ const testCookieAndLocalStorage = (description, test, only = false) => { }; describe(`UID2 module`, function () { - let suiteSandbox, testSandbox, timerSpy, fullTestTitle, restoreSubtleToUndefined = false; + let server, suiteSandbox, testSandbox, timerSpy, fullTestTitle, restoreSubtleToUndefined = false; before(function () { timerSpy = configureTimerInterceptors(debugOutput); hook.ready(); @@ -116,13 +99,31 @@ describe(`UID2 module`, function () { if (restoreSubtleToUndefined) window.crypto.subtle = undefined; }); + const configureUid2Response = (httpStatus, response) => server.respondWith('POST', apiUrl, (xhr) => xhr.respond(httpStatus, headers, response)); + const configureUid2ApiSuccessResponse = () => configureUid2Response(200, makeSuccessResponseBody()); + const configureUid2ApiFailResponse = () => configureUid2Response(500, 'Error'); + // Runs the provided test twice - once with a successful API mock, once with one which returns a server error + const testApiSuccessAndFailure = (act, testDescription, failTestDescription, only = false) => { + const testFn = only ? it.only : it; + testFn(`API responds successfully: ${testDescription}`, async function() { + configureUid2ApiSuccessResponse(); + await act(true); + }); + testFn(`API responds with an error: ${failTestDescription ?? testDescription}`, async function() { + configureUid2ApiFailResponse(); + await act(false); + }); + } + const getFullTestTitle = (test) => `${test.parent.title ? getFullTestTitle(test.parent) + ' | ' : ''}${test.title}`; + beforeEach(function () { debugOutput(`----------------- START TEST ------------------`); fullTestTitle = getFullTestTitle(this.test.ctx.currentTest); debugOutput(fullTestTitle); testSandbox = sinon.sandbox.create(); testSandbox.stub(utils, 'logWarn'); + server = sinon.createFakeServer(); init(config); setSubmoduleRegistry([uid2IdSubmodule]); @@ -143,7 +144,6 @@ describe(`UID2 module`, function () { } cookieHelpers.clearCookies(moduleCookieName, publisherCookieName); coreStorage.removeDataFromLocalStorage(moduleCookieName); - debugOutput('----------------- END TEST ------------------'); }); @@ -247,7 +247,7 @@ describe(`UID2 module`, function () { describe('When the refresh is available in time', function() { testApiSuccessAndFailure(async function(apiSucceeds) { scenario.setConfig(apiHelpers.makeTokenResponse(initialToken, true, true)); - apiHelpers.respondAfterDelay(auctionDelayMs / 10); + apiHelpers.respondAfterDelay(auctionDelayMs / 10, server); const bid = await runAuction(); if (apiSucceeds) expectToken(bid, refreshedToken); @@ -256,7 +256,7 @@ describe(`UID2 module`, function () { testApiSuccessAndFailure(async function(apiSucceeds) { scenario.setConfig(apiHelpers.makeTokenResponse(initialToken, true, true)); - apiHelpers.respondAfterDelay(auctionDelayMs / 10); + apiHelpers.respondAfterDelay(auctionDelayMs / 10, server); await runAuction(); if (apiSucceeds) { @@ -275,7 +275,7 @@ describe(`UID2 module`, function () { testApiSuccessAndFailure(async function(apiSucceeds) { scenario.setConfig(apiHelpers.makeTokenResponse(initialToken, true, true)); - const promise = apiHelpers.respondAfterDelay(auctionDelayMs * 2); + const promise = apiHelpers.respondAfterDelay(auctionDelayMs * 2, server); const bid = await runAuction(); expectNoIdentity(bid); @@ -319,13 +319,13 @@ describe(`UID2 module`, function () { scenario.setConfig(apiHelpers.makeTokenResponse(initialToken, true), {auctionDelay: 0, syncDelay: 1}); }); testApiSuccessAndFailure(async function() { - apiHelpers.respondAfterDelay(10); + apiHelpers.respondAfterDelay(10, server); const bid = await runAuction(); expectToken(bid, initialToken); }, 'it should not be refreshed before the auction runs'); testApiSuccessAndFailure(async function(success) { - const promise = apiHelpers.respondAfterDelay(1); + const promise = apiHelpers.respondAfterDelay(1, server); await runAuction(); await promise; if (success) { diff --git a/test/spec/modules/userId_spec.js b/test/spec/modules/userId_spec.js index acc016a903d..68e42310c33 100644 --- a/test/spec/modules/userId_spec.js +++ b/test/spec/modules/userId_spec.js @@ -3005,7 +3005,7 @@ describe('User ID', function () { expect(server.requests).to.be.empty; return endAuction(); }).then(() => { - expect(server.requests[0].url).to.equal('/any/unifiedid/url'); + expect(server.requests[0].url).to.match(/\/any\/unifiedid\/url/); }); }); diff --git a/test/spec/modules/zeta_global_sspAnalyticsAdapter_spec.js b/test/spec/modules/zeta_global_sspAnalyticsAdapter_spec.js index 15a1155f378..cbba815cfc1 100644 --- a/test/spec/modules/zeta_global_sspAnalyticsAdapter_spec.js +++ b/test/spec/modules/zeta_global_sspAnalyticsAdapter_spec.js @@ -1,7 +1,7 @@ import zetaAnalyticsAdapter from 'modules/zeta_global_sspAnalyticsAdapter.js'; import {config} from 'src/config'; import CONSTANTS from 'src/constants.json'; -import {logError} from '../../../src/utils'; +import {server} from '../../mocks/xhr.js'; let utils = require('src/utils'); let events = require('src/events'); @@ -358,14 +358,11 @@ const MOCK = { describe('Zeta Global SSP Analytics Adapter', function() { let sandbox; - let xhr; let requests; beforeEach(function() { sandbox = sinon.sandbox.create(); - requests = []; - xhr = sandbox.useFakeXMLHttpRequest(); - xhr.onCreate = request => requests.push(request); + requests = server.requests; sandbox.stub(events, 'getEvents').returns([]); }); diff --git a/test/spec/unit/core/ajax_spec.js b/test/spec/unit/core/ajax_spec.js new file mode 100644 index 00000000000..ddcd3305900 --- /dev/null +++ b/test/spec/unit/core/ajax_spec.js @@ -0,0 +1,376 @@ +import {attachCallbacks, fetcherFactory, toFetchRequest} from '../../../../src/ajax.js'; +import {config} from 'src/config.js'; +import {server} from '../../../mocks/xhr.js'; + +const EXAMPLE_URL = 'https://www.example.com'; + +describe('fetcherFactory', () => { + let clock; + + beforeEach(() => { + clock = sinon.useFakeTimers(); + server.autoTimeout = true; + }); + + afterEach(() => { + clock.runAll(); + clock.restore(); + config.resetConfig(); + }); + + Object.entries({ + 'URL': EXAMPLE_URL, + 'request object': new Request(EXAMPLE_URL) + }).forEach(([t, resource]) => { + it(`times out after timeout when fetching ${t}`, (done) => { + const fetch = fetcherFactory(1000); + const resp = fetch(resource); + clock.tick(900); + expect(server.requests[0].fetch.request.signal.aborted).to.be.false; + clock.tick(100); + expect(server.requests[0].fetch.request.signal.aborted).to.be.true; + resp.catch(() => done()); + }); + }); + + it('does not timeout after it completes', () => { + const fetch = fetcherFactory(1000); + const resp = fetch(EXAMPLE_URL); + server.requests[0].respond(); + return resp.then(() => { + clock.tick(2000); + expect(server.requests[0].fetch.request.signal.aborted).to.be.false; + }); + }); + + Object.entries({ + 'disableAjaxTimeout is set'() { + const fetcher = fetcherFactory(1000); + config.setConfig({disableAjaxTimeout: true}); + return fetcher; + }, + 'timeout is null'() { + return fetcherFactory(null); + }, + }).forEach(([t, mkFetcher]) => { + it(`does not timeout if ${t}`, (done) => { + const fetch = mkFetcher(); + const pm = fetch(EXAMPLE_URL); + clock.tick(2000); + server.requests[0].respond(); + pm.then(() => done()); + }); + }); + + Object.entries({ + 'local URL': ['/local.html', window.origin], + 'remote URL': [EXAMPLE_URL + '/remote.html', EXAMPLE_URL], + 'request with local URL': [new Request('/local.html'), window.origin], + 'request with remote URL': [new Request(EXAMPLE_URL + '/remote.html'), EXAMPLE_URL] + }).forEach(([t, [resource, expectedOrigin]]) => { + describe(`using ${t}`, () => { + it('calls request, passing origin', () => { + const request = sinon.stub(); + const fetch = fetcherFactory(1000, {request}); + fetch(resource); + sinon.assert.calledWith(request, expectedOrigin); + }); + + Object.entries({ + success: 'respond', + error: 'error' + }).forEach(([t, method]) => { + it(`calls done on ${t}, passing origin`, () => { + const done = sinon.stub(); + const fetch = fetcherFactory(1000, {done}); + const req = fetch(resource).catch(() => null).then(() => { + sinon.assert.calledWith(done, expectedOrigin); + }); + server.requests[0][method](); + return req; + }); + }); + }); + }); +}); + +describe('toFetchRequest', () => { + Object.entries({ + 'simple POST': { + url: EXAMPLE_URL, + data: 'data', + expect: { + request: { + url: EXAMPLE_URL + '/', + method: 'POST', + }, + text: 'data', + headers: { + 'content-type': 'text/plain' + } + } + }, + 'POST with headers': { + url: EXAMPLE_URL, + data: '{"json": "body"}', + options: { + contentType: 'application/json', + customHeaders: { + 'x-custom': 'value' + } + }, + expect: { + request: { + url: EXAMPLE_URL + '/', + method: 'POST', + }, + text: '{"json": "body"}', + headers: { + 'content-type': 'application/json', + 'X-Custom': 'value' + } + } + }, + 'simple GET': { + url: EXAMPLE_URL, + data: {p1: 'v1', p2: 'v2'}, + options: { + method: 'GET', + }, + expect: { + request: { + url: EXAMPLE_URL + '/?p1=v1&p2=v2', + method: 'GET' + }, + text: '', + headers: { + 'content-type': 'text/plain' + } + } + }, + 'GET with credentials': { + url: EXAMPLE_URL, + data: null, + options: { + method: 'GET', + withCredentials: true, + }, + expect: { + request: { + url: EXAMPLE_URL + '/', + method: 'GET', + credentials: 'include' + }, + text: '', + headers: { + 'content-type': 'text/plain' + } + } + } + }).forEach(([t, {url, data, options, expect: {request, text, headers}}]) => { + it(`can build ${t}`, () => { + const req = toFetchRequest(url, data, options); + return req.text().then(body => { + Object.entries(request).forEach(([prop, val]) => { + expect(req[prop]).to.eql(val); + }); + const hdr = new Headers(headers); + Array.from(req.headers.entries()).forEach(([name, val]) => { + expect(hdr.get(name)).to.eql(val); + }); + expect(body).to.eql(text); + }); + }); + }); +}); + +describe('attachCallbacks', () => { + const sampleHeaders = new Headers({ + 'x-1': 'v1', + 'x-2': 'v2' + }); + + function responseFactory(body, props) { + props = Object.assign({headers: sampleHeaders, url: EXAMPLE_URL}, props); + return function () { + return { + response: Object.defineProperties(new Response(body, props), { + url: { + get: () => props.url + } + }), + body: body || '' + }; + }; + } + + function expectNullXHR(response) { + return new Promise((resolve, reject) => { + attachCallbacks(Promise.resolve(response), { + success: () => { + reject(new Error('should not succeed')); + }, + error(statusText, xhr) { + expect(statusText).to.eql(''); + sinon.assert.match(xhr, { + readyState: XMLHttpRequest.DONE, + status: 0, + statusText: '', + responseText: '', + response: '', + responseXML: null + }); + expect(xhr.getResponseHeader('any')).to.be.null; + resolve(); + } + }); + }); + } + + it('runs error callback on rejections', () => { + return expectNullXHR(Promise.reject(new Error())); + }); + + Object.entries({ + '2xx response': { + success: true, + makeResponse: responseFactory('body', {status: 200, statusText: 'OK'}) + }, + '2xx response with no body': { + success: true, + makeResponse: responseFactory(null, {status: 204, statusText: 'No content'}) + }, + '2xx response with XML': { + success: true, + xml: true, + makeResponse: responseFactory('', { + status: 200, + statusText: 'OK', + headers: {'content-type': 'application/xml;charset=UTF8'} + }) + }, + '2xx response with HTML': { + success: true, + xml: true, + makeResponse: responseFactory('

', { + status: 200, + statusText: 'OK', + headers: {'content-type': 'text/html;charset=UTF-8'} + }) + }, + '304 response': { + success: true, + makeResponse: responseFactory(null, {status: 304, statusText: 'Moved permanently'}) + }, + '4xx response': { + success: false, + makeResponse: responseFactory('body', {status: 400, statusText: 'Invalid request'}) + }, + '5xx response': { + success: false, + makeResponse: responseFactory('body', {status: 503, statusText: 'Gateway error'}) + }, + '4xx response with XML': { + success: false, + xml: true, + makeResponse: responseFactory('', { + status: 404, + statusText: 'Not found', + headers: { + 'content-type': 'application/xml' + } + }) + } + }).forEach(([t, {success, makeResponse, xml}]) => { + const cbType = success ? 'success' : 'error'; + + describe(`for ${t}`, () => { + let response, body; + beforeEach(() => { + ({response, body} = makeResponse()); + }); + + function checkXHR(xhr) { + sinon.assert.match(xhr, { + readyState: XMLHttpRequest.DONE, + status: response.status, + statusText: response.statusText, + responseType: '', + responseURL: response.url, + response: body, + responseText: body, + }); + if (xml) { + expect(xhr.responseXML.querySelectorAll('*').length > 0).to.be.true; + } else { + expect(xhr.responseXML).to.not.exist; + } + Array.from(response.headers.entries()).forEach(([name, value]) => { + expect(xhr.getResponseHeader(name)).to.eql(value); + }); + expect(xhr.getResponseHeader('$$missing-header')).to.be.null; + } + + it(`runs ${cbType} callback`, (done) => { + attachCallbacks(Promise.resolve(response), { + success(payload, xhr) { + expect(success).to.be.true; + expect(payload).to.eql(body); + checkXHR(xhr); + done(); + }, + error(statusText, xhr) { + expect(success).to.be.false; + expect(statusText).to.eql(response.statusText); + checkXHR(xhr); + done(); + } + }); + }); + + it(`runs error callback if body cannot be retrieved`, () => { + response.text = () => Promise.reject(new Error()); + return expectNullXHR(response); + }); + + if (success) { + it('accepts a single function as success callback', (done) => { + attachCallbacks(Promise.resolve(response), function (payload, xhr) { + expect(payload).to.eql(body); + checkXHR(xhr); + done(); + }) + }) + } + }); + }); + + describe('callback exceptions', () => { + Object.entries({ + success: responseFactory(null, {status: 204}), + error: responseFactory('', {status: 400}), + }).forEach(([cbType, makeResponse]) => { + it(`do not choke ${cbType} callbacks`, () => { + const {response} = makeResponse(); + return new Promise((resolve) => { + const result = {success: false, error: false}; + attachCallbacks(Promise.resolve(response), { + success() { + result.success = true; + throw new Error(); + }, + error() { + result.error = true; + throw new Error(); + } + }); + setTimeout(() => resolve(result), 20); + }).then(result => { + Object.entries(result).forEach(([typ, ran]) => { + expect(ran).to.be[typ === cbType ? 'true' : 'false'] + }) + }); + }); + }); + }); +}); diff --git a/test/spec/videoCache_spec.js b/test/spec/videoCache_spec.js index c7c0b2eb329..c746fdd2afd 100644 --- a/test/spec/videoCache_spec.js +++ b/test/spec/videoCache_spec.js @@ -174,7 +174,7 @@ describe('The video cache', function () { const request = server.requests[0]; request.method.should.equal('POST'); request.url.should.equal('https://prebid.adnxs.com/pbc/v1/cache'); - request.requestHeaders['Content-Type'].should.equal('text/plain;charset=utf-8'); + request.requestHeaders['Content-Type'].should.equal('text/plain'); let payload = { puts: [{ type: 'xml', @@ -224,7 +224,7 @@ describe('The video cache', function () { const request = server.requests[0]; request.method.should.equal('POST'); request.url.should.equal('https://prebid.adnxs.com/pbc/v1/cache'); - request.requestHeaders['Content-Type'].should.equal('text/plain;charset=utf-8'); + request.requestHeaders['Content-Type'].should.equal('text/plain'); let payload = { puts: [{ type: 'xml', @@ -295,7 +295,7 @@ describe('The video cache', function () { const request = server.requests[0]; request.method.should.equal('POST'); request.url.should.equal('https://prebid.adnxs.com/pbc/v1/cache'); - request.requestHeaders['Content-Type'].should.equal('text/plain;charset=utf-8'); + request.requestHeaders['Content-Type'].should.equal('text/plain'); let payload = { puts: [{ type: 'xml', @@ -356,7 +356,7 @@ describe('The video cache', function () { const request = server.requests[0]; request.method.should.equal('POST'); request.url.should.equal('https://prebid.adnxs.com/pbc/v1/cache'); - request.requestHeaders['Content-Type'].should.equal('text/plain;charset=utf-8'); + request.requestHeaders['Content-Type'].should.equal('text/plain'); JSON.parse(request.requestBody).should.deep.equal({ puts: [{ From 5a8e4f2d8214b33a2ab63ad7dd5017c948fe81e9 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Wed, 9 Aug 2023 09:45:25 -0700 Subject: [PATCH 2/4] Enable browsing topics header for client bidders --- src/adapters/bidderFactory.js | 19 +++-- src/ajax.js | 11 +-- test/spec/unit/core/ajax_spec.js | 15 +++- test/spec/unit/core/bidderFactory_spec.js | 88 ++++++++++++++++++++--- 4 files changed, 115 insertions(+), 18 deletions(-) diff --git a/src/adapters/bidderFactory.js b/src/adapters/bidderFactory.js index d316ad5924c..b31019a6d79 100644 --- a/src/adapters/bidderFactory.js +++ b/src/adapters/bidderFactory.js @@ -25,7 +25,7 @@ import {useMetrics} from '../utils/perfMetrics.js'; import {isActivityAllowed} from '../activities/rules.js'; import {activityParams} from '../activities/activityParams.js'; import {MODULE_TYPE_BIDDER} from '../activities/modules.js'; -import {ACTIVITY_TRANSMIT_TID} from '../activities/activities.js'; +import {ACTIVITY_TRANSMIT_TID, ACTIVITY_TRANSMIT_UFPD} from '../activities/activities.js'; /** * This file aims to support Adapters during the Prebid 0.x -> 1.x transition. @@ -454,6 +454,15 @@ export const processBidderRequests = hook('sync', function (spec, bids, bidderRe onRequest(request); const networkDone = requestMetrics.startTiming('net'); + + function getOptions(defaults) { + const ro = request.options; + return Object.assign(defaults, ro, { + browsingTopics: ro?.hasOwnProperty('browsingTopics') && !ro.browsingTopics + ? false + : isActivityAllowed(ACTIVITY_TRANSMIT_UFPD, activityParams(MODULE_TYPE_BIDDER, spec.code)) + }) + } switch (request.method) { case 'GET': ajax( @@ -463,10 +472,10 @@ export const processBidderRequests = hook('sync', function (spec, bids, bidderRe error: onFailure }, undefined, - Object.assign({ + getOptions({ method: 'GET', withCredentials: true - }, request.options) + }) ); break; case 'POST': @@ -477,11 +486,11 @@ export const processBidderRequests = hook('sync', function (spec, bids, bidderRe error: onFailure }, typeof request.data === 'string' ? request.data : JSON.stringify(request.data), - Object.assign({ + getOptions({ method: 'POST', contentType: 'text/plain', withCredentials: true - }, request.options) + }) ); break; default: diff --git a/src/ajax.js b/src/ajax.js index cb2adda16a6..9083f2eda6d 100644 --- a/src/ajax.js +++ b/src/ajax.js @@ -37,17 +37,20 @@ export function toFetchRequest(url, data, options = {}) { } const headers = new Headers(options.customHeaders); headers.set(CTYPE, options.contentType || 'text/plain'); - const opts = { + const rqOpts = { method, headers } if (method !== GET && data) { - opts.body = data; + rqOpts.body = data; } if (options.withCredentials) { - opts.credentials = 'include'; + rqOpts.credentials = 'include'; } - return dep.makeRequest(url, opts); + if (options.browsingTopics) { + rqOpts.browsingTopics = true; + } + return dep.makeRequest(url, rqOpts); } /** diff --git a/test/spec/unit/core/ajax_spec.js b/test/spec/unit/core/ajax_spec.js index ddcd3305900..fbb111d6e69 100644 --- a/test/spec/unit/core/ajax_spec.js +++ b/test/spec/unit/core/ajax_spec.js @@ -1,4 +1,4 @@ -import {attachCallbacks, fetcherFactory, toFetchRequest} from '../../../../src/ajax.js'; +import {dep, attachCallbacks, fetcherFactory, toFetchRequest} from '../../../../src/ajax.js'; import {config} from 'src/config.js'; import {server} from '../../../mocks/xhr.js'; @@ -182,6 +182,19 @@ describe('toFetchRequest', () => { }); }); }); + + describe('browsingTopics', () => { + Object.entries({ + 'browsingTopics = true': [{browsingTopics: true}, true], + 'browsingTopics = false': [{browsingTopics: false}, false], + 'browsingTopics is undef': [{}, false] + }).forEach(([t, [opts, shouldBeSet]]) => { + it(`should ${!shouldBeSet ? 'not ' : ''}be set on request when options has ${t}`, () => { + toFetchRequest(EXAMPLE_URL, null, opts); + sinon.assert.calledWithMatch(dep.makeRequest, sinon.match.any, {browsingTopics: shouldBeSet ? true : undefined}); + }) + }) + }) }); describe('attachCallbacks', () => { diff --git a/test/spec/unit/core/bidderFactory_spec.js b/test/spec/unit/core/bidderFactory_spec.js index 138ffcb608d..0360679ca52 100644 --- a/test/spec/unit/core/bidderFactory_spec.js +++ b/test/spec/unit/core/bidderFactory_spec.js @@ -14,7 +14,7 @@ import {bidderSettings} from '../../../../src/bidderSettings.js'; import {decorateAdUnitsWithNativeParams} from '../../../../src/native.js'; import * as activityRules from 'src/activities/rules.js'; import {MODULE_TYPE_BIDDER} from '../../../../src/activities/modules.js'; -import {ACTIVITY_TRANSMIT_TID} from '../../../../src/activities/activities.js'; +import {ACTIVITY_TRANSMIT_TID, ACTIVITY_TRANSMIT_UFPD} from '../../../../src/activities/activities.js'; const CODE = 'sampleBidder'; const MOCK_BIDS_REQUEST = { @@ -319,7 +319,7 @@ describe('bidders created by newBidder', function () { expect(ajaxStub.calledOnce).to.equal(true); expect(ajaxStub.firstCall.args[0]).to.equal(url); expect(ajaxStub.firstCall.args[2]).to.equal(JSON.stringify(data)); - expect(ajaxStub.firstCall.args[3]).to.deep.equal({ + sinon.assert.match(ajaxStub.firstCall.args[3], { method: 'POST', contentType: 'text/plain', withCredentials: true @@ -344,11 +344,11 @@ describe('bidders created by newBidder', function () { expect(ajaxStub.calledOnce).to.equal(true); expect(ajaxStub.firstCall.args[0]).to.equal(url); expect(ajaxStub.firstCall.args[2]).to.equal(JSON.stringify(data)); - expect(ajaxStub.firstCall.args[3]).to.deep.equal({ + sinon.assert.match(ajaxStub.firstCall.args[3], { method: 'POST', contentType: 'application/json', withCredentials: true - }); + }) }); it('should make the appropriate GET request', function () { @@ -367,10 +367,10 @@ describe('bidders created by newBidder', function () { expect(ajaxStub.calledOnce).to.equal(true); expect(ajaxStub.firstCall.args[0]).to.equal(`${url}?arg=2`); expect(ajaxStub.firstCall.args[2]).to.be.undefined; - expect(ajaxStub.firstCall.args[3]).to.deep.equal({ + sinon.assert.match(ajaxStub.firstCall.args[3], { method: 'GET', withCredentials: true - }); + }) }); it('should make the appropriate GET request when options are passed', function () { @@ -391,10 +391,10 @@ describe('bidders created by newBidder', function () { expect(ajaxStub.calledOnce).to.equal(true); expect(ajaxStub.firstCall.args[0]).to.equal(`${url}?arg=2`); expect(ajaxStub.firstCall.args[2]).to.be.undefined; - expect(ajaxStub.firstCall.args[3]).to.deep.equal({ + sinon.assert.match(ajaxStub.firstCall.args[3], { method: 'GET', withCredentials: false - }); + }) }); it('should make multiple calls if the spec returns them', function () { @@ -420,6 +420,78 @@ describe('bidders created by newBidder', function () { expect(ajaxStub.calledTwice).to.equal(true); }); + describe('browsingTopics ajax option', () => { + let transmitUfpdAllowed, bidder; + beforeEach(() => { + activityRules.isActivityAllowed.reset(); + activityRules.isActivityAllowed.callsFake((activity) => activity === ACTIVITY_TRANSMIT_UFPD ? transmitUfpdAllowed : true); + bidder = newBidder(spec); + spec.isBidRequestValid.returns(true); + }); + + it(`should be set to false when adapter sets browsingTopics = false`, () => { + transmitUfpdAllowed = true; + spec.buildRequests.returns([ + { + method: 'GET', + url: 'url', + options: { + browsingTopics: false + } + } + ]); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + sinon.assert.calledWith(ajaxStub, 'url', sinon.match.any, sinon.match.any, sinon.match({ + browsingTopics: false + })); + }); + + Object.entries({ + 'allowed': true, + 'not allowed': false + }).forEach(([t, allow]) => { + it(`should be set to ${allow} when transmitUfpd is ${t}`, () => { + transmitUfpdAllowed = allow; + spec.buildRequests.returns([ + { + method: 'GET', + url: '1', + }, + { + method: 'POST', + url: '2', + data: {} + }, + { + method: 'GET', + url: '3', + options: { + browsingTopics: true + } + }, + { + method: 'POST', + url: '4', + data: {}, + options: { + browsingTopics: true + } + } + ]); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + ['1', '2', '3', '4'].forEach(url => { + sinon.assert.calledWith( + ajaxStub, + url, + sinon.match.any, + sinon.match.any, + sinon.match({browsingTopics: allow}) + ); + }); + }); + }); + }); + it('should not add bids for each placement code if no requests are given', function () { const bidder = newBidder(spec); From 3dbf1cf94eb0a1eb8e58e91881b5eaf8b100934d Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Wed, 9 Aug 2023 10:28:42 -0700 Subject: [PATCH 3/4] Enable browsingTopics for PBS --- modules/prebidServerBidAdapter/index.js | 10 ++++-- src/ajax.js | 4 ++- .../modules/prebidServerBidAdapter_spec.js | 35 +++++++++++++++++-- test/spec/unit/core/ajax_spec.js | 20 +++++++++-- 4 files changed, 61 insertions(+), 8 deletions(-) diff --git a/modules/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js index 3cc38923d57..3cae4497354 100644 --- a/modules/prebidServerBidAdapter/index.js +++ b/modules/prebidServerBidAdapter/index.js @@ -18,7 +18,7 @@ import { deepAccess, } from '../../src/utils.js'; import CONSTANTS from '../../src/constants.json'; -import adapterManager from '../../src/adapterManager.js'; +import adapterManager, {s2sActivityParams} from '../../src/adapterManager.js'; import {config} from '../../src/config.js'; import {addComponentAuction, isValid} from '../../src/adapters/bidderFactory.js'; import * as events from '../../src/events.js'; @@ -29,6 +29,8 @@ import {hook} from '../../src/hook.js'; import {hasPurpose1Consent} from '../../src/utils/gpdr.js'; import {buildPBSRequest, interpretPBSResponse} from './ortbConverter.js'; import {useMetrics} from '../../src/utils/perfMetrics.js'; +import {isActivityAllowed} from '../../src/activities/rules.js'; +import {ACTIVITY_TRANSMIT_UFPD} from '../../src/activities/activities.js'; const getConfig = config.getConfig; @@ -571,7 +573,11 @@ export const processPBSRequest = hook('sync', function (s2sBidRequest, bidReques } }, requestJson, - {contentType: 'text/plain', withCredentials: true} + { + contentType: 'text/plain', + withCredentials: true, + browsingTopics: isActivityAllowed(ACTIVITY_TRANSMIT_UFPD, s2sActivityParams(s2sBidRequest.s2sConfig)) + } ); } else { logError('PBS request not made. Check endpoints.'); diff --git a/src/ajax.js b/src/ajax.js index 9083f2eda6d..0601cc0e22b 100644 --- a/src/ajax.js +++ b/src/ajax.js @@ -47,7 +47,9 @@ export function toFetchRequest(url, data, options = {}) { if (options.withCredentials) { rqOpts.credentials = 'include'; } - if (options.browsingTopics) { + if (options.browsingTopics && isSecureContext) { + // the Request constructor will throw an exception if the browser supports topics + // but we're not in a secure context rqOpts.browsingTopics = true; } return dep.makeRequest(url, rqOpts); diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index e4f06c8835f..1626d6f2c9d 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -6,7 +6,7 @@ import { resetWurlMap, s2sDefaultConfig } from 'modules/prebidServerBidAdapter/index.js'; -import adapterManager from 'src/adapterManager.js'; +import adapterManager, {PBS_ADAPTER_NAME} from 'src/adapterManager.js'; import * as utils from 'src/utils.js'; import {deepAccess, deepClone, mergeDeep} from 'src/utils.js'; import {ajax} from 'src/ajax.js'; @@ -27,6 +27,7 @@ import 'modules/consentManagementUsp.js'; import 'modules/schain.js'; import 'modules/fledgeForGpt.js'; import * as redactor from 'src/activities/redactor.js'; +import * as activityRules from 'src/activities/rules.js'; import {hook} from '../../../src/hook.js'; import {decorateAdUnitsWithNativeParams} from '../../../src/native.js'; import {auctionManager} from '../../../src/auctionManager.js'; @@ -35,6 +36,10 @@ import {addComponentAuction, registerBidder} from 'src/adapters/bidderFactory.js import {getGlobal} from '../../../src/prebidGlobal.js'; import {syncAddFPDEnrichments, syncAddFPDToBidderRequest} from '../../helpers/fpd.js'; import {deepSetValue} from '../../../src/utils.js'; +import {sandbox} from 'sinon'; +import {ACTIVITY_TRANSMIT_UFPD} from '../../../src/activities/activities.js'; +import {activityParams} from '../../../src/activities/activityParams.js'; +import {MODULE_TYPE_PREBID} from '../../../src/activities/modules.js'; let CONFIG = { accountId: '1', @@ -734,13 +739,39 @@ describe('S2S Adapter', function () { }) }) + describe('browsingTopics', () => { + const sandbox = sinon.createSandbox(); + afterEach(() => { + sandbox.restore() + }); + Object.entries({ + 'allowed': true, + 'not allowed': false, + }).forEach(([t, allow]) => { + it(`should be set to ${allow} when transmitUfpd is ${t}`, () => { + sandbox.stub(activityRules, 'isActivityAllowed').callsFake((activity, params) => { + if (activity === ACTIVITY_TRANSMIT_UFPD && params.component === `${MODULE_TYPE_PREBID}.${PBS_ADAPTER_NAME}`) { + return allow; + } + return false; + }); + config.setConfig({s2sConfig: CONFIG}); + const ajax = sinon.stub(); + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + sinon.assert.calledWith(ajax, sinon.match.any, sinon.match.any, sinon.match.any, sinon.match({ + browsingTopics: allow + })); + }); + }); + }) + it('should set tmax to s2sConfig.timeout', () => { const cfg = {...CONFIG, timeout: 123}; config.setConfig({s2sConfig: cfg}); adapter.callBids({...REQUEST, s2sConfig: cfg}, BID_REQUESTS, addBidResponse, done, ajax); const req = JSON.parse(server.requests[0].requestBody); expect(req.tmax).to.eql(123); - }) + }); it('should block request if config did not define p1Consent URL in endpoint object config', function () { let badConfig = utils.deepClone(CONFIG); diff --git a/test/spec/unit/core/ajax_spec.js b/test/spec/unit/core/ajax_spec.js index fbb111d6e69..df0ce02c15c 100644 --- a/test/spec/unit/core/ajax_spec.js +++ b/test/spec/unit/core/ajax_spec.js @@ -1,6 +1,7 @@ import {dep, attachCallbacks, fetcherFactory, toFetchRequest} from '../../../../src/ajax.js'; import {config} from 'src/config.js'; import {server} from '../../../mocks/xhr.js'; +import {sandbox} from 'sinon'; const EXAMPLE_URL = 'https://www.example.com'; @@ -189,9 +190,22 @@ describe('toFetchRequest', () => { 'browsingTopics = false': [{browsingTopics: false}, false], 'browsingTopics is undef': [{}, false] }).forEach(([t, [opts, shouldBeSet]]) => { - it(`should ${!shouldBeSet ? 'not ' : ''}be set on request when options has ${t}`, () => { - toFetchRequest(EXAMPLE_URL, null, opts); - sinon.assert.calledWithMatch(dep.makeRequest, sinon.match.any, {browsingTopics: shouldBeSet ? true : undefined}); + describe(`when options has ${t}`, () => { + const sandbox = sinon.createSandbox(); + afterEach(() => { + sandbox.restore(); + }); + + it(`should ${!shouldBeSet ? 'not ' : ''}be set when in a secure context`, () => { + sandbox.stub(window, 'isSecureContext').get(() => true); + toFetchRequest(EXAMPLE_URL, null, opts); + sinon.assert.calledWithMatch(dep.makeRequest, sinon.match.any, {browsingTopics: shouldBeSet ? true : undefined}); + }); + it(`should not be set when not in a secure context`, () => { + sandbox.stub(window, 'isSecureContext').get(() => false); + toFetchRequest(EXAMPLE_URL, null, opts); + sinon.assert.calledWithMatch(dep.makeRequest, sinon.match.any, {browsingTopics: undefined}); + }); }) }) }) From c73020b22a748011eaf209b102b86e7ede6ddbb5 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Wed, 9 Aug 2023 11:32:11 -0700 Subject: [PATCH 4/4] Improve mockFetchServer requestHeaders property --- test/mocks/xhr.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/test/mocks/xhr.js b/test/mocks/xhr.js index c92b842d4ab..e7b1d96f0a4 100644 --- a/test/mocks/xhr.js +++ b/test/mocks/xhr.js @@ -52,10 +52,23 @@ function mockFetchServer() { statusText: '', requestHeaders: new Proxy(request.headers, { get(target, prop) { - return target[prop] != null ? target[prop] : target.get(prop); + return typeof prop === 'string' && target.has(prop) ? target.get(prop) : {}[prop]; }, has(target, prop) { - return target.has(prop); + return typeof prop === 'string' && target.has(prop); + }, + ownKeys(target) { + return Array.from(target.keys()); + }, + getOwnPropertyDescriptor(target, prop) { + if (typeof prop === 'string' && target.has(prop)) { + return { + enumerable: true, + configurable: true, + writable: false, + value: target.get(prop) + } + } } }), withCredentials: request.credentials === 'include',