diff --git a/.babelrc.js b/.babelrc.js index 43303f59a8b..add243a5b5d 100644 --- a/.babelrc.js +++ b/.babelrc.js @@ -1,25 +1 @@ - -let path = require('path'); - -function useLocal(module) { - return require.resolve(module, { - paths: [ - __dirname - ] - }) -} - -module.exports = { - "presets": [ - [ - useLocal('@babel/preset-env'), - { - "useBuiltIns": "entry" - } - ] - ], - "plugins": [ - path.resolve(__dirname, './plugins/pbjsGlobals.js'), - useLocal('babel-plugin-transform-object-assign') - ] -}; +module.exports = require('./babelConfig.js')(); diff --git a/.circleci/config.yml b/.circleci/config.yml index ea5fb916a91..784b520ecc1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,11 +3,11 @@ # Check https://circleci.com/docs/2.0/language-javascript/ for more details # -aliases: +aliases: - &environment docker: # specify the version you desire here - - image: circleci/node:12.16.1 + - image: circleci/node:12.16.1-browsers resource_class: xlarge # Specify service dependencies here if necessary # CircleCI maintains a library of pre-built images @@ -52,7 +52,7 @@ aliases: - &unit_test_steps - checkout - restore_cache: *restore_dep_cache - - run: npm install + - run: npm ci - save_cache: *save_dep_cache - run: *install - run: *setup_browserstack @@ -72,7 +72,7 @@ jobs: build: <<: *environment steps: *unit_test_steps - + e2etest: <<: *environment steps: *endtoend_test_steps diff --git a/.eslintrc.js b/.eslintrc.js index 78e4fb1bb33..d3379d70919 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -23,10 +23,13 @@ module.exports = { 'BROWSERSTACK_USERNAME': false, 'BROWSERSTACK_KEY': false }, + // use babel as parser for fancy syntax + parser: '@babel/eslint-parser', parserOptions: { sourceType: 'module', ecmaVersion: 2018, }, + rules: { 'comma-dangle': 'off', semi: 'off', @@ -49,5 +52,9 @@ module.exports = { rules: { 'prebid/validate-imports': ['error', allowedModules[key]] } - })) + })).concat([{ + // code in other packages (such as plugins/eslint) is not "seen" by babel and its parser will complain. + files: 'plugins/*/**/*.js', + parser: 'esprima' + }]) }; diff --git a/.github/workflows/issue_tracker.yml b/.github/workflows/issue_tracker.yml new file mode 100644 index 00000000000..4397337b4c7 --- /dev/null +++ b/.github/workflows/issue_tracker.yml @@ -0,0 +1,89 @@ +name: Issue tracking +on: + issues: + types: + - opened +jobs: + track_issue: + runs-on: ubuntu-latest + steps: + - name: Generate token + id: generate_token + uses: tibdex/github-app-token@36464acb844fc53b9b8b2401da68844f6b05ebb0 + with: + app_id: ${{ secrets.ISSUE_APP_ID }} + private_key: ${{ secrets.ISSUE_APP_PEM }} + + - name: Get project data + env: + GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} + ORGANIZATION: prebid + DATE_FIELD: Created on + PROJECT_NUMBER: 2 + run: | + gh api graphql -f query=' + query($org: String!, $number: Int!) { + organization(login: $org){ + projectNext(number: $number) { + id + fields(first:100) { + nodes { + id + name + settings + } + } + } + } + }' -f org=$ORGANIZATION -F number=$PROJECT_NUMBER > project_data.json + + echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV + echo 'DATE_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "'"$DATE_FIELD"'") | .id' project_data.json) >> $GITHUB_ENV + + - name: Add issue to project + env: + GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} + ISSUE_ID: ${{ github.event.issue.node_id }} + run: | + gh api graphql -f query=' + mutation($project:ID!, $issue:ID!) { + addProjectNextItem(input: {projectId: $project, contentId: $issue}) { + projectNextItem { + id, + content { + ... on Issue { + createdAt + } + ... on PullRequest { + createdAt + } + } + } + } + }' -f project=$PROJECT_ID -f issue=$ISSUE_ID > issue_data.json + + echo 'ITEM_ID='$(jq '.data.addProjectNextItem.projectNextItem.id' issue_data.json) >> $GITHUB_ENV + echo 'ITEM_CREATION_DATE='$(jq '.data.addProjectNextItem.projectNextItem.content.createdAt' issue_data.json) >> $GITHUB_ENV + + - name: Set fields + env: + GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} + run: | + gh api graphql -f query=' + mutation ( + $project: ID! + $item: ID! + $date_field: ID! + $date_value: String! + ) { + set_creation_date: updateProjectNextItemField(input: { + projectId: $project + itemId: $item + fieldId: $date_field + value: $date_value + }) { + projectNextItem { + id + } + } + }' -f project=$PROJECT_ID -f item=$ITEM_ID -f date_field=$DATE_FIELD_ID -f date_value=$ITEM_CREATION_DATE --silent diff --git a/README.md b/README.md index 11fce459e56..42d747b20b6 100644 --- a/README.md +++ b/README.md @@ -130,16 +130,22 @@ Once setup, run the following command to globally install the `gulp-cli` package ## Build for Development -To build the project on your local machine, run: +To build the project on your local machine we recommend, running: - $ gulp serve + $ gulp serve-and-test --file -This runs some code quality checks, starts a web server at `http://localhost:9999` serving from the project root and generates the following files: +This will run testing but not linting. A web server will start at `http://localhost:9999` serving from the project root and generates the following files: + `./build/dev/prebid.js` - Full source code for dev and debug + `./build/dev/prebid.js.map` - Source map for dev and debug -+ `./build/dist/prebid.js` - Minified production code -+ `./prebid.js_.zip` - Distributable zip archive ++ `./build/dev/prebid-core.js` ++ `./build/dev/prebid-core.js.map` + + +Development may be a bit slower but if you prefer linting and additional watch files you can also still run just: + + $ gulp serve + ### Build Optimization @@ -274,7 +280,7 @@ As you make code changes, the bundles will be rebuilt and the page reloaded auto ## Contribute -Many SSPs, bidders, and publishers have contributed to this project. [Hundreds of bidders](https://github.com/prebid/Prebid.js/tree/master/src/adapters) are supported by Prebid.js. +Many SSPs, bidders, and publishers have contributed to this project. [Hundreds of bidders](https://github.com/prebid/Prebid.js/tree/master/modules) are supported by Prebid.js. For guidelines, see [Contributing](./CONTRIBUTING.md). diff --git a/RELEASE_SCHEDULE.md b/RELEASE_SCHEDULE.md index bfbd0772c3e..b68495ed4ae 100644 --- a/RELEASE_SCHEDULE.md +++ b/RELEASE_SCHEDULE.md @@ -3,12 +3,7 @@ - [Release Process](#release-process) - [1. Make sure that all PRs have been named and labeled properly per the PR Process](#1-make-sure-that-all-prs-have-been-named-and-labeled-properly-per-the-pr-process) - [2. Make sure all browserstack tests are passing](#2-make-sure-all-browserstack-tests-are-passing) - - [3. Prepare Prebid Code](#3-prepare-prebid-code) - - [4. Verify the Release](#4-verify-the-release) - - [5. Create a GitHub release](#5-create-a-github-release) - - [6. Update coveralls _(skip for legacy)_](#6-update-coveralls-skip-for-legacy) - - [7. Distribute the code](#7-distribute-the-code) - - [8. Increment Version for Next Release](#8-increment-version-for-next-release) + - [3. Start the release](#3-start-the-release) - [Beta Releases](#beta-releases) - [FAQs](#faqs) @@ -21,12 +16,10 @@ it will be about a week before the Prebid Org [Download Page](http://prebid.org/ You can determine what is in a given build using the [releases page](https://github.com/prebid/Prebid.js/releases) -Announcements regarding releases will be made to the #headerbidding-dev channel in subredditadops.slack.com. +Announcements regarding releases will be made to the #prebid-js channel in prebid.slack.com. ## Release Process -_Note: If `github.com/prebid/Prebid.js` is not configured as the git origin for your repo, all of the following git commands will have to be modified to reference the proper remote (e.g. `upstream`)_ - ### 1. Make sure that all PRs have been named and labeled properly per the [PR Process](https://github.com/prebid/Prebid.js/blob/master/PR_REVIEW.md#general-pr-review-process) * Do this by checking the latest draft release from the [releases page](https://github.com/prebid/Prebid.js/releases) and make sure nothing appears in the first section called "In This Release". If they do, please open the PRs and add the appropriate labels. * Do a quick check that all the titles/descriptions look ok, and if not, adjust the PR title. @@ -57,61 +50,10 @@ _Note: If `github.com/prebid/Prebid.js` is not configured as the git origin for ``` -### 3. Prepare Prebid Code - - Update the package.json version to become the current release. Then commit your changes. - - ``` - git commit -m "Prebid 4.x.x Release" - git push - ``` - -### 4. Verify the Release - - Make sure your there are no more merges to master branch. Prebid code is clean and up to date. - -### 5. Create a GitHub release - - Edit the most recent [release notes](https://github.com/prebid/Prebid.js/releases) draft and make sure the correct version is set and the master branch is selected in the dropdown. Click `Publish release`. GitHub will create release tag. - - Pull these changes locally by running command - ``` - git pull - git fetch --tags - ``` - - and verify the tag. - -### 6. Update coveralls _(skip for legacy)_ - - We use https://coveralls.io/ to show parts of code covered by unit tests. - - Set the environment variables. You may want to add these to your `~/.bashrc` for convenience. - ``` - export COVERALLS_SERVICE_NAME="travis-ci" - export COVERALLS_REPO_TOKEN="talk to Matt Kendall" - ``` - - Run `gulp coveralls` to update code coverage history. - -### 7. Distribute the code - - _Note: do not go to step 8 until step 7 has been verified completed._ - - Reach out to any of the Appnexus folks to trigger the jenkins job. - - // TODO: - Jenkins job is moving files to appnexus cdn, pushing prebid.js to npm, purging cache and sending notification to slack. - Move all the files from Appnexus CDN to jsDelivr and create bash script to do above tasks. - -### 8. Increment Version for Next Release - - Update the version by manually editing Prebid's `package.json` to become "4.x.x-pre" (using the values for the next release). Then commit your changes. - ``` - git commit -m "Increment pre version" - git push - ``` +### 3. Start the release +Follow the instructions at https://github.com/prebid/prebidjs-releaser. Note that you will need to be a member of the [https://github.com/orgs/prebid/teams/prebidjs-release](prebidjs-release) GitHub team. + ## Beta Releases Prebid.js features may be released as Beta or as Generally Available (GA). diff --git a/allowedModules.js b/allowedModules.js index 81920cdc15f..be9a2dc2abf 100644 --- a/allowedModules.js +++ b/allowedModules.js @@ -1,12 +1,5 @@ const sharedWhiteList = [ - 'core-js-pure/features/array/find', // no ie11 - 'core-js-pure/features/array/includes', // no ie11 - 'core-js-pure/features/set', // ie11 supports Set but not Set#values - 'core-js-pure/features/string/includes', // no ie11 - 'core-js-pure/features/number/is-integer', // no ie11, - 'core-js-pure/features/array/from', // no ie11 - 'core-js-pure/web/url-search-params' // no ie11 ]; module.exports = { diff --git a/babelConfig.js b/babelConfig.js new file mode 100644 index 00000000000..c1ddc11b689 --- /dev/null +++ b/babelConfig.js @@ -0,0 +1,30 @@ + +let path = require('path'); + +function useLocal(module) { + return require.resolve(module, { + paths: [ + __dirname + ] + }) +} + +module.exports = function (test = false) { + return { + 'presets': [ + [ + useLocal('@babel/preset-env'), + { + 'useBuiltIns': 'entry', + 'corejs': '3.13.0', + // a lot of tests use sinon.stub & others that stopped working on ES6 modules with webpack 5 + 'modules': test ? 'commonjs' : 'auto', + } + ] + ], + 'plugins': [ + path.resolve(__dirname, './plugins/pbjsGlobals.js'), + useLocal('babel-plugin-transform-object-assign'), + ], + } +} diff --git a/browsers.json b/browsers.json index dd3955c47ea..bd6bd5772d6 100644 --- a/browsers.json +++ b/browsers.json @@ -1,65 +1,49 @@ { - "bs_edge_17_windows_10": { + "bs_edge_latest_windows_10": { "base": "BrowserStack", "os_version": "10", "browser": "edge", - "browser_version": "17.0", + "browser_version": "latest", "device": null, "os": "Windows" }, - "bs_edge_90_windows_10": { - "base": "BrowserStack", - "os_version": "10", - "browser": "edge", - "browser_version": "90.0", - "device": null, - "os": "Windows" - }, - "bs_chrome_90_windows_10": { + "bs_chrome_latest_windows_10": { "base": "BrowserStack", "os_version": "10", "browser": "chrome", - "browser_version": "90.0", + "browser_version": "latest", "device": null, "os": "Windows" }, - "bs_chrome_79_windows_10": { + "bs_chrome_87_windows_10": { "base": "BrowserStack", "os_version": "10", "browser": "chrome", - "browser_version": "79.0", - "device": null, - "os": "Windows" - }, - "bs_firefox_88_windows_10": { - "base": "BrowserStack", - "os_version": "10", - "browser": "firefox", - "browser_version": "88.0", + "browser_version": "87.0", "device": null, "os": "Windows" }, - "bs_firefox_72_windows_10": { + "bs_firefox_latest_windows_10": { "base": "BrowserStack", "os_version": "10", "browser": "firefox", - "browser_version": "72.0", + "browser_version": "latest", "device": null, "os": "Windows" }, - "bs_safari_14_mac_bigsur": { + "bs_safari_latest_mac_bigsur": { "base": "BrowserStack", "os_version": "Big Sur", "browser": "safari", - "browser_version": "14.0", + "browser_version": "latest", "device": null, "os": "OS X" }, - "bs_safari_12_mac_mojave": { + "bs_safari_15_catalina": { "base": "BrowserStack", - "os_version": "Mojave", + "os_version": "Catalina", "browser": "safari", - "browser_version": "12.0", + "browser_version": "13.1", "device": null, "os": "OS X" } diff --git a/gulpfile.js b/gulpfile.js index 98caf1d7312..ef51dacf45e 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -8,7 +8,6 @@ var gutil = require('gulp-util'); var connect = require('gulp-connect'); var webpack = require('webpack'); var webpackStream = require('webpack-stream'); -var terser = require('gulp-terser'); var gulpClean = require('gulp-clean'); var KarmaServer = require('karma').Server; var karmaConfMaker = require('./karma.conf.maker.js'); @@ -115,41 +114,12 @@ function viewReview(done) { viewReview.displayName = 'view-review'; -// Watch Task with Live Reload -function watch(done) { - var mainWatcher = gulp.watch([ - 'src/**/*.js', - 'modules/**/*.js', - 'test/spec/**/*.js', - '!test/spec/loaders/**/*.js' - ]); - var loaderWatcher = gulp.watch([ - 'loaders/**/*.js', - 'test/spec/loaders/**/*.js' - ]); - - connect.server({ - https: argv.https, - port: port, - host: FAKE_SERVER_HOST, - root: './', - livereload: true - }); - - mainWatcher.on('all', gulp.series(clean, gulp.parallel(lint, 'build-bundle-dev', test))); - loaderWatcher.on('all', gulp.series(lint)); - done(); -}; - -function makeModuleList(modules) { - return modules.map(module => { - return '"' + module + '"' - }); -} - function makeDevpackPkg() { var cloned = _.cloneDeep(webpackConfig); - cloned.devtool = 'source-map'; + Object.assign(cloned, { + devtool: 'source-map', + mode: 'development' + }) var externalModules = helpers.getArgModules(); const analyticsSources = helpers.getAnalyticsSources(); @@ -158,7 +128,6 @@ function makeDevpackPkg() { return gulp.src([].concat(moduleSources, analyticsSources, 'src/prebid.js')) .pipe(helpers.nameModules(externalModules)) .pipe(webpackStream(cloned, webpack)) - .pipe(replace(/('|")v\$prebid\.modulesList\$('|")/g, makeModuleList(externalModules))) .pipe(gulp.dest('build/dev')) .pipe(connect.reload()); } @@ -175,8 +144,6 @@ function makeWebpackPkg() { return gulp.src([].concat(moduleSources, analyticsSources, 'src/prebid.js')) .pipe(helpers.nameModules(externalModules)) .pipe(webpackStream(cloned, webpack)) - .pipe(terser()) - .pipe(replace(/('|")v\$prebid\.modulesList\$('|")/g, makeModuleList(externalModules))) .pipe(gulpif(file => file.basename === 'prebid-core.js', header(banner, { prebid: prebid }))) .pipe(gulp.dest('build/dist')); } @@ -255,60 +222,68 @@ function bundle(dev, moduleArr) { // If --browsers is given, browsers can be chosen explicitly. e.g. --browsers=chrome,firefox,ie9 // If --notest is given, it will immediately skip the test task (useful for developing changes with `gulp serve --notest`) -function test(done) { - if (argv.notest) { - done(); - } else if (argv.e2e) { - let wdioCmd = path.join(__dirname, 'node_modules/.bin/wdio'); - let wdioConf = path.join(__dirname, 'wdio.conf.js'); - let wdioOpts; - - if (argv.file) { - wdioOpts = [ - wdioConf, - `--spec`, - `${argv.file}` - ] - } else { - wdioOpts = [ - wdioConf - ]; - } +function testTaskMaker(options = {}) { + ['watch', 'e2e', 'file', 'browserstack', 'notest'].forEach(opt => { + options[opt] = options[opt] || argv[opt]; + }) - // run fake-server - const fakeServer = spawn('node', ['./test/fake-server/index.js', `--port=${FAKE_SERVER_PORT}`]); - fakeServer.stdout.on('data', (data) => { - console.log(`stdout: ${data}`); - }); - fakeServer.stderr.on('data', (data) => { - console.log(`stderr: ${data}`); - }); + return function test(done) { + if (options.notest) { + done(); + } else if (options.e2e) { + let wdioCmd = path.join(__dirname, 'node_modules/.bin/wdio'); + let wdioConf = path.join(__dirname, 'wdio.conf.js'); + let wdioOpts; + + if (options.file) { + wdioOpts = [ + wdioConf, + `--spec`, + `${options.file}` + ] + } else { + wdioOpts = [ + wdioConf + ]; + } - execa(wdioCmd, wdioOpts, { stdio: 'inherit' }) - .then(stdout => { - // kill fake server - fakeServer.kill('SIGINT'); - done(); - process.exit(0); - }) - .catch(err => { - // kill fake server - fakeServer.kill('SIGINT'); - done(new Error(`Tests failed with error: ${err}`)); - process.exit(1); + // run fake-server + const fakeServer = spawn('node', ['./test/fake-server/index.js', `--port=${FAKE_SERVER_PORT}`]); + fakeServer.stdout.on('data', (data) => { + console.log(`stdout: ${data}`); + }); + fakeServer.stderr.on('data', (data) => { + console.log(`stderr: ${data}`); }); - } else { - var karmaConf = karmaConfMaker(false, argv.browserstack, argv.watch, argv.file); - var browserOverride = helpers.parseBrowserArgs(argv); - if (browserOverride.length > 0) { - karmaConf.browsers = browserOverride; - } + execa(wdioCmd, wdioOpts, { stdio: 'inherit' }) + .then(stdout => { + // kill fake server + fakeServer.kill('SIGINT'); + done(); + process.exit(0); + }) + .catch(err => { + // kill fake server + fakeServer.kill('SIGINT'); + done(new Error(`Tests failed with error: ${err}`)); + process.exit(1); + }); + } else { + var karmaConf = karmaConfMaker(false, options.browserstack, options.watch, options.file); - new KarmaServer(karmaConf, newKarmaCallback(done)).start(); + var browserOverride = helpers.parseBrowserArgs(argv); + if (browserOverride.length > 0) { + karmaConf.browsers = browserOverride; + } + + new KarmaServer(karmaConf, newKarmaCallback(done)).start(); + } } } +const test = testTaskMaker(); + function newKarmaCallback(done) { return function (exitCode) { if (exitCode) { @@ -385,6 +360,35 @@ function startFakeServer() { }); } +// Watch Task with Live Reload +function watchTaskMaker(options = {}) { + if (options.livereload == null) { + options.livereload = true; + } + options.alsoWatch = options.alsoWatch || []; + + return function watch(done) { + var mainWatcher = gulp.watch([ + 'src/**/*.js', + 'modules/**/*.js', + ].concat(options.alsoWatch)); + + connect.server({ + https: argv.https, + port: port, + host: FAKE_SERVER_HOST, + root: './', + livereload: options.livereload + }); + + mainWatcher.on('all', options.task()); + done(); + } +} + +const watch = watchTaskMaker({alsoWatch: ['test/**/*.js'], task: () => gulp.series(clean, gulp.parallel(lint, 'build-bundle-dev', test))}); +const watchFast = watchTaskMaker({livereload: false, task: () => gulp.series('build-bundle-dev')}); + // support tasks gulp.task(lint); gulp.task(watch); @@ -397,7 +401,8 @@ gulp.task('build-bundle-dev', gulp.series(makeDevpackPkg, gulpBundle.bind(null, gulp.task('build-bundle-prod', gulp.series(makeWebpackPkg, gulpBundle.bind(null, false))); // public tasks (dependencies are needed for each task since they can be ran on their own) -gulp.task('test', gulp.series(clean, lint, test)); +gulp.task('test-only', test); +gulp.task('test', gulp.series(clean, lint, 'test-only')); gulp.task('test-coverage', gulp.series(clean, testCoverage)); gulp.task(viewCoverage); @@ -408,7 +413,8 @@ gulp.task('build', gulp.series(clean, 'build-bundle-prod')); gulp.task('build-postbid', gulp.series(escapePostbidConfig, buildPostbid)); gulp.task('serve', gulp.series(clean, lint, gulp.parallel('build-bundle-dev', watch, test))); -gulp.task('serve-fast', gulp.series(clean, gulp.parallel('build-bundle-dev', watch))); +gulp.task('serve-fast', gulp.series(clean, gulp.parallel('build-bundle-dev', watchFast))); +gulp.task('serve-and-test', gulp.series(clean, gulp.parallel('build-bundle-dev', watchFast, testTaskMaker({watch: true})))); gulp.task('serve-fake', gulp.series(clean, gulp.parallel('build-bundle-dev', watch), injectFakeServerEndpointDev, test, startFakeServer)); gulp.task('default', gulp.series(clean, makeWebpackPkg)); diff --git a/integrationExamples/gpt/adloox.html b/integrationExamples/gpt/adloox.html index e8920cf2ee1..fd61267479d 100644 --- a/integrationExamples/gpt/adloox.html +++ b/integrationExamples/gpt/adloox.html @@ -161,7 +161,11 @@ }, rubicon: { singleRequest: true - } + }, + // RTD module honors pageUrl for referrer detection and + // the analytics module uses this for the 'pageurl' macro + // N.B. set this to a non-example.com URL to see the video + //pageUrl: 'https://yourdomain.com/some/path/to/content.html' }); pbjs.enableAnalytics({ provider: 'adloox', diff --git a/integrationExamples/gpt/amp/creative.html b/integrationExamples/gpt/amp/creative.html index 86f669dd6b5..384b81107cc 100644 --- a/integrationExamples/gpt/amp/creative.html +++ b/integrationExamples/gpt/amp/creative.html @@ -1,38 +1,16 @@ + diff --git a/integrationExamples/gpt/esp_example.html b/integrationExamples/gpt/esp_example.html new file mode 100644 index 00000000000..c39a67243cc --- /dev/null +++ b/integrationExamples/gpt/esp_example.html @@ -0,0 +1,177 @@ + + + + + + + + + + + + +

Basic Prebid.js Example

+ +
Div-1
+
+ +
+ +
+ +
Div-2
+
+ +
+ + + + \ No newline at end of file diff --git a/integrationExamples/gpt/haloRtdProvider_example.html b/integrationExamples/gpt/hadronRtdProvider_example.html similarity index 87% rename from integrationExamples/gpt/haloRtdProvider_example.html rename to integrationExamples/gpt/hadronRtdProvider_example.html index 14debbd2698..065c8379956 100644 --- a/integrationExamples/gpt/haloRtdProvider_example.html +++ b/integrationExamples/gpt/hadronRtdProvider_example.html @@ -1,8 +1,8 @@ -Halo Id: -
+Hadron Id: +
-Halo Real-Time Data: +Hadron Real-Time Data:
diff --git a/integrationExamples/gpt/idImportLibrary_example.html b/integrationExamples/gpt/idImportLibrary_example.html index 07a4f0fe1c5..363e8015f53 100644 --- a/integrationExamples/gpt/idImportLibrary_example.html +++ b/integrationExamples/gpt/idImportLibrary_example.html @@ -69,10 +69,10 @@ name: "zeotapIdPlus" }, { - name: 'haloId', + name: 'hadronId', storage: { type: "html5", - name: "haloId", + name: "hadronId", expires: 28 } }, { diff --git a/integrationExamples/gpt/idward_segments_example.html b/integrationExamples/gpt/idward_segments_example.html new file mode 100644 index 00000000000..9bc06124c77 --- /dev/null +++ b/integrationExamples/gpt/idward_segments_example.html @@ -0,0 +1,112 @@ + + + + + + + + + + + + + +

Prebid.js Test

+
Div-1
+
+ +
+
First Party Data (ortb2) Sent to Bidding Adapter
+
+ + diff --git a/integrationExamples/gpt/userId_example.html b/integrationExamples/gpt/userId_example.html index 653dd9c59f3..e11a0b626c9 100644 --- a/integrationExamples/gpt/userId_example.html +++ b/integrationExamples/gpt/userId_example.html @@ -215,10 +215,10 @@ "name": "zeotapIdPlus" }, { - "name": "haloId", + "name": "hadronId", "storage": { "type": "cookie", - "name": "haloId", + "name": "hadronId", "expires": 28 } }, @@ -252,6 +252,9 @@ "params": { "cid": 5126 // Set your Intimate Merger Customer ID here for production } + }, + { + "name": "dacId" } ], "syncDelay": 5000, diff --git a/integrationExamples/gpt/weboramaRtdProvider_example.html b/integrationExamples/gpt/weboramaRtdProvider_example.html index 824d7a2f0c7..b81ec52b2c4 100644 --- a/integrationExamples/gpt/weboramaRtdProvider_example.html +++ b/integrationExamples/gpt/weboramaRtdProvider_example.html @@ -1,126 +1,174 @@ - + + + - - + + - + - - -
-

-test webo ctx using prebid.js -

-
-

Basic Prebid.js Example

-
Div-1
-
- -
- + }); + } + + + // in case PBJS doesn't load + setTimeout(function () { + initAdserver(); + }, FAILSAFE_TIMEOUT); + + googletag.cmd.push(function () { + googletag.defineSlot('/1056029/webo-ctx-prebid', div_1_sizes, 'div-gpt-ad-1620653642627-0').addService(googletag.pubads()); + googletag.pubads().disableInitialLoad(); + googletag.enableServices(); + }); + + + +
+

+ test webo ctx using prebid.js +

+
+

Basic Prebid.js Example

+
Div-1
+
+ +
+ - + + \ No newline at end of file diff --git a/integrationExamples/gpt/x-domain/creative.html b/integrationExamples/gpt/x-domain/creative.html index fce46bb380f..bea8b70b4fe 100644 --- a/integrationExamples/gpt/x-domain/creative.html +++ b/integrationExamples/gpt/x-domain/creative.html @@ -2,37 +2,45 @@ // this script can be returned by an ad server delivering a cross domain iframe, into which the // creative will be rendered, e.g. DFP delivering a SafeFrame -let windowLocation = window.location; -var urlParser = document.createElement('a'); +const windowLocation = window.location; +const urlParser = document.createElement('a'); urlParser.href = '%%PATTERN:url%%'; -var publisherDomain = urlParser.protocol + '//' + urlParser.hostname; +const publisherDomain = urlParser.protocol + '//' + urlParser.hostname; +const adId = '%%PATTERN:hb_adid%%'; + +function receiveMessage(ev) { + const origin = ev.origin || ev.originalEvent.origin; + if (origin === publisherDomain) { + renderAd(ev); + } +} function renderAd(ev) { - var key = ev.message ? 'message' : 'data'; - var adObject = {}; - try { - adObject = JSON.parse(ev[key]); - } catch (e) { - return; - } + const key = ev.message ? 'message' : 'data'; + let adObject = {}; + try { + adObject = JSON.parse(ev[key]); + } catch (e) { + return; + } - var origin = ev.origin || ev.originalEvent.origin; - if (adObject.message && adObject.message === 'Prebid Response' && - publisherDomain === origin && - adObject.adId === '%%PATTERN:hb_adid%%' && - (adObject.ad || adObject.adUrl)) { - var body = window.document.body; - var ad = adObject.ad; - var url = adObject.adUrl; - var width = adObject.width; - var height = adObject.height; + if (adObject.message && adObject.message === 'Prebid Response' && + adObject.adId === adId) { + try { + const body = window.document.body; + const ad = adObject.ad; + const url = adObject.adUrl; + const width = adObject.width; + const height = adObject.height; if (adObject.mediaType === 'video') { + signalRenderResult(false, { + reason: 'preventWritingOnMainDocument', + message: `Cannot render video ad ${adId}` + }); console.log('Error trying to write ad.'); - } else - - if (ad) { - var frame = document.createElement('iframe'); + } else if (ad) { + const frame = document.createElement('iframe'); frame.setAttribute('FRAMEBORDER', 0); frame.setAttribute('SCROLLING', 'no'); frame.setAttribute('MARGINHEIGHT', 0); @@ -46,24 +54,50 @@ frame.contentDocument.open(); frame.contentDocument.write(ad); frame.contentDocument.close(); + signalRenderResult(true); } else if (url) { body.insertAdjacentHTML('beforeend', ''); + signalRenderResult(true); } else { - console.log('Error trying to write ad. No ad for bid response id: ' + id); + signalRenderResult(false, { + reason: 'noAd', + message: `No ad for ${adId}` + }); + console.log(`Error trying to write ad. No ad markup or adUrl for ${adId}`); } + } catch (e) { + signalRenderResult(false, {reason: 'exception', message: e.message}); + console.log(`Error in rendering ad`, e); + } + } + + function signalRenderResult(success, {reason, message} = {}) { + const payload = { + message: 'Prebid Event', + adId, + event: success ? 'adRenderSucceeded' : 'adRenderFailed', } + if (!success) { + payload.info = {reason, message}; + } + window.parent.postMessage(JSON.stringify(payload), publisherDomain); } +} + + function requestAdFromPrebid() { - var message = JSON.stringify({ + const message = JSON.stringify({ message: 'Prebid Request', - adId: '%%PATTERN:hb_adid%%' + adId }); - window.parent.postMessage(message, publisherDomain); + const channel = new MessageChannel(); + channel.port1.onmessage = renderAd; + window.parent.postMessage(message, publisherDomain, [channel.port2]); } function listenAdFromPrebid() { - window.addEventListener('message', renderAd, false); + window.addEventListener('message', receiveMessage, false); } listenAdFromPrebid(); diff --git a/karma.conf.maker.js b/karma.conf.maker.js index cf5999ba85e..b5c6b44e4fd 100644 --- a/karma.conf.maker.js +++ b/karma.conf.maker.js @@ -2,6 +2,7 @@ // // For more information, see http://karma-runner.github.io/1.0/config/configuration-file.html +const babelConfig = require('./babelConfig.js'); var _ = require('lodash'); var webpackConf = require('./webpack.conf.js'); var karmaConstants = require('karma').constants; @@ -10,10 +11,19 @@ function newWebpackConfig(codeCoverage) { // Make a clone here because we plan on mutating this object, and don't want parallel tasks to trample each other. var webpackConfig = _.cloneDeep(webpackConf); - // remove optimize plugin for tests - webpackConfig.plugins.pop() + Object.assign(webpackConfig, { + mode: 'development', + devtool: 'inline-source-map', + }); - webpackConfig.devtool = 'inline-source-map'; + delete webpackConfig.entry; + + webpackConfig.module.rules + .flatMap((r) => r.use) + .filter((use) => use.loader === 'babel-loader') + .forEach((use) => { + use.options = babelConfig(true); + }); if (codeCoverage) { webpackConfig.module.rules.push({ @@ -111,7 +121,7 @@ module.exports = function(codeCoverage, browserstack, watchMode, file) { var webpackConfig = newWebpackConfig(codeCoverage); var plugins = newPluginsArray(browserstack); - var files = file ? ['test/helpers/prebidGlobal.js', file] : ['test/test_index.js']; + var files = file ? ['test/test_deps.js', file] : ['test/test_index.js']; // This file opens the /debug.html tab automatically. // It has no real value unless you're running --watch, and intend to do some debugging in the browser. if (watchMode) { @@ -166,7 +176,7 @@ module.exports = function(codeCoverage, browserstack, watchMode, file) { browserNoActivityTimeout: 3e5, // default 10000 captureTimeout: 3e5, // default 60000, browserDisconnectTolerance: 3, - concurrency: 5, + concurrency: 6, plugins: plugins } diff --git a/modules/.submodules.json b/modules/.submodules.json index ea3f556dbb4..85e4658cc61 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -7,16 +7,20 @@ "britepoolIdSystem", "connectIdSystem", "criteoIdSystem", + "dacIdSystem", "deepintentDpesIdSystem", "dmdIdSystem", "fabrickIdSystem", "flocIdSystem", + "hadronIdSystem", "haloIdSystem", "id5IdSystem", + "ftrackIdSystem", "identityLinkIdSystem", "idxIdSystem", "imuIdSystem", "intentIqIdSystem", + "justIdSystem", "kinessoIdSystem", "liveIntentIdSystem", "lotamePanoramaIdSystem", @@ -32,10 +36,12 @@ "quantcastIdSystem", "sharedIdSystem", "tapadIdSystem", + "trustpidSystem", "uid2IdSystem", "unifiedIdSystem", "verizonMediaIdSystem", - "zeotapIdPlusIdSystem" + "zeotapIdPlusIdSystem", + "adqueryIdSystem" ], "adpod": [ "freeWheelAdserverVideo", @@ -45,6 +51,7 @@ "browsiRtdProvider", "dgkeywordRtdProvider", "geoedgeRtdProvider", + "hadronRtdProvider", "haloRtdProvider", "iasRtdProvider", "jwplayerRtdProvider", diff --git a/modules/33acrossBidAdapter.js b/modules/33acrossBidAdapter.js index 2bdbdd6414b..498e6cf8634 100644 --- a/modules/33acrossBidAdapter.js +++ b/modules/33acrossBidAdapter.js @@ -1,16 +1,26 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import { - deepAccess, uniques, isArray, getWindowTop, isGptPubadsDefined, isSlotMatchingAdUnitCode, logInfo, logWarn, - getWindowSelf + deepAccess, + uniques, + isArray, + getWindowTop, + isGptPubadsDefined, + isSlotMatchingAdUnitCode, + logInfo, + logWarn, + getWindowSelf, + mergeDeep, } from '../src/utils.js'; -import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +// **************************** UTILS *************************** // const BIDDER_CODE = '33across'; const END_POINT = 'https://ssc.33across.com/api/v1/hb'; const SYNC_ENDPOINT = 'https://ssc-cms.33across.com/ps/?m=xch&rt=html&ru=deb'; const CURRENCY = 'USD'; +const GVLID = 58; const GUID_PATTERN = /^[a-zA-Z0-9_-]{22}$/; const PRODUCT = { @@ -42,6 +52,14 @@ const adapterState = { const NON_MEASURABLE = 'nm'; +function getTTXConfig() { + const ttxSettings = Object.assign({}, + config.getConfig('ttxSettings') + ); + + return ttxSettings; +} + // **************************** VALIDATION *************************** // function isBidRequestValid(bid) { return ( @@ -74,6 +92,7 @@ function _validateGUID(bid) { function _validateBanner(bid) { const banner = deepAccess(bid, 'mediaTypes.banner'); + // If there's no banner no need to validate against banner rules if (banner === undefined) { return true; @@ -140,91 +159,125 @@ function _validateVideo(bid) { // NOTE: With regards to gdrp consent data, the server will independently // infer the gdpr applicability therefore, setting the default value to false function buildRequests(bidRequests, bidderRequest) { + const { + ttxSettings, + gdprConsent, + uspConsent, + pageUrl + } = _buildRequestParams(bidRequests, bidderRequest); + + const groupedRequests = _buildRequestGroups(ttxSettings, bidRequests); + + const serverRequests = []; + + for (const key in groupedRequests) { + serverRequests.push( + _createServerRequest({ + bidRequests: groupedRequests[key], + gdprConsent, + uspConsent, + pageUrl, + ttxSettings + }) + ) + } + + return serverRequests; +} + +function _buildRequestParams(bidRequests, bidderRequest) { + const ttxSettings = getTTXConfig(); + const gdprConsent = Object.assign({ consentString: undefined, gdprApplies: false }, bidderRequest && bidderRequest.gdprConsent); const uspConsent = bidderRequest && bidderRequest.uspConsent; + const pageUrl = (bidderRequest && bidderRequest.refererInfo) ? (bidderRequest.refererInfo.referer) : (undefined); adapterState.uniqueSiteIds = bidRequests.map(req => req.params.siteId).filter(uniques); - return bidRequests.map(bidRequest => _createServerRequest( - { - bidRequest, - gdprConsent, - uspConsent, - pageUrl - }) - ); + return { + ttxSettings, + gdprConsent, + uspConsent, + pageUrl + } +} + +function _buildRequestGroups(ttxSettings, bidRequests) { + const bidRequestsComplete = bidRequests.map(_inferProduct); + const enableSRAMode = ttxSettings && ttxSettings.enableSRAMode; + const keyFunc = (enableSRAMode === true) ? _getSRAKey : _getMRAKey; + + return _groupBidRequests(bidRequestsComplete, keyFunc); +} + +function _groupBidRequests(bidRequests, keyFunc) { + const groupedRequests = {}; + + bidRequests.forEach((req) => { + const key = keyFunc(req); + + groupedRequests[key] = groupedRequests[key] || []; + groupedRequests[key].push(req); + }); + + return groupedRequests; +} + +function _getSRAKey(bidRequest) { + return `${bidRequest.params.siteId}:${bidRequest.params.productId}`; +} + +function _getMRAKey(bidRequest) { + return `${bidRequest.bidId}`; } // Infer the necessary data from valid bid for a minimal ttxRequest and create HTTP request -// NOTE: At this point, TTX only accepts request for a single impression -function _createServerRequest({bidRequest, gdprConsent = {}, uspConsent, pageUrl}) { +function _createServerRequest({ bidRequests, gdprConsent = {}, uspConsent, pageUrl, ttxSettings }) { const ttxRequest = {}; - const params = bidRequest.params; + const { siteId, test } = bidRequests[0].params; /* * Infer data for the request payload */ - ttxRequest.imp = [{}]; + ttxRequest.imp = []; - if (deepAccess(bidRequest, 'mediaTypes.banner')) { - ttxRequest.imp[0].banner = { - ..._buildBannerORTB(bidRequest) - } - } - - if (deepAccess(bidRequest, 'mediaTypes.video')) { - ttxRequest.imp[0].video = _buildVideoORTB(bidRequest); - } - - ttxRequest.imp[0].ext = { - ttx: { - prod: _getProduct(bidRequest) - } - }; + bidRequests.forEach((req) => { + ttxRequest.imp.push(_buildImpORTB(req)); + }); - ttxRequest.site = { id: params.siteId }; + ttxRequest.site = { id: siteId }; if (pageUrl) { ttxRequest.site.page = pageUrl; } - // Go ahead send the bidId in request to 33exchange so it's kept track of in the bid response and - // therefore in ad targetting process - ttxRequest.id = bidRequest.bidId; + ttxRequest.id = bidRequests[0].auctionId; if (gdprConsent.consentString) { - ttxRequest.user = setExtension( - ttxRequest.user, - 'consent', - gdprConsent.consentString - ) + ttxRequest.user = setExtensions(ttxRequest.user, { + 'consent': gdprConsent.consentString + }); } - if (Array.isArray(bidRequest.userIdAsEids) && bidRequest.userIdAsEids.length > 0) { - ttxRequest.user = setExtension( - ttxRequest.user, - 'eids', - bidRequest.userIdAsEids - ) + if (Array.isArray(bidRequests[0].userIdAsEids) && bidRequests[0].userIdAsEids.length > 0) { + ttxRequest.user = setExtensions(ttxRequest.user, { + 'eids': bidRequests[0].userIdAsEids + }); } - ttxRequest.regs = setExtension( - ttxRequest.regs, - 'gdpr', - Number(gdprConsent.gdprApplies) - ); + ttxRequest.regs = setExtensions(ttxRequest.regs, { + 'gdpr': Number(gdprConsent.gdprApplies) + }); if (uspConsent) { - ttxRequest.regs = setExtension( - ttxRequest.regs, - 'us_privacy', - uspConsent - ) + ttxRequest.regs = setExtensions(ttxRequest.regs, { + 'us_privacy': uspConsent + }); } ttxRequest.ext = { @@ -237,16 +290,14 @@ function _createServerRequest({bidRequest, gdprConsent = {}, uspConsent, pageUrl } }; - if (bidRequest.schain) { - ttxRequest.source = setExtension( - ttxRequest.source, - 'schain', - bidRequest.schain - ) + if (bidRequests[0].schain) { + ttxRequest.source = setExtensions(ttxRequest.source, { + 'schain': bidRequests[0].schain + }); } // Finally, set the openRTB 'test' param if this is to be a test bid - if (params.test === 1) { + if (test === 1) { ttxRequest.test = 1; } @@ -259,8 +310,7 @@ function _createServerRequest({bidRequest, gdprConsent = {}, uspConsent, pageUrl }; // Allow the ability to configure the HB endpoint for testing purposes. - const ttxSettings = config.getConfig('ttxSettings'); - const url = (ttxSettings && ttxSettings.url) || `${END_POINT}?guid=${params.siteId}`; + const url = (ttxSettings && ttxSettings.url) || `${END_POINT}?guid=${siteId}`; // Return the server request return { @@ -272,14 +322,36 @@ function _createServerRequest({bidRequest, gdprConsent = {}, uspConsent, pageUrl } // BUILD REQUESTS: SET EXTENSIONS -function setExtension(obj = {}, key, value) { - return Object.assign({}, obj, { - ext: Object.assign({}, obj.ext, { - [key]: value - }) +function setExtensions(obj = {}, extFields) { + return mergeDeep({}, obj, { + 'ext': extFields }); } +// BUILD REQUESTS: IMP +function _buildImpORTB(bidRequest) { + const imp = { + id: bidRequest.bidId, + ext: { + ttx: { + prod: deepAccess(bidRequest, 'params.productId') + } + } + }; + + if (deepAccess(bidRequest, 'mediaTypes.banner')) { + imp.banner = { + ..._buildBannerORTB(bidRequest) + } + } + + if (deepAccess(bidRequest, 'mediaTypes.video')) { + imp.video = _buildVideoORTB(bidRequest); + } + + return imp; +} + // BUILD REQUESTS: SIZE INFERENCE function _transformSizes(sizes) { if (isArray(sizes) && sizes.length === 2 && !isArray(sizes[0])) { @@ -297,6 +369,14 @@ function _getSize(size) { } // BUILD REQUESTS: PRODUCT INFERENCE +function _inferProduct(bidRequest) { + return mergeDeep({}, bidRequest, { + params: { + productId: _getProduct(bidRequest) + } + }); +} + function _getProduct(bidRequest) { const { params, mediaTypes } = bidRequest; @@ -367,7 +447,7 @@ function _buildVideoORTB(bidRequest) { const video = {} - const {w, h} = _getSize(videoParams.playerSize[0]); + const { w, h } = _getSize(videoParams.playerSize[0]); video.w = w; video.h = h; @@ -388,11 +468,11 @@ function _buildVideoORTB(bidRequest) { if (product === PRODUCT.INSTREAM) { video.startdelay = video.startdelay || 0; video.placement = 1; - }; + } // bidfloors if (typeof bidRequest.getFloor === 'function') { - const bidfloors = _getBidFloors(bidRequest, {w: video.w, h: video.h}, VIDEO); + const bidfloors = _getBidFloors(bidRequest, { w: video.w, h: video.h }, VIDEO); if (bidfloors) { Object.assign(video, { @@ -404,6 +484,7 @@ function _buildVideoORTB(bidRequest) { }); } } + return video; } @@ -556,54 +637,61 @@ function _isIframe() { } // **************************** INTERPRET RESPONSE ******************************** // -// NOTE: At this point, the response from 33exchange will only ever contain one bid -// i.e. the highest bid function interpretResponse(serverResponse, bidRequest) { - const bidResponses = []; - - // If there are bids, look at the first bid of the first seatbid (see NOTE above for assumption about ttx) - if (serverResponse.body.seatbid.length > 0 && serverResponse.body.seatbid[0].bid.length > 0) { - bidResponses.push(_createBidResponse(serverResponse.body)); - } - - return bidResponses; + const { seatbid, cur = 'USD' } = serverResponse.body; + + if (!isArray(seatbid)) { + return []; + } + + // Pick seats with valid bids and convert them into an Array of responses + // in format expected by Prebid Core + return seatbid + .filter((seat) => ( + isArray(seat.bid) && + seat.bid.length > 0 + )) + .reduce((acc, seat) => { + return acc.concat( + seat.bid.map((bid) => _createBidResponse(bid, cur)) + ); + }, []); } -// All this assumes that only one bid is ever returned by ttx -function _createBidResponse(response) { +function _createBidResponse(bid, cur) { const isADomainPresent = - response.seatbid[0].bid[0].adomain && response.seatbid[0].bid[0].adomain.length; - const bid = { - requestId: response.id, + bid.adomain && bid.adomain.length; + const bidResponse = { + requestId: bid.impid, bidderCode: BIDDER_CODE, - cpm: response.seatbid[0].bid[0].price, - width: response.seatbid[0].bid[0].w, - height: response.seatbid[0].bid[0].h, - ad: response.seatbid[0].bid[0].adm, - ttl: response.seatbid[0].bid[0].ttl || 60, - creativeId: response.seatbid[0].bid[0].crid, - mediaType: deepAccess(response.seatbid[0].bid[0], 'ext.ttx.mediaType', BANNER), - currency: response.cur, + cpm: bid.price, + width: bid.w, + height: bid.h, + ad: bid.adm, + ttl: bid.ttl || 60, + creativeId: bid.crid, + mediaType: deepAccess(bid, 'ext.ttx.mediaType', BANNER), + currency: cur, netRevenue: true } if (isADomainPresent) { - bid.meta = { - advertiserDomains: response.seatbid[0].bid[0].adomain + bidResponse.meta = { + advertiserDomains: bid.adomain }; } - if (bid.mediaType === VIDEO) { - const vastType = deepAccess(response.seatbid[0].bid[0], 'ext.ttx.vastType', 'xml'); + if (bidResponse.mediaType === VIDEO) { + const vastType = deepAccess(bid, 'ext.ttx.vastType', 'xml'); if (vastType === 'xml') { - bid.vastXml = bid.ad; + bidResponse.vastXml = bidResponse.ad; } else { - bid.vastUrl = bid.ad; + bidResponse.vastUrl = bidResponse.ad; } } - return bid; + return bidResponse; } // **************************** USER SYNC *************************** // @@ -648,6 +736,7 @@ export const spec = { code: BIDDER_CODE, supportedMediaTypes: [ BANNER, VIDEO ], + gvlid: GVLID, isBidRequestValid, buildRequests, interpretResponse, diff --git a/modules/adagioBidAdapter.js b/modules/adagioBidAdapter.js index 264cf5f9fcb..a24bc889411 100644 --- a/modules/adagioBidAdapter.js +++ b/modules/adagioBidAdapter.js @@ -1,18 +1,37 @@ -import find from 'core-js-pure/features/array/find.js'; +import {find} from '../src/polyfill.js'; import { - isInteger, isArray, deepAccess, mergeDeep, logWarn, logInfo, logError, getWindowTop, getWindowSelf, generateUUID, _map, - getDNT, parseUrl, getUniqueIdentifierStr, isNumber, cleanObj, isFn, inIframe, deepClone, getGptSlotInfoForAdUnitCode + _map, + cleanObj, + deepAccess, + deepClone, + generateUUID, + getDNT, + getGptSlotInfoForAdUnitCode, + getUniqueIdentifierStr, + getWindowSelf, + getWindowTop, + inIframe, + isArray, + isFn, + isInteger, + isNumber, + logError, + logInfo, + logWarn, + mergeDeep, + parseUrl } from '../src/utils.js'; -import { config } from '../src/config.js'; +import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import { loadExternalScript } from '../src/adloader.js'; -import { verify } from 'criteo-direct-rsa-validate/build/verify.js'; -import { getStorageManager } from '../src/storageManager.js'; -import { getRefererInfo } from '../src/refererDetection.js'; -import { createEidsArray } from './userId/eids.js'; -import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { Renderer } from '../src/Renderer.js'; -import { OUTSTREAM } from '../src/video.js'; +import {loadExternalScript} from '../src/adloader.js'; +import {verify} from 'criteo-direct-rsa-validate/build/verify.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {getRefererInfo} from '../src/refererDetection.js'; +import {createEidsArray} from './userId/eids.js'; +import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; +import {Renderer} from '../src/Renderer.js'; +import {OUTSTREAM} from '../src/video.js'; + const BIDDER_CODE = 'adagio'; const LOG_PREFIX = 'Adagio:'; const FEATURES_VERSION = '1'; @@ -21,7 +40,7 @@ const SUPPORTED_MEDIA_TYPES = [BANNER, NATIVE, VIDEO]; const ADAGIO_TAG_URL = 'https://script.4dex.io/localstore.js'; const ADAGIO_LOCALSTORAGE_KEY = 'adagioScript'; const GVLID = 617; -export const storage = getStorageManager(GVLID, 'adagio'); +export const storage = getStorageManager({gvlid: GVLID, bidderCode: BIDDER_CODE}); export const RENDERER_URL = 'https://script.4dex.io/outstream-player.js'; const MAX_SESS_DURATION = 30 * 60 * 1000; const ADAGIO_PUBKEY = 'AL16XT44Sfp+8SHVF1UdC7hydPSMVLMhsYknKDdwqq+0ToDSJrP0+Qh0ki9JJI2uYm/6VEYo8TJED9WfMkiJ4vf02CW3RvSWwc35bif2SK1L8Nn/GfFYr/2/GG/Rm0vUsv+vBHky6nuuYls20Og0HDhMgaOlXoQ/cxMuiy5QSktp'; @@ -268,8 +287,6 @@ function getSite(bidderRequest) { } else if (refererInfo.stack && refererInfo.stack.length && refererInfo.stack[0]) { // important note check if refererInfo.stack[0] is 'thruly' because a `null` value // will be considered as "localhost" by the parseUrl function. - // As the isBidRequestValid returns false when it does not reach the referer - // this should never called. const url = parseUrl(refererInfo.stack[0]); domain = url.hostname; } @@ -308,7 +325,7 @@ function getElementFromTopWindow(element, currentWindow) { function autoDetectAdUnitElementIdFromGpt(adUnitCode) { const autoDetectedAdUnit = getGptSlotInfoForAdUnitCode(adUnitCode); - if (autoDetectedAdUnit && autoDetectedAdUnit.divId) { + if (autoDetectedAdUnit.divId) { return autoDetectedAdUnit.divId; } }; @@ -806,7 +823,7 @@ function getPrintNumber(adUnitCode, bidderRequest) { return 1; } const adagioBid = find(bidderRequest.bids, bid => bid.adUnitCode === adUnitCode); - return adagioBid.bidRequestsCount || 1; + return adagioBid.bidderRequestsCount || 1; } /** @@ -873,12 +890,6 @@ export const spec = { autoFillParams(bid); - if (!internal.getRefererInfo().reachedTop) { - logWarn(`${LOG_PREFIX} the main page url is unreachabled.`); - // internal.enqueue(debugData()); - return false; - } - if (!(bid.params.organizationId && bid.params.site && bid.params.placement)) { logWarn(`${LOG_PREFIX} at least one required param is missing.`); // internal.enqueue(debugData()); diff --git a/modules/adagioBidAdapter.md b/modules/adagioBidAdapter.md index 2779ced8cea..889822d9bd4 100644 --- a/modules/adagioBidAdapter.md +++ b/modules/adagioBidAdapter.md @@ -18,7 +18,7 @@ Below, the list of Adagio params and where they can be set. | ---------- | ------------- | ------------- | | siteId | x | | organizationId (obsolete) | | x -| site (obsolete) | | x +| site (obsolete) | | x | pagetype | x | x | environment | x | x | category | x | x diff --git a/modules/adbookpspBidAdapter.js b/modules/adbookpspBidAdapter.js index ca4795c574f..de8a3598be1 100644 --- a/modules/adbookpspBidAdapter.js +++ b/modules/adbookpspBidAdapter.js @@ -1,13 +1,24 @@ -import includes from 'core-js-pure/features/array/includes.js'; -import find from 'core-js-pure/features/array/find'; -import { config } from '../src/config.js'; -import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {find, includes} from '../src/polyfill.js'; +import {config} from '../src/config.js'; +import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; +import {getStorageManager} from '../src/storageManager.js'; import { - isPlainObject, deepSetValue, deepAccess, logWarn, inIframe, isNumber, logError, isArray, uniques, - flatten, triggerPixel, isStr, isEmptyStr, generateUUID + deepAccess, + deepSetValue, + flatten, + generateUUID, + inIframe, + isArray, + isEmptyStr, + isNumber, + isPlainObject, + isStr, + logError, + logWarn, + triggerPixel, + uniques } from '../src/utils.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; /** * CONSTANTS @@ -363,7 +374,7 @@ function impBidsToPrebidBids( } const impToPrebidBid = - (bidderRequestBody, bidResponseCurrency, referrer, targetingMap) => (bid) => { + (bidderRequestBody, bidResponseCurrency, referrer, targetingMap) => (bid, bidIndex) => { try { const bidRequest = findBidRequest(bidderRequestBody, bid); @@ -377,7 +388,7 @@ const impToPrebidBid = let prebidBid = { ad: bid.adm, adId: bid.adid, - adserverTargeting: targetingMap[bid.impid], + adserverTargeting: targetingMap[bidIndex], adUnitCode: bidRequest.tagid, bidderRequestId: bidderRequestBody.id, bidId: bid.id, @@ -408,6 +419,9 @@ const impToPrebidBid = }; } + if (deepAccess(bid, 'ext.pa_win') === true) { + prebidBid.auctionWinner = true; + } return prebidBid; } catch (error) { logError(`${BIDDER_CODE}: Error while building bid`, error); @@ -429,29 +443,43 @@ function buildTargetingMap(bids) { const values = impIds.reduce((result, id) => { result[id] = { lineItemIds: [], + orderIds: [], dealIds: [], adIds: [], + adAndOrderIndexes: [] }; return result; }, {}); - bids.forEach((bid) => { - values[bid.impid].lineItemIds.push(bid.ext.liid); - values[bid.impid].dealIds.push(bid.dealid); - values[bid.impid].adIds.push(bid.adid); + bids.forEach((bid, bidIndex) => { + let impId = bid.impid; + values[impId].lineItemIds.push(bid.ext.liid); + values[impId].dealIds.push(bid.dealid); + values[impId].adIds.push(bid.adid); + + if (deepAccess(bid, 'ext.ordid')) { + values[impId].orderIds.push(bid.ext.ordid); + bid.ext.ordid.split(TARGETING_VALUE_SEPARATOR).forEach((ordid, ordIndex) => { + let adIdIndex = values[impId].adIds.indexOf(bid.adid); + values[impId].adAndOrderIndexes.push(adIdIndex + '_' + ordIndex) + }) + } }); const targetingMap = {}; - for (const id of impIds) { - targetingMap[id] = { + bids.forEach((bid, bidIndex) => { + let id = bid.impid; + + targetingMap[bidIndex] = { hb_liid_adbookpsp: values[id].lineItemIds.join(TARGETING_VALUE_SEPARATOR), hb_deal_adbookpsp: values[id].dealIds.join(TARGETING_VALUE_SEPARATOR), + hb_ad_ord_adbookpsp: values[id].adAndOrderIndexes.join(TARGETING_VALUE_SEPARATOR), hb_adid_c_adbookpsp: values[id].adIds.join(TARGETING_VALUE_SEPARATOR), + hb_ordid_adbookpsp: values[id].orderIds.join(TARGETING_VALUE_SEPARATOR), }; - } - + }) return targetingMap; } @@ -560,7 +588,7 @@ function bannerHasSingleSize(bidRequest) { * USER SYNC */ -export const storage = getStorageManager(); +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); function getUserSyncs(syncOptions, responses, gdprConsent, uspConsent) { return responses diff --git a/modules/addefendBidAdapter.js b/modules/addefendBidAdapter.js index 18cafe829b5..dcc453ef35a 100644 --- a/modules/addefendBidAdapter.js +++ b/modules/addefendBidAdapter.js @@ -19,6 +19,7 @@ export const spec = { v: $$PREBID_GLOBAL$$.version, auctionId: false, pageId: false, + gdpr_applies: bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies ? bidderRequest.gdprConsent.gdprApplies : 'true', gdpr_consent: bidderRequest.gdprConsent && bidderRequest.gdprConsent.consentString ? bidderRequest.gdprConsent.consentString : '', referer: bidderRequest.refererInfo.referer, bids: [], diff --git a/modules/adgenerationBidAdapter.js b/modules/adgenerationBidAdapter.js index 4dd320d3f24..e0d3a881cad 100644 --- a/modules/adgenerationBidAdapter.js +++ b/modules/adgenerationBidAdapter.js @@ -25,7 +25,7 @@ export const spec = { * @return ServerRequest Info describing the request to the server. */ buildRequests: function (validBidRequests, bidderRequest) { - const ADGENE_PREBID_VERSION = '1.2.0'; + const ADGENE_PREBID_VERSION = '1.3.0'; let serverRequests = []; for (let i = 0, len = validBidRequests.length; i < len; i++) { const validReq = validBidRequests[i]; @@ -50,6 +50,12 @@ export const spec = { data = tryAppendQueryString(data, 'imark', '1'); } data = tryAppendQueryString(data, 'tp', bidderRequest.refererInfo.referer); + if (isIos()) { + const hyperId = getHyperId(validReq); + if (hyperId != null) { + data = tryAppendQueryString(data, 'hyper_id', hyperId); + } + } // remove the trailing "&" if (data.lastIndexOf('&') === data.length - 1) { data = data.substring(0, data.length - 1); @@ -263,4 +269,20 @@ function getCurrencyType() { return 'JPY'; } +/** + * + * @param validReq request + * @return {null|string} + */ +function getHyperId(validReq) { + if (validReq.userId && validReq.userId.novatiq && validReq.userId.novatiq.snowflake.syncResponse === 1) { + return validReq.userId.novatiq.snowflake.id; + } + return null; +} + +function isIos() { + return (/(ios|ipod|ipad|iphone)/i).test(window.navigator.userAgent); +} + registerBidder(spec); diff --git a/modules/adhashBidAdapter.js b/modules/adhashBidAdapter.js index c94a4e35efd..7f5af047993 100644 --- a/modules/adhashBidAdapter.js +++ b/modules/adhashBidAdapter.js @@ -1,12 +1,85 @@ -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import includes from 'core-js-pure/features/array/includes.js'; -import { BANNER } from '../src/mediaTypes.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {includes} from '../src/polyfill.js'; +import {BANNER} from '../src/mediaTypes.js'; const VERSION = '1.0'; +const BAD_WORD_STEP = 0.1; +const BAD_WORD_MIN = 0.2; + +/** + * Function that checks the page where the ads are being served for brand safety. + * If unsafe words are found the scoring of that page increases. + * If it becomes greater than the maximum allowed score false is returned. + * The rules may vary based on the website language or the publisher. + * The AdHash bidder will not bid on unsafe pages (according to 4A's). + * @param badWords list of scoring rules to chech against + * @param maxScore maximum allowed score for that bidding + * @returns boolean flag is the page safe + */ +function brandSafety(badWords, maxScore) { + /** + * Performs the ROT13 encoding on the string argument and returns the resulting string. + * The Adhash bidder uses ROT13 so that the response is not blocked by: + * - ad blocking software + * - parental control software + * - corporate firewalls + * due to the bad words contained in the response. + * @param value The input string. + * @returns string Returns the ROT13 version of the given string. + */ + const rot13 = value => { + const input = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + const output = 'NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm'; + const index = x => input.indexOf(x); + const translate = x => index(x) > -1 ? output[index(x)] : x; + return value.split('').map(translate).join(''); + }; + + /** + * Calculates the scoring for each bad word with dimishing returns + * @param {integer} points points that this word costs + * @param {integer} occurances number of occurances + * @returns {float} final score + */ + const scoreCalculator = (points, occurances) => { + let positive = true; + if (points < 0) { + points *= -1; + positive = false; + } + let result = 0; + for (let i = 0; i < occurances; i++) { + result += Math.max(points - i * BAD_WORD_STEP, BAD_WORD_MIN); + } + return positive ? result : -result; + }; + + // Default parameters if the bidder is unable to send some of them + badWords = badWords || []; + maxScore = parseInt(maxScore) || 10; + + try { + let score = 0; + const content = window.top.document.body.innerText.toLowerCase(); + const words = content.trim().split(/\s+/).length; + for (const [word, rule, points] of badWords) { + if (rule === 'full' && new RegExp('\\b' + rot13(word) + '\\b', 'i').test(content)) { + const occurances = content.match(new RegExp('\\b' + rot13(word) + '\\b', 'g')).length; + score += scoreCalculator(points, occurances); + } else if (rule === 'partial' && content.indexOf(rot13(word.toLowerCase())) > -1) { + const occurances = content.match(new RegExp(rot13(word), 'g')).length; + score += scoreCalculator(points, occurances); + } + } + return score < maxScore * words / 500; + } catch (e) { + return true; + } +} export const spec = { code: 'adhash', - url: 'https://bidder.adhash.org/rtb?version=' + VERSION + '&prebid=true', + url: 'https://bidder.adhash.com/rtb?version=' + VERSION + '&prebid=true', supportedMediaTypes: [ BANNER ], isBidRequestValid: (bid) => { @@ -37,7 +110,7 @@ export const spec = { var size = validBidRequests[i].sizes[index].join('x'); bidRequests.push({ method: 'POST', - url: url, + url: url + '&publisher=' + validBidRequests[i].params.publisherId, bidRequest: validBidRequests[i], data: { timezone: new Date().getTimezoneOffset() / 60, @@ -59,7 +132,8 @@ export const spec = { blockedCreatives: [], currentTimestamp: new Date().getTime(), recentAds: [], - GDPR: gdprConsent + GDPRApplies: gdprConsent ? gdprConsent.gdprApplies : null, + GDPR: gdprConsent ? gdprConsent.consentString : null }, options: { withCredentials: false, @@ -73,7 +147,11 @@ export const spec = { interpretResponse: (serverResponse, request) => { const responseBody = serverResponse ? serverResponse.body : {}; - if (!responseBody.creatives || responseBody.creatives.length === 0) { + if ( + !responseBody.creatives || + responseBody.creatives.length === 0 || + !brandSafety(responseBody.badWords, responseBody.maxScore) + ) { return []; } @@ -87,7 +165,7 @@ export const spec = { cpm: responseBody.creatives[0].costEUR, ad: `
- + `, width: request.bidRequest.sizes[0][0], height: request.bidRequest.sizes[0][1], diff --git a/modules/adkernelAdnAnalyticsAdapter.js b/modules/adkernelAdnAnalyticsAdapter.js index 2b4e67736f3..de5d59ca6f8 100644 --- a/modules/adkernelAdnAnalyticsAdapter.js +++ b/modules/adkernelAdnAnalyticsAdapter.js @@ -10,7 +10,7 @@ const GVLID = 14; const ANALYTICS_VERSION = '1.0.2'; const DEFAULT_QUEUE_TIMEOUT = 4000; const DEFAULT_HOST = 'tag.adkernel.com'; -const storageObj = getStorageManager(GVLID); +const storageObj = getStorageManager({gvlid: GVLID}); const ADK_HB_EVENTS = { AUCTION_INIT: 'auctionInit', diff --git a/modules/adkernelBidAdapter.js b/modules/adkernelBidAdapter.js index 2a54c45aa40..c2d6ca4d4dd 100644 --- a/modules/adkernelBidAdapter.js +++ b/modules/adkernelBidAdapter.js @@ -1,11 +1,26 @@ import { - isStr, isArray, isPlainObject, deepSetValue, isNumber, deepAccess, getAdUnitSizes, parseGPTSingleSizeArrayToRtbSize, - cleanObj, contains, getDNT, parseUrl, createTrackPixelHtml, _each, isArrayOfNums + _each, + cleanObj, + contains, + createTrackPixelHtml, + deepAccess, + deepSetValue, + getAdUnitSizes, + getDNT, + inIframe, + isArray, + isArrayOfNums, + isEmpty, + isNumber, + isPlainObject, + isStr, + mergeDeep, + parseGPTSingleSizeArrayToRtbSize, + parseUrl } from '../src/utils.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import find from 'core-js-pure/features/array/find.js'; -import includes from 'core-js-pure/features/array/includes.js'; +import {find, includes} from '../src/polyfill.js'; import {config} from '../src/config.js'; /* @@ -77,7 +92,9 @@ export const spec = { {code: 'unibots'}, {code: 'catapultx'}, {code: 'ergadx'}, - {code: 'turktelekom'} + {code: 'turktelekom'}, + {code: 'felixads'}, + {code: 'motionspots'} ], supportedMediaTypes: [BANNER, VIDEO, NATIVE], @@ -103,17 +120,16 @@ export const spec = { * @returns {ServerRequest[]} */ buildRequests: function (bidRequests, bidderRequest) { - let impDispatch = dispatchImps(bidRequests, bidderRequest.refererInfo); + let impGroups = groupImpressionsByHostZone(bidRequests, bidderRequest.refererInfo); let requests = []; let schain = bidRequests[0].schain; - Object.keys(impDispatch).forEach(host => { - Object.keys(impDispatch[host]).forEach(zoneId => { - const request = buildRtbRequest(impDispatch[host][zoneId], bidderRequest, schain); - requests.push({ - method: 'POST', - url: `https://${host}/hb?zone=${zoneId}&v=${VERSION}`, - data: JSON.stringify(request) - }); + _each(impGroups, impGroup => { + let {host, zoneId, imps} = impGroup; + const request = buildRtbRequest(imps, bidderRequest, schain); + requests.push({ + method: 'POST', + url: `https://${host}/hb?zone=${zoneId}&v=${VERSION}`, + data: JSON.stringify(request) }); }); return requests; @@ -209,17 +225,19 @@ registerBidder(spec); * @param bidRequests {BidRequest[]} * @param refererInfo {refererInfo} */ -function dispatchImps(bidRequests, refererInfo) { +function groupImpressionsByHostZone(bidRequests, refererInfo) { let secure = (refererInfo && refererInfo.referer.indexOf('https:') === 0); - return bidRequests.map(bidRequest => buildImp(bidRequest, secure)) - .reduce((acc, curr, index) => { - let bidRequest = bidRequests[index]; - let {zoneId, host} = bidRequest.params; - acc[host] = acc[host] || {}; - acc[host][zoneId] = acc[host][zoneId] || []; - acc[host][zoneId].push(curr); - return acc; - }, {}); + return Object.values( + bidRequests.map(bidRequest => buildImp(bidRequest, secure)) + .reduce((acc, curr, index) => { + let bidRequest = bidRequests[index]; + let {zoneId, host} = bidRequest.params; + let key = `${host}_${zoneId}`; + acc[key] = acc[key] || {host: host, zoneId: zoneId, imps: []}; + acc[key].imps.push(curr); + return acc; + }, {}) + ); } function getBidFloor(bid, mediaType, sizes) { @@ -365,57 +383,142 @@ function getAllowedSyncMethod(bidderCode) { } /** - * Builds complete rtb request - * @param imps {Object} Collection of rtb impressions - * @param bidderRequest {BidderRequest} - * @param schain {Object=} Supply chain config - * @return {Object} Complete rtb request + * Create device object from fpd and host-collected data + * @param fpd {Object} + * @returns {{device: Object}} */ -function buildRtbRequest(imps, bidderRequest, schain) { - let {bidderCode, gdprConsent, auctionId, refererInfo, timeout, uspConsent} = bidderRequest; - let coppa = config.getConfig('coppa'); - let req = { - 'id': auctionId, - 'imp': imps, - 'site': createSite(refererInfo), - 'at': 1, - 'device': { - 'ip': 'caller', - 'ipv6': 'caller', - 'ua': 'caller', - 'js': 1, - 'language': getLanguage() - }, - 'tmax': parseInt(timeout) - }; +function makeDevice(fpd) { + let device = mergeDeep({ + 'ip': 'caller', + 'ipv6': 'caller', + 'ua': 'caller', + 'js': 1, + 'language': getLanguage() + }, fpd.device || {}); if (getDNT()) { - req.device.dnt = 1; + device.dnt = 1; + } + return {device: device}; +} + +/** + * Create site or app description object + * @param bidderRequest {BidderRequest} + * @param fpd {Object} + * @returns {{site: Object}|{app: Object}} + */ +function makeSiteOrApp(bidderRequest, fpd) { + let {refererInfo} = bidderRequest; + let appConfig = config.getConfig('app'); + if (isEmpty(appConfig)) { + return {site: createSite(refererInfo, fpd)} + } else { + return {app: appConfig}; } +} + +/** + * Create user description object + * @param bidderRequest {BidderRequest} + * @param fpd {Object} + * @returns {{user: Object} | undefined} + */ +function makeUser(bidderRequest, fpd) { + let {gdprConsent} = bidderRequest; + let user = fpd.user || {}; + if (gdprConsent && gdprConsent.consentString !== undefined) { + deepSetValue(user, 'ext.consent', gdprConsent.consentString); + } + let eids = getExtendedUserIds(bidderRequest); + if (eids) { + deepSetValue(user, 'ext.eids', eids); + } + if (!isEmpty(user)) { return {user: user}; } +} + +/** + * Create privacy regulations object + * @param bidderRequest {BidderRequest} + * @returns {{regs: Object} | undefined} + */ +function makeRegulations(bidderRequest) { + let {gdprConsent, uspConsent} = bidderRequest; + let regs = {}; if (gdprConsent) { if (gdprConsent.gdprApplies !== undefined) { - deepSetValue(req, 'regs.ext.gdpr', ~~gdprConsent.gdprApplies); - } - if (gdprConsent.consentString !== undefined) { - deepSetValue(req, 'user.ext.consent', gdprConsent.consentString); + deepSetValue(regs, 'regs.ext.gdpr', ~~gdprConsent.gdprApplies); } } if (uspConsent) { - deepSetValue(req, 'regs.ext.us_privacy', uspConsent); + deepSetValue(regs, 'regs.ext.us_privacy', uspConsent); + } + if (config.getConfig('coppa')) { + deepSetValue(regs, 'regs.coppa', 1); } - if (coppa) { - deepSetValue(req, 'regs.coppa', 1); + if (!isEmpty(regs)) { + return regs; } +} + +/** + * Create top-level request object + * @param bidderRequest {BidderRequest} + * @param imps {Object} Impressions + * @param fpd {Object} First party data + * @returns + */ +function makeBaseRequest(bidderRequest, imps, fpd) { + let {auctionId, timeout} = bidderRequest; + let request = { + 'id': auctionId, + 'imp': imps, + 'at': 1, + 'tmax': parseInt(timeout) + }; + if (!isEmpty(fpd.bcat)) { + request.bcat = fpd.bcat; + } + if (!isEmpty(fpd.badv)) { + request.badv = fpd.badv; + } + return request; +} + +/** + * Initialize sync capabilities + * @param bidderRequest {BidderRequest} + */ +function makeSyncInfo(bidderRequest) { + let {bidderCode} = bidderRequest; let syncMethod = getAllowedSyncMethod(bidderCode); if (syncMethod) { - deepSetValue(req, 'ext.adk_usersync', syncMethod); + let res = {}; + deepSetValue(res, 'ext.adk_usersync', syncMethod); + return res; } +} + +/** + * Builds complete rtb request + * @param imps {Object} Collection of rtb impressions + * @param bidderRequest {BidderRequest} + * @param schain {Object=} Supply chain config + * @return {Object} Complete rtb request + */ +function buildRtbRequest(imps, bidderRequest, schain) { + let fpd = config.getConfig('ortb2') || {}; + + let req = mergeDeep( + makeBaseRequest(bidderRequest, imps, fpd), + makeDevice(fpd), + makeSiteOrApp(bidderRequest, fpd), + makeUser(bidderRequest, fpd), + makeRegulations(bidderRequest), + makeSyncInfo(bidderRequest) + ); if (schain) { deepSetValue(req, 'source.ext.schain', schain); } - let eids = getExtendedUserIds(bidderRequest); - if (eids) { - deepSetValue(req, 'user.ext.eids', eids); - } return req; } @@ -431,18 +534,17 @@ function getLanguage() { /** * Creates site description object */ -function createSite(refInfo) { +function createSite(refInfo, fpd) { let url = parseUrl(refInfo.referer); let site = { 'domain': url.hostname, 'page': `${url.protocol}://${url.hostname}${url.pathname}` }; - if (self === top && document.referrer) { + mergeDeep(site, fpd.site); + if (!inIframe() && document.referrer) { site.ref = document.referrer; - } - let keywords = document.getElementsByTagName('meta')['keywords']; - if (keywords && keywords.content) { - site.keywords = keywords.content; + } else { + delete site.ref; } return site; } diff --git a/modules/adlooxAdServerVideo.js b/modules/adlooxAdServerVideo.js index 7305283039c..bd715cb34f3 100644 --- a/modules/adlooxAdServerVideo.js +++ b/modules/adlooxAdServerVideo.js @@ -9,7 +9,7 @@ import { registerVideoSupport } from '../src/adServerManager.js'; import { command as analyticsCommand, COMMAND } from './adlooxAnalyticsAdapter.js'; import { ajax } from '../src/ajax.js'; -import { EVENTS } from '../src/constants.json'; +import CONSTANTS from '../src/constants.json'; import { targeting } from '../src/targeting.js'; import { logInfo, isFn, logError, isPlainObject, isStr, isBoolean, deepSetValue, deepClone, timestamp, logWarn } from '../src/utils.js'; @@ -74,7 +74,7 @@ function track(options, callback) { bid.ext.adloox.video.adserver = false; analyticsCommand(COMMAND.TRACK, { - eventType: EVENTS.BID_WON, + eventType: CONSTANTS.EVENTS.BID_WON, args: bid }); } diff --git a/modules/adlooxAnalyticsAdapter.js b/modules/adlooxAnalyticsAdapter.js index 6ea1df1b72c..1091b87a22d 100644 --- a/modules/adlooxAnalyticsAdapter.js +++ b/modules/adlooxAnalyticsAdapter.js @@ -6,14 +6,27 @@ import adapterManager from '../src/adapterManager.js'; import adapter from '../src/AnalyticsAdapter.js'; -import { loadExternalScript } from '../src/adloader.js'; -import { auctionManager } from '../src/auctionManager.js'; -import { AUCTION_COMPLETED } from '../src/auction.js'; -import { EVENTS } from '../src/constants.json'; -import find from 'core-js-pure/features/array/find.js'; +import {loadExternalScript} from '../src/adloader.js'; +import {auctionManager} from '../src/auctionManager.js'; +import {AUCTION_COMPLETED} from '../src/auction.js'; +import CONSTANTS from '../src/constants.json'; +import {find} from '../src/polyfill.js'; +import {getRefererInfo} from '../src/refererDetection.js'; import { - deepAccess, logInfo, isPlainObject, logError, isStr, isNumber, getGptSlotInfoForAdUnitCode, - isFn, mergeDeep, logMessage, insertElement, logWarn, getUniqueIdentifierStr, parseUrl + deepAccess, + getGptSlotInfoForAdUnitCode, + getUniqueIdentifierStr, + insertElement, + isFn, + isNumber, + isPlainObject, + isStr, + logError, + logInfo, + logMessage, + logWarn, + mergeDeep, + parseUrl } from '../src/utils.js'; const MODULE = 'adlooxAnalyticsAdapter'; @@ -46,6 +59,10 @@ MACRO['targetelt'] = function(b, c) { MACRO['creatype'] = function(b, c) { return b.mediaType == 'video' ? ADLOOX_MEDIATYPE.VIDEO : ADLOOX_MEDIATYPE.DISPLAY; }; +MACRO['pageurl'] = function(b, c) { + const refererInfo = getRefererInfo(); + return (refererInfo.canonicalUrl || refererInfo.referer || '').substr(0, 300).split(/[?#]/)[0]; +}; MACRO['pbadslot'] = function(b, c) { const adUnit = find(auctionManager.getAdUnits(), a => b.adUnitCode === a.code); return deepAccess(adUnit, 'ortb2Imp.ext.data.pbadslot') || getGptSlotInfoForAdUnitCode(b.adUnitCode).gptSlot || b.adUnitCode; @@ -199,9 +216,9 @@ analyticsAdapter.url = function(url, args, bid) { return url + a2qs(args); } -analyticsAdapter[`handle_${EVENTS.AUCTION_END}`] = function(auctionDetails) { +analyticsAdapter[`handle_${CONSTANTS.EVENTS.AUCTION_END}`] = function(auctionDetails) { if (!(auctionDetails.auctionStatus == AUCTION_COMPLETED && auctionDetails.bidsReceived.length > 0)) return; - analyticsAdapter[`handle_${EVENTS.AUCTION_END}`] = NOOP; + analyticsAdapter[`handle_${CONSTANTS.EVENTS.AUCTION_END}`] = NOOP; logMessage(MODULE, 'preloading verification JS'); @@ -214,7 +231,7 @@ analyticsAdapter[`handle_${EVENTS.AUCTION_END}`] = function(auctionDetails) { insertElement(link); } -analyticsAdapter[`handle_${EVENTS.BID_WON}`] = function(bid) { +analyticsAdapter[`handle_${CONSTANTS.EVENTS.BID_WON}`] = function(bid) { if (deepAccess(bid, 'ext.adloox.video.adserver')) { logMessage(MODULE, `measuring '${bid.mediaType}' ad unit code '${bid.adUnitCode}' via Ad Server module`); return; diff --git a/modules/adlooxAnalyticsAdapter.md b/modules/adlooxAnalyticsAdapter.md index c4618a2e3aa..e21261d0b8d 100644 --- a/modules/adlooxAnalyticsAdapter.md +++ b/modules/adlooxAnalyticsAdapter.md @@ -127,6 +127,7 @@ The following macros are available * `%%pbadslot%%`: [Prebid Ad Slot](https://docs.prebid.org/features/pbAdSlot.html) returns [`AdUnit.code`](https://docs.prebid.org/features/pbAdSlot.html) if set otherwise returns [`AdUnit.code`](https://docs.prebid.org/dev-docs/adunit-reference.html#adunit) * it is recommended you read the [Prebid Ad Slot section in the Adloox RTD Provider documentation](./adlooxRtdProvider.md#prebid-ad-slot) + * `%%pageurl%%`: [`canonicalUrl`](https://docs.prebid.org/dev-docs/publisher-api-reference/setConfig.html#setConfig-Page-URL) from the [`refererInfo` object](https://docs.prebid.org/dev-docs/bidder-adaptor.html#referrers) otherwise uses `referer` ### Functions diff --git a/modules/adlooxRtdProvider.js b/modules/adlooxRtdProvider.js index 34d1428ea1d..bb8334ec8fe 100644 --- a/modules/adlooxRtdProvider.js +++ b/modules/adlooxRtdProvider.js @@ -11,16 +11,29 @@ /* eslint standard/no-callback-literal: "off" */ /* eslint prebid/validate-imports: "off" */ -import { command as analyticsCommand, COMMAND } from './adlooxAnalyticsAdapter.js'; -import { config as _config } from '../src/config.js'; -import { submodule } from '../src/hook.js'; -import { ajax } from '../src/ajax.js'; -import { getGlobal } from '../src/prebidGlobal.js'; +import {command as analyticsCommand, COMMAND} from './adlooxAnalyticsAdapter.js'; +import {config as _config} from '../src/config.js'; +import {submodule} from '../src/hook.js'; +import {ajax} from '../src/ajax.js'; +import {getGlobal} from '../src/prebidGlobal.js'; +import {getRefererInfo} from '../src/refererDetection.js'; import { - getAdUnitSizes, logInfo, isPlainObject, logError, isStr, isInteger, isArray, isBoolean, mergeDeep, deepAccess, - _each, deepSetValue, logWarn, getGptSlotInfoForAdUnitCode + _each, + deepAccess, + deepSetValue, + getAdUnitSizes, + getGptSlotInfoForAdUnitCode, + isArray, + isBoolean, + isInteger, + isPlainObject, + isStr, + logError, + logInfo, + logWarn, + mergeDeep } from '../src/utils.js'; -import includes from 'core-js-pure/features/array/includes.js'; +import {includes} from '../src/polyfill.js'; const MODULE_NAME = 'adloox'; const MODULE = `${MODULE_NAME}RtdProvider`; @@ -285,6 +298,7 @@ function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { } } + const refererInfo = getRefererInfo(); const args = [ [ 'v', `pbjs-${getGlobal().version}` ], [ 'c', config.params.clientid ], @@ -293,7 +307,7 @@ function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { [ 'imp', config.params.imps ], [ 'fc_ip', config.params.freqcap_ip ], [ 'fc_ipua', config.params.freqcap_ipua ], - [ 'pn', document.location.pathname ] + [ 'pn', (refererInfo.canonicalUrl || refererInfo.referer || '').substr(0, 300).split(/[?#]/)[0] ] ]; if (!adUnits.length) { diff --git a/modules/admanBidAdapter.js b/modules/admanBidAdapter.js index e02a1a9df04..666e9aea309 100644 --- a/modules/admanBidAdapter.js +++ b/modules/admanBidAdapter.js @@ -1,10 +1,11 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import { isFn, deepAccess, logMessage } from '../src/utils.js'; +import {config} from '../src/config.js'; const BIDDER_CODE = 'adman'; const AD_URL = 'https://pub.admanmedia.com/?c=o&m=multi'; -const URL_SYNC = 'https://pub.admanmedia.com/?c=o&m=sync'; +const URL_SYNC = 'https://pub.admanmedia.com'; function isBidResponseValid(bid) { if (!bid.requestId || !bid.cpm || !bid.creativeId || @@ -108,6 +109,7 @@ export const spec = { } if (bid.userId) { getUserId(placement.eids, bid.userId.uid2 && bid.userId.uid2.id, 'uidapi.com'); + getUserId(placement.eids, bid.userId.lotamePanoramaId, 'lotame.com'); } if (traff === VIDEO) { placement.playerSize = bid.mediaTypes[VIDEO].playerSize; @@ -151,19 +153,24 @@ export const spec = { }, getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { - let syncUrl = URL_SYNC + let syncType = syncOptions.iframeEnabled ? 'iframe' : 'image'; + let syncUrl = URL_SYNC + `/${syncType}?pbjs=1`; if (gdprConsent && gdprConsent.consentString) { if (typeof gdprConsent.gdprApplies === 'boolean') { syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; } else { - syncUrl += `&gdpr==0&gdpr_consent=${gdprConsent.consentString}`; + syncUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; } } if (uspConsent && uspConsent.consentString) { syncUrl += `&ccpa_consent=${uspConsent.consentString}`; } + + const coppa = config.getConfig('coppa') ? 1 : 0; + syncUrl += `&coppa=${coppa}`; + return [{ - type: 'image', + type: syncType, url: syncUrl }]; } diff --git a/modules/admaruBidAdapter.js b/modules/admaruBidAdapter.js new file mode 100644 index 00000000000..65f62c77e26 --- /dev/null +++ b/modules/admaruBidAdapter.js @@ -0,0 +1,81 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER} from '../src/mediaTypes.js'; + +const ADMARU_ENDPOINT = 'https://p1.admaru.net/AdCall'; +const BIDDER_CODE = 'admaru'; + +const DEFAULT_BID_TTL = 360; + +function parseBid(rawBid, currency) { + const bid = {}; + + bid.cpm = rawBid.price; + bid.impid = rawBid.impid; + bid.requestId = rawBid.impid; + bid.netRevenue = true; + bid.dealId = ''; + bid.creativeId = rawBid.crid; + bid.currency = currency; + bid.ad = rawBid.adm; + bid.width = rawBid.w; + bid.height = rawBid.h; + bid.mediaType = BANNER; + bid.ttl = DEFAULT_BID_TTL; + + return bid; +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + + isBidRequestValid: function (bid) { + return !!(bid && bid.params && bid.params.pub_id && bid.params.adspace_id); + }, + + buildRequests: function (validBidRequests, bidderRequest) { + return validBidRequests.map(bid => { + const payload = { + pub_id: bid.params.pub_id, + adspace_id: bid.params.adspace_id, + bidderRequestId: bid.bidderRequestId, + bidId: bid.bidId + }; + + return { + method: 'GET', + url: ADMARU_ENDPOINT, + data: payload, + } + }) + }, + + interpretResponse: function (serverResponse, bidRequest) { + const bidResponses = []; + let bid = null; + + if (!serverResponse.hasOwnProperty('body') || !serverResponse.body.hasOwnProperty('seatbid')) { + return bidResponses; + } + + const serverBody = serverResponse.body; + const seatbid = serverBody.seatbid; + + for (let i = 0; i < seatbid.length; i++) { + if (!seatbid[i].hasOwnProperty('bid')) { + continue; + } + + const innerBids = seatbid[i].bid; + for (let j = 0; j < innerBids.length; j++) { + bid = parseBid(innerBids[j], serverBody.cur); + + bidResponses.push(bid); + } + } + + return bidResponses; + } +} + +registerBidder(spec); diff --git a/modules/admaruBidAdapter.md b/modules/admaruBidAdapter.md new file mode 100644 index 00000000000..9985a660ac6 --- /dev/null +++ b/modules/admaruBidAdapter.md @@ -0,0 +1,34 @@ +# Overview + +``` +Module Name: Admaru Bidder Adapter +Module Type: Bidder Adapter +Maintainer: support@admaru.com +``` + +# Description + +Module that connects to Admaru demand sources + +# Test Parameters +``` + var adUnits = [ + { + code: 'test-div', + mediaTypes: { + banner: { + sizes: [[300, 250]], // a display size + } + }, + bids: [ + { + bidder: "admaru", + params: { + pub_id: '1234', // string - required + adspace_id: '1234' // string - required + } + } + ] + } + ]; +``` diff --git a/modules/admixerBidAdapter.js b/modules/admixerBidAdapter.js index bb91ddcdfc8..dfb76a03804 100644 --- a/modules/admixerBidAdapter.js +++ b/modules/admixerBidAdapter.js @@ -1,14 +1,15 @@ import { logError } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; +import {BANNER, VIDEO, NATIVE} from '../src/mediaTypes.js'; const BIDDER_CODE = 'admixer'; -const ALIASES = ['go2net', 'adblender', 'adsyield']; +const ALIASES = ['go2net', 'adblender', 'adsyield', 'futureads']; const ENDPOINT_URL = 'https://inv-nets.admixer.net/prebid.1.2.aspx'; export const spec = { code: BIDDER_CODE, aliases: ALIASES, - supportedMediaTypes: ['banner', 'video'], + supportedMediaTypes: [BANNER, VIDEO, NATIVE], /** * Determines whether or not the given bid request is valid. */ diff --git a/modules/adnowBidAdapter.js b/modules/adnowBidAdapter.js index badf57ed5c9..472d0fdb2e1 100644 --- a/modules/adnowBidAdapter.js +++ b/modules/adnowBidAdapter.js @@ -1,7 +1,7 @@ -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { NATIVE, BANNER } from '../src/mediaTypes.js'; -import { parseSizesInput, deepAccess, parseQueryStringParameters } from '../src/utils.js'; -import includes from 'core-js-pure/features/array/includes.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, NATIVE} from '../src/mediaTypes.js'; +import {deepAccess, parseQueryStringParameters, parseSizesInput} from '../src/utils.js'; +import {includes} from '../src/polyfill.js'; const BIDDER_CODE = 'adnow'; const ENDPOINT = 'https://n.ads3-adnow.com/a'; diff --git a/modules/adnuntiusBidAdapter.js b/modules/adnuntiusBidAdapter.js index e013ed553ef..9e05ea664d8 100644 --- a/modules/adnuntiusBidAdapter.js +++ b/modules/adnuntiusBidAdapter.js @@ -27,7 +27,7 @@ const getSegmentsFromOrtb = function (ortb2) { } const handleMeta = function () { - const storage = getStorageManager(GVLID, 'adnuntius') + const storage = getStorageManager({gvlid: GVLID, bidderCode: BIDDER_CODE}) let adnMeta = null if (storage.localStorageIsEnabled()) { adnMeta = JSON.parse(storage.getDataFromLocalStorage('adn.metaData')) @@ -56,6 +56,8 @@ export const spec = { const requests = []; const request = []; const ortb2 = config.getConfig('ortb2'); + const bidderConfig = config.getConfig(); + const adnMeta = handleMeta() const usi = getUsi(adnMeta, ortb2, bidderRequest) const segments = getSegmentsFromOrtb(ortb2); @@ -68,7 +70,7 @@ export const spec = { if (gdprApplies !== undefined) request.push('consentString=' + consentString); if (segments.length > 0) request.push('segments=' + segments.join(',')); if (usi) request.push('userId=' + usi); - + if (bidderConfig.useCookie === false) request.push('noCookies=true') for (var i = 0; i < validBidRequests.length; i++) { const bid = validBidRequests[i] const network = bid.params.network || 'network'; diff --git a/modules/adnuntiusRtdProvider.js b/modules/adnuntiusRtdProvider.js new file mode 100644 index 00000000000..9234a30aa33 --- /dev/null +++ b/modules/adnuntiusRtdProvider.js @@ -0,0 +1,96 @@ + +import { submodule } from '../src/hook.js' +import { logError, logInfo } from '../src/utils.js' +import { ajax } from '../src/ajax.js'; + +import { config as sourceConfig } from '../src/config.js'; + +const GVLID = 855; + +function init(config, userConsent) { + if (!config.params || !config.params.providers) return false + logInfo(userConsent) + return true; +} + +// Make sure that ajax has a function as callback +function prepProvider(provider) { + // Map parameter to something that adnuntius endpoint understands. + const mappedParameters = { + siteId: 's', + userId: 'browserId', + browserId: 'browserId', + folderId: 'folderId' + } + + const tzo = new Date().getTimezoneOffset(); + const URL = ['https://data.adnuntius.com/usr?tzo=' + tzo] + Object.keys(provider).forEach(key => { + URL.push(`${mappedParameters[key]}=${provider[key]}`) + }) + + return new Promise((resolve, reject) => { + ajax(URL.join('&'), { + success: function (res) { + const response = JSON.parse(res) + resolve(response) + }, + error: function (err) { reject(err) } + }); + }); +} + +function setGlobalConfig(config, segments) { + const ortbSegments = { + ortb2: { + user: { + data: [{ + name: 'adnuntius', + segment: segments + }] + } + } + } + if (config.params && config.params.bidders) { + sourceConfig.mergeBidderConfig({ + bidders: config.params.bidders, + config: ortbSegments + }) + } else { + sourceConfig.mergeConfig(ortbSegments) + } +} + +function alterBidRequests(reqBidsConfigObj, callback, config, userConsent) { + const gdpr = userConsent && userConsent.gdpr; + let allowedToRun = true + if (gdpr) { + if (userConsent.gdpr.gdprApplies) { + if (gdpr.gdprApplies && !gdpr.vendorData.vendorConsents[GVLID]) allowedToRun = false; + } + } + if (allowedToRun) { + const providerRequests = config.params.providers.map(provider => prepProvider(provider)) + + Promise.allSettled(providerRequests).then((values) => { + const segments = values.reduce((segments, array) => (array.status === 'fulfilled') ? segments.concat(array.value.segments) : [], []).map(segmentId => ({ id: segmentId })) + setGlobalConfig(config, segments) + callback(); + }) + .catch(err => logError('ADN: err', err)); + } else callback(); +} + +/** @type {RtdSubmodule} */ +export const adnuntiusSubmodule = { + name: 'adnuntius', + init: init, + getBidRequestData: alterBidRequests, + setGlobalConfig: setGlobalConfig, +}; + +export function beforeInit() { + submodule('realTimeData', adnuntiusSubmodule); +} + +beforeInit(); diff --git a/modules/adnuntiusRtdProvider.md b/modules/adnuntiusRtdProvider.md new file mode 100644 index 00000000000..e62eba13e2c --- /dev/null +++ b/modules/adnuntiusRtdProvider.md @@ -0,0 +1,41 @@ +### Overview + +The Adnuntius Real Time Data Provider will request segments from adnuntius data, based on what is defined in the realTimeData object. It uses the siteId and userId that a publisher provides. These will have to correspond to a previously uploaded user to Adnuntius Data. + +### Integration + +1. Build the adnuntiusRTD module into the Prebid.js package with: + +``` +gulp build --modules=adnuntiusRtdProvider,... +``` + +2. Use `setConfig` to instruct Prebid.js to initilaize the adnuntiusRtdProvider module, as specified below. + +### Configuration + +``` +var pbjs = pbjs || { que: [] } +pbjs.que.push(function () { + pbjs.setConfig({ + realTimeData: { + auctionDelay: 300, + dataProviders: [ + { + name: 'adnuntius', + waitForIt: true, + params: { + bidders: ['adnuntius'], + providers: [{ + siteId: 'site123', + userId: 'user123' + }] + } + } + ] + }, + }); +}); +``` + +Please reach out to Adnuntius if you need more info about this: prebid@adnuntius.com diff --git a/modules/adomikAnalyticsAdapter.js b/modules/adomikAnalyticsAdapter.js index 99f079b2574..44c0c6868cf 100644 --- a/modules/adomikAnalyticsAdapter.js +++ b/modules/adomikAnalyticsAdapter.js @@ -1,9 +1,8 @@ import adapter from '../src/AnalyticsAdapter.js'; import CONSTANTS from '../src/constants.json'; import adapterManager from '../src/adapterManager.js'; -import { logInfo } from '../src/utils.js'; -import find from 'core-js-pure/features/array/find.js'; -import findIndex from 'core-js-pure/features/array/find-index.js'; +import {logInfo} from '../src/utils.js'; +import {find, findIndex} from '../src/polyfill.js'; // Events used in adomik analytics adapter const auctionInit = CONSTANTS.EVENTS.AUCTION_INIT; @@ -14,6 +13,8 @@ const bidWon = CONSTANTS.EVENTS.BID_WON; const bidTimeout = CONSTANTS.EVENTS.BID_TIMEOUT; const ua = navigator.userAgent; +var _sampled = true; + let adomikAdapter = Object.assign(adapter({}), { // Track every event needed @@ -76,9 +77,12 @@ adomikAdapter.sendTypedEvent = function() { const groupedTypedEvents = adomikAdapter.buildTypedEvents(); const bulkEvents = { + testId: adomikAdapter.currentContext.testId, + testValue: adomikAdapter.currentContext.testValue, uid: adomikAdapter.currentContext.uid, ahbaid: adomikAdapter.currentContext.id, hostname: window.location.hostname, + sampling: adomikAdapter.currentContext.sampling, eventsByPlacementCode: groupedTypedEvents.map(function(typedEventsByType) { let sizes = []; const eventKeys = ['request', 'response', 'winner']; @@ -126,6 +130,8 @@ adomikAdapter.sendTypedEvent = function() { }; adomikAdapter.sendWonEvent = function (wonEvent) { + let keyValues = { testId: adomikAdapter.currentContext.testId, testValue: adomikAdapter.currentContext.testValue } + wonEvent = {...wonEvent, ...keyValues} const stringWonEvent = JSON.stringify(wonEvent) logInfo('Won event sent to adomik prebid analytic ' + stringWonEvent); @@ -199,17 +205,28 @@ adomikAdapter.adapterEnableAnalytics = adomikAdapter.enableAnalytics; adomikAdapter.enableAnalytics = function (config) { adomikAdapter.currentContext = {}; - const initOptions = config.options; - if (initOptions) { - adomikAdapter.currentContext = { - uid: initOptions.id, - url: initOptions.url, - id: '', - timeouted: false, + + _sampled = typeof config === 'undefined' || + typeof config.sampling === 'undefined' || + Math.random() < parseFloat(config.sampling); + + if (_sampled) { + if (initOptions) { + adomikAdapter.currentContext = { + uid: initOptions.id, + url: initOptions.url, + testId: initOptions.testId, + testValue: initOptions.testValue, + id: '', + timeouted: false, + sampling: config.sampling + } + logInfo('Adomik Analytics enabled with config', initOptions); + adomikAdapter.adapterEnableAnalytics(config); } - logInfo('Adomik Analytics enabled with config', initOptions); - adomikAdapter.adapterEnableAnalytics(config); + } else { + logInfo('Adomik Analytics ignored for sampling', config.sampling); } }; diff --git a/modules/adotBidAdapter.js b/modules/adotBidAdapter.js index ddd9531eb43..a28ab4257df 100644 --- a/modules/adotBidAdapter.js +++ b/modules/adotBidAdapter.js @@ -1,313 +1,238 @@ import {Renderer} from '../src/Renderer.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; -import {isStr, isArray, isNumber, isPlainObject, isBoolean, logError, replaceAuctionPrice} from '../src/utils.js'; -import find from 'core-js-pure/features/array/find.js'; -import { config } from '../src/config.js'; +import {isArray, isBoolean, isFn, isPlainObject, isStr, logError, replaceAuctionPrice} from '../src/utils.js'; +import {find} from '../src/polyfill.js'; +import {config} from '../src/config.js'; +import { OUTSTREAM } from '../src/video.js'; -const ADAPTER_VERSION = 'v1.0.0'; +const BIDDER_CODE = 'adot'; +const ADAPTER_VERSION = 'v2.0.0'; const BID_METHOD = 'POST'; const BIDDER_URL = 'https://dsp.adotmob.com/headerbidding{PUBLISHER_PATH}/bidrequest'; -const FIRST_PRICE = 1; -const NET_REVENUE = true; -// eslint-disable-next-line no-template-curly-in-string -const AUCTION_PRICE = '${AUCTION_PRICE}'; -const TTL = 10; - -const SUPPORTED_VIDEO_CONTEXTS = ['instream', 'outstream']; -const SUPPORTED_INSTREAM_CONTEXTS = ['pre-roll', 'mid-roll', 'post-roll']; -const SUPPORTED_VIDEO_MIMES = ['video/mp4']; -const BID_SUPPORTED_MEDIA_TYPES = ['banner', 'video', 'native']; - +const REQUIRED_VIDEO_PARAMS = ['mimes', 'minduration', 'maxduration', 'protocols']; const DOMAIN_REGEX = new RegExp('//([^/]*)'); -const OUTSTREAM_VIDEO_PLAYER_URL = 'https://adserver.adotmob.com/video/player.min.js'; - +const FIRST_PRICE = 1; +const IMP_BUILDER = { banner: buildBanner, video: buildVideo, native: buildNative }; const NATIVE_PLACEMENTS = { - title: {id: 1, name: 'title'}, - icon: {id: 2, type: 1, name: 'img'}, - image: {id: 3, type: 3, name: 'img'}, - sponsoredBy: {id: 4, name: 'data', type: 1}, - body: {id: 5, name: 'data', type: 2}, - cta: {id: 6, type: 12, name: 'data'} + title: { id: 1, name: 'title' }, + icon: { id: 2, type: 1, name: 'img' }, + image: { id: 3, type: 3, name: 'img' }, + sponsoredBy: { id: 4, name: 'data', type: 1 }, + body: { id: 5, name: 'data', type: 2 }, + cta: { id: 6, type: 12, name: 'data' } }; -const NATIVE_ID_MAPPING = {1: 'title', 2: 'icon', 3: 'image', 4: 'sponsoredBy', 5: 'body', 6: 'cta'}; -const NATIVE_PRESET_FORMATTERS = { - image: formatNativePresetImage -} - -function isNone(value) { - return (value === null) || (value === undefined); -} - -function groupBy(values, key) { - const groups = values.reduce((acc, value) => { - const groupId = value[key]; - - if (!acc[groupId]) acc[groupId] = []; - acc[groupId].push(value); - - return acc; - }, {}); - - return Object - .keys(groups) - .map(id => ({id, key, values: groups[id]})); -} - -function validateMediaTypes(mediaTypes, allowedMediaTypes) { - if (!isPlainObject(mediaTypes)) return false; - if (!allowedMediaTypes.some(mediaType => mediaType in mediaTypes)) return false; - - if (isBanner(mediaTypes)) { - if (!validateBanner(mediaTypes.banner)) return false; - } - - if (isVideo(mediaTypes)) { - if (!validateVideo(mediaTypes.video)) return false; +const NATIVE_ID_MAPPING = { 1: 'title', 2: 'icon', 3: 'image', 4: 'sponsoredBy', 5: 'body', 6: 'cta' }; +const OUTSTREAM_VIDEO_PLAYER_URL = 'https://adserver.adotmob.com/video/player.min.js'; +const BID_RESPONSE_NET_REVENUE = true; +const BID_RESPONSE_TTL = 10; +const DEFAULT_CURRENCY = 'USD'; + +/** + * Parse string in plain object + * + * @param {string} data + * @returns {object|null} Parsed object or null + */ +function tryParse(data) { + try { + return JSON.parse(data); + } catch (err) { + logError(err); + return null; } - - return true; -} - -function isBanner(mediaTypes) { - return isPlainObject(mediaTypes) && isPlainObject(mediaTypes.banner); -} - -function isVideo(mediaTypes) { - return isPlainObject(mediaTypes) && 'video' in mediaTypes; -} - -function validateBanner(banner) { - return isPlainObject(banner) && - isArray(banner.sizes) && - (banner.sizes.length > 0) && - banner.sizes.every(validateMediaSizes); } -function validateVideo(video) { - if (!isPlainObject(video)) return false; - if (!isStr(video.context)) return false; - if (SUPPORTED_VIDEO_CONTEXTS.indexOf(video.context) === -1) return false; - - if (!video.playerSize) return true; - if (!isArray(video.playerSize)) return false; - - return video.playerSize.every(validateMediaSizes); -} - -function validateMediaSizes(mediaSize) { - return isArray(mediaSize) && - (mediaSize.length === 2) && - mediaSize.every(size => (isNumber(size) && size >= 0)); -} - -function validateParameters(parameters, adUnit) { - if (isVideo(adUnit.mediaTypes)) { - if (!isPlainObject(parameters)) return false; - if (!isPlainObject(adUnit.mediaTypes.video)) return false; - if (!validateVideoParameters(parameters.video, adUnit)) return false; - } - - return true; +/** + * Extract domain from given url + * + * @param {string} url + * @returns {string|null} Extracted domain + */ +function extractDomainFromURL(url) { + if (!url || !isStr(url)) return null; + const domain = url.match(DOMAIN_REGEX); + if (isArray(domain) && domain.length === 2) return domain[1]; + return null; } -function validateVideoParameters(videoParams, adUnit) { - const video = adUnit.mediaTypes.video; - - if (!video) return false; +/** + * Create and return site OpenRtb object from given bidderRequest + * + * @param {BidderRequest} bidderRequest + * @returns {Site|null} Formatted Site OpenRtb object or null + */ +function getOpenRTBSiteObject(bidderRequest) { + if (!bidderRequest || !bidderRequest.refererInfo) return null; - if (!isArray(video.mimes)) return false; - if (video.mimes.length === 0) return false; - if (!video.mimes.every(isStr)) return false; - - if (video.minDuration && !isNumber(video.minDuration)) return false; - if (video.maxDuration && !isNumber(video.maxDuration)) return false; - - if (!isArray(video.protocols)) return false; - if (video.protocols.length === 0) return false; - if (!video.protocols.every(isNumber)) return false; - - if (isInstream(video)) { - if (!videoParams.instreamContext) return false; - if (SUPPORTED_INSTREAM_CONTEXTS.indexOf(videoParams.instreamContext) === -1) return false; - } - - return true; -} + const domain = extractDomainFromURL(bidderRequest.refererInfo.referer); + const publisherId = config.getConfig('adot.publisherId'); -function validateServerRequest(serverRequest) { - return isPlainObject(serverRequest) && - isPlainObject(serverRequest.data) && - isArray(serverRequest.data.imp) && - isPlainObject(serverRequest._adot_internal) && - isArray(serverRequest._adot_internal.impressions) -} + if (!domain) return null; -function createServerRequestFromAdUnits(adUnits, bidRequestId, adUnitContext) { - const publisherPath = config.getConfig('adot.publisherPath') === undefined ? '' : '/' + config.getConfig('adot.publisherPath'); return { - method: BID_METHOD, - url: BIDDER_URL.replace('{PUBLISHER_PATH}', publisherPath), - data: generateBidRequestsFromAdUnits(adUnits, bidRequestId, adUnitContext), - _adot_internal: generateAdotInternal(adUnits) - } -} - -function generateAdotInternal(adUnits) { - const impressions = adUnits.reduce((acc, adUnit) => { - const {bidId, mediaTypes, adUnitCode, params} = adUnit; - const base = {bidId, adUnitCode, container: params.video && params.video.container}; - - const imps = Object - .keys(mediaTypes) - .reduce((acc, mediaType, index) => { - const data = mediaTypes[mediaType]; - const impressionId = `${bidId}_${index}`; - - if (mediaType !== 'banner') return acc.concat({...base, impressionId}); - - const bannerImps = data.sizes.map((item, i) => ({...base, impressionId: `${impressionId}_${i}`})); - - return acc.concat(bannerImps); - }, []); - - return acc.concat(imps); - }, []); - - return {impressions}; + page: bidderRequest.refererInfo.referer, + domain: domain, + name: domain, + publisher: { + id: publisherId + } + }; } -function generateBidRequestsFromAdUnits(adUnits, bidRequestId, adUnitContext) { +/** + * Create and return Device OpenRtb object + * + * @returns {Device} Formatted Device OpenRtb object or null + */ +function getOpenRTBDeviceObject() { + return { ua: navigator.userAgent, language: navigator.language }; +} + +/** + * Create and return User OpenRtb object + * + * @param {BidderRequest} bidderRequest + * @returns {User|null} Formatted User OpenRtb object or null + */ +function getOpenRTBUserObject(bidderRequest) { + if (!bidderRequest || !bidderRequest.gdprConsent || !isStr(bidderRequest.gdprConsent.consentString)) return null; + return { ext: { consent: bidderRequest.gdprConsent.consentString } }; +} + +/** + * Create and return Regs OpenRtb object + * + * @param {BidderRequest} bidderRequest + * @returns {Regs|null} Formatted Regs OpenRtb object or null + */ +function getOpenRTBRegsObject(bidderRequest) { + if (!bidderRequest || !bidderRequest.gdprConsent || !isBoolean(bidderRequest.gdprConsent.gdprApplies)) return null; + return { ext: { gdpr: bidderRequest.gdprConsent.gdprApplies } }; +} + +/** + * Create and return Ext OpenRtb object + * + * @param {BidderRequest} bidderRequest + * @returns {Ext|null} Formatted Ext OpenRtb object or null + */ +function getOpenRTBExtObject() { return { - id: bidRequestId, - imp: adUnits.reduce(generateImpressionsFromAdUnit, []), - site: generateSiteFromAdUnitContext(adUnitContext), - device: getDeviceInfo(), - user: getUserInfoFromAdUnitContext(adUnitContext), - regs: getRegulationFromAdUnitContext(adUnitContext), - at: FIRST_PRICE, - ext: generateBidRequestExtension() + adot: { adapter_version: ADAPTER_VERSION }, + should_use_gzip: true }; } -function generateImpressionsFromAdUnit(acc, adUnit) { - const {bidId, mediaTypes, params} = adUnit; - const {placementId} = params; - const pmp = {}; - const ext = {placementId}; - - if (placementId) pmp.deals = [{id: placementId}] - - const imps = Object - .keys(mediaTypes) - .reduce((acc, mediaType, index) => { - const data = mediaTypes[mediaType]; - const impId = `${bidId}_${index}`; - - if (mediaType === 'banner') return acc.concat(generateBannerFromAdUnit(impId, data, params)); - if (mediaType === 'video') return acc.concat({id: impId, video: generateVideoFromAdUnit(data, params), pmp, ext}); - if (mediaType === 'native') return acc.concat({id: impId, native: generateNativeFromAdUnit(data), pmp, ext}); - }, []); - - return acc.concat(imps); -} - -function isImpressionAVideo(impression) { - return isPlainObject(impression) && isPlainObject(impression.video); +/** + * Return MediaType from MediaTypes object + * + * @param {MediaType} mediaTypes Prebid MediaTypes + * @returns {string|null} Mediatype or null if not found + */ +function getMediaType(mediaTypes) { + if (mediaTypes.banner) return 'banner'; + if (mediaTypes.video) return 'video'; + if (mediaTypes.native) return 'native'; + return null; } -function generateBannerFromAdUnit(impId, data, params) { - const {position, placementId} = params; - const pos = position || 0; - const pmp = {}; - const ext = {placementId}; - - if (placementId) pmp.deals = [{id: placementId}] +/** + * Build OpenRtb imp banner from given bidderRequest and media + * + * @param {Banner} banner MediaType Banner Object + * @param {BidderRequest} bidderRequest + * @returns {OpenRtbBanner} OpenRtb banner object + */ +function buildBanner(banner, bidderRequest) { + const pos = bidderRequest.position || 0; + const format = (banner.sizes || []).map(([w, h]) => ({ w, h })); + return { format, pos }; +} + +/** + * Build object with w and h value depending on given video media + * + * @param {Video} video MediaType Video Object + * @returns {Object} Size as { w: number; h: number } + */ +function getVideoSize(video) { + const sizes = video.playerSize || []; + const format = sizes.length > 0 ? sizes[0] : []; - return data.sizes.map(([w, h], index) => ({id: `${impId}_${index}`, banner: {format: [{w, h}], w, h, pos}, pmp, ext})); + return { + w: format[0] || null, + h: format[1] || null + }; } -function generateVideoFromAdUnit(data, params) { - const {playerSize} = data; - const video = data - - const hasPlayerSize = isArray(playerSize) && playerSize.length > 0; - const {minDuration, maxDuration, protocols} = video; - - const size = {width: hasPlayerSize ? playerSize[0][0] : null, height: hasPlayerSize ? playerSize[0][1] : null}; - const duration = {min: isNumber(minDuration) ? minDuration : null, max: isNumber(maxDuration) ? maxDuration : null}; - const startdelay = computeStartDelay(data, params); +/** + * Build OpenRtb imp video from given bidderRequest and media + * + * @param {Video} video MediaType Video Object + * @returns {OpenRtbVideo} OpenRtb video object + */ +function buildVideo(video) { + const { w, h } = getVideoSize(video); return { - mimes: SUPPORTED_VIDEO_MIMES, - skip: video.skippable || 0, - w: size.width, - h: size.height, - startdelay: startdelay, + api: video.api, + w, + h, linearity: video.linearity || null, - minduration: duration.min, - maxduration: duration.max, - protocols, - api: getApi(protocols), - format: hasPlayerSize ? playerSize.map(s => { - return {w: s[0], h: s[1]}; - }) : null, - pos: video.position || 0 + mimes: video.mimes, + minduration: video.minduration, + maxduration: video.maxduration, + placement: video.placement, + playbackmethod: video.playbackmethod, + pos: video.position || 0, + protocols: video.protocols, + skip: video.skip || 0, + startdelay: video.startdelay }; } -function getApi(protocols) { - let defaultValue = [2]; - let listProtocols = [ - {key: 'VPAID_1_0', value: 1}, - {key: 'VPAID_2_0', value: 2}, - {key: 'MRAID_1', value: 3}, - {key: 'ORMMA', value: 4}, - {key: 'MRAID_2', value: 5}, - {key: 'MRAID_3', value: 6}, - ]; - if (protocols) { - return listProtocols.filter(p => { - return protocols.indexOf(p.key) !== -1; - }).map(p => p.value) - } else { - return defaultValue; - } -} - -function isInstream(video) { - return isPlainObject(video) && (video.context === 'instream'); -} +/** + * Check if given Native Media is an asset of type Image. + * + * Return default native assets if given media is an asset + * Return given native assets if given media is not an asset + * + * @param {NativeMedia} native Native Mediatype + * @returns {OpenRtbNativeAssets} + */ +function cleanNativeMedia(native) { + if (native.type !== 'image') return native; -function isOutstream(video) { - return isPlainObject(video) && (video.startdelay === null) + return { + image: { required: true, sizes: native.sizes }, + title: { required: true }, + sponsoredBy: { required: true }, + body: { required: false }, + cta: { required: false }, + icon: { required: false } + }; } -function computeStartDelay(data, params) { - if (isInstream(data)) { - if (params.video.instreamContext === 'pre-roll') return 0; - if (params.video.instreamContext === 'mid-roll') return -1; - if (params.video.instreamContext === 'post-roll') return -2; - } - - return null; -} +/** + * Build Native OpenRtb Imp from Native Mediatype + * + * @param {NativeMedia} native Native Mediatype + * @returns {OpenRtbNative} + */ +function buildNative(native) { + native = cleanNativeMedia(native); -function generateNativeFromAdUnit(data) { - const {type} = data; - const presetFormatter = type && NATIVE_PRESET_FORMATTERS[data.type]; - const nativeFields = presetFormatter ? presetFormatter(data) : data; + const assets = Object.keys(native) + .reduce((nativeAssets, assetKey) => { + const asset = native[assetKey]; + const assetInfo = NATIVE_PLACEMENTS[assetKey]; - const assets = Object - .keys(nativeFields) - .reduce((acc, placement) => { - const placementData = nativeFields[placement]; - const assetInfo = NATIVE_PLACEMENTS[placement]; + if (!assetInfo) return nativeAssets; - if (!assetInfo) return acc; + const { id, name, type } = assetInfo; + const { required, len, sizes = [] } = asset; - const {id, name, type} = assetInfo; - const {required, len, sizes = []} = placementData; let wmin; let hmin; @@ -319,249 +244,165 @@ function generateNativeFromAdUnit(data) { hmin = sizes[1]; } - const content = {}; + const newAsset = {}; - if (type) content.type = type; - if (len) content.len = len; - if (wmin) content.wmin = wmin; - if (hmin) content.hmin = hmin; + if (type) newAsset.type = type; + if (len) newAsset.len = len; + if (wmin) newAsset.wmin = wmin; + if (hmin) newAsset.hmin = hmin; - acc.push({id, required, [name]: content}); + nativeAssets.push({ id, required, [name]: newAsset }); - return acc; + return nativeAssets; }, []); - return { - request: JSON.stringify({assets}) - }; + return { request: JSON.stringify({ assets }) }; } -function formatNativePresetImage(data) { - const sizes = data.sizes; +/** + * Build OpenRtb Imp object from given Adunit and Context + * + * @param {AdUnit} adUnit PrebidJS Adunit + * @param {BidderRequest} bidderRequest PrebidJS Bidder Request + * @returns {Imp} OpenRtb Impression + */ +function buildImpFromAdUnit(adUnit, bidderRequest) { + const { bidId, mediaTypes, params, adUnitCode } = adUnit; + const mediaType = getMediaType(mediaTypes); - return { - image: { - required: true, - sizes - }, - title: { - required: true - }, - sponsoredBy: { - required: true - }, - body: { - required: false - }, - cta: { - required: false - }, - icon: { - required: false - } - }; -} + if (!mediaType) return null; -function generateSiteFromAdUnitContext(adUnitContext) { - if (!adUnitContext || !adUnitContext.refererInfo) return null; - - const domain = extractSiteDomainFromURL(adUnitContext.refererInfo.referer); - const publisherId = config.getConfig('adot.publisherId'); - - if (!domain) return null; - - return { - page: adUnitContext.refererInfo.referer, - domain: domain, - name: domain, - publisher: { - id: publisherId - } - }; -} - -function extractSiteDomainFromURL(url) { - if (!url || !isStr(url)) return null; - - const domain = url.match(DOMAIN_REGEX); - - if (isArray(domain) && domain.length === 2) return domain[1]; - - return null; -} - -function getDeviceInfo() { - return {ua: navigator.userAgent, language: navigator.language}; -} - -function getUserInfoFromAdUnitContext(adUnitContext) { - if (!adUnitContext || !adUnitContext.gdprConsent) return null; - if (!isStr(adUnitContext.gdprConsent.consentString)) return null; - - return { - ext: { - consent: adUnitContext.gdprConsent.consentString - } - }; -} - -function getRegulationFromAdUnitContext(adUnitContext) { - if (!adUnitContext || !adUnitContext.gdprConsent) return null; - if (!isBoolean(adUnitContext.gdprConsent.gdprApplies)) return null; + const media = IMP_BUILDER[mediaType](mediaTypes[mediaType], bidderRequest, adUnit) + const currency = config.getConfig('currency.adServerCurrency') || DEFAULT_CURRENCY; + const bidfloor = getMainFloor(adUnit, media.format, mediaType, currency); return { + id: bidId, ext: { - gdpr: adUnitContext.gdprConsent.gdprApplies - } - }; -} - -function generateBidRequestExtension() { - return { - adot: {adapter_version: ADAPTER_VERSION}, - should_use_gzip: true + placementId: params.placementId, + adUnitCode, + container: params.video && params.video.container + }, + [mediaType]: media, + bidfloorcur: currency, + bidfloor }; } -function validateServerResponse(serverResponse) { - return isPlainObject(serverResponse) && - isPlainObject(serverResponse.body) && - isStr(serverResponse.body.cur) && - isArray(serverResponse.body.seatbid); -} - -function seatBidsToAds(seatBid, bidResponse, serverRequest) { - return seatBid.bid - .filter(bid => validateBids(bid, serverRequest)) - .map(bid => generateAdFromBid(bid, bidResponse, serverRequest)); -} - -function validateBids(bid, serverRequest) { - if (!isPlainObject(bid)) return false; - if (!isStr(bid.impid)) return false; - if (!isStr(bid.crid)) return false; - if (!isNumber(bid.price)) return false; - - if (!isPlainObject(bid.ext)) return false; - if (!isPlainObject(bid.ext.adot)) return false; - if (!isStr(bid.ext.adot.media_type)) return false; - if (BID_SUPPORTED_MEDIA_TYPES.indexOf(bid.ext.adot.media_type) === -1) return false; - - if (!bid.adm && !bid.nurl) return false; - if (bid.adm) { - if (!isStr(bid.adm)) return false; - if (bid.adm.indexOf(AUCTION_PRICE) === -1) return false; - } - if (bid.nurl) { - if (!isStr(bid.nurl)) return false; - if (bid.nurl.indexOf(AUCTION_PRICE) === -1) return false; - } - - if (isBidABanner(bid)) { - if (!isNumber(bid.h)) return false; - if (!isNumber(bid.w)) return false; - } - if (isBidAVideo(bid)) { - if (!(isNone(bid.h) || isNumber(bid.h))) return false; - if (!(isNone(bid.w) || isNumber(bid.w))) return false; - } - - const impression = getImpressionData(serverRequest, bid.impid); - - if (!isPlainObject(impression.openRTB)) return false; - if (!isPlainObject(impression.internal)) return false; - if (!isStr(impression.internal.adUnitCode)) return false; - - if (isBidABanner(bid)) { - if (!isPlainObject(impression.openRTB.banner)) return false; - } - if (isBidAVideo(bid)) { - if (!isPlainObject(impression.openRTB.video)) return false; - } - if (isBidANative(bid)) { - if (!isPlainObject(impression.openRTB.native) || !tryParse(bid.adm)) return false; - } - +/** + * Return if given video is Valid. + * A video is defined as valid if it contains all required fields + * + * @param {VideoMedia} video + * @returns {boolean} + */ +function isValidVideo(video) { + if (REQUIRED_VIDEO_PARAMS.some((param) => video[param] === undefined)) return false; return true; } -function isBidABanner(bid) { - return isPlainObject(bid) && - isPlainObject(bid.ext) && - isPlainObject(bid.ext.adot) && - bid.ext.adot.media_type === 'banner'; -} - -function isBidAVideo(bid) { - return isPlainObject(bid) && - isPlainObject(bid.ext) && - isPlainObject(bid.ext.adot) && - bid.ext.adot.media_type === 'video'; -} - -function isBidANative(bid) { - return isPlainObject(bid) && - isPlainObject(bid.ext) && - isPlainObject(bid.ext.adot) && - bid.ext.adot.media_type === 'native'; -} - -function getImpressionData(serverRequest, impressionId) { - const openRTBImpression = find(serverRequest.data.imp, imp => imp.id === impressionId); - const internalImpression = find(serverRequest._adot_internal.impressions, imp => imp.impressionId === impressionId); - +/** + * Return if given bid is Valid. + * A bid is defined as valid if it media is a valid video or other media + * + * @param {Bid} bid + * @returns {boolean} + */ +function isBidRequestValid(bid) { + const video = bid.mediaTypes.video; + return !video || isValidVideo(video); +} + +/** + * Build OpenRtb request from Prebid AdUnits and Bidder request + * + * @param {Array} adUnits Array of PrebidJS Adunit + * @param {BidderRequest} bidderRequest PrebidJS BidderRequest + * @param {string} requestId Request ID + * + * @returns {OpenRTBBidRequest} OpenRTB bid request + */ +function buildBidRequest(adUnits, bidderRequest, requestId) { + const data = { + id: requestId, + imp: adUnits.map((adUnit) => buildImpFromAdUnit(adUnit, bidderRequest)).filter((item) => !!item), + site: getOpenRTBSiteObject(bidderRequest), + device: getOpenRTBDeviceObject(), + user: getOpenRTBUserObject(bidderRequest), + regs: getOpenRTBRegsObject(bidderRequest), + ext: getOpenRTBExtObject(), + at: FIRST_PRICE + }; + return data; +} + +/** + * Build PrebidJS Ajax request + * + * @param {Array} adUnits Array of PrebidJS Adunit + * @param {BidderRequest} bidderRequest PrebidJS BidderRequest + * @param {string} bidderUrl Adot Bidder URL + * @param {string} requestId Request ID + * @returns + */ +function buildAjaxRequest(adUnits, bidderRequest, bidderUrl, requestId) { return { - id: impressionId, - openRTB: openRTBImpression || null, - internal: internalImpression || null + method: BID_METHOD, + url: bidderUrl, + data: buildBidRequest(adUnits, bidderRequest, requestId) }; } -function generateAdFromBid(bid, bidResponse, serverRequest) { - const impressionData = getImpressionData(serverRequest, bid.impid); - const isVideo = isBidAVideo(bid); - const base = { - requestId: impressionData.internal.bidId, - cpm: bid.price, - currency: bidResponse.cur, - ttl: TTL, - creativeId: bid.crid, - netRevenue: NET_REVENUE, - mediaType: bid.ext.adot.media_type, - }; - - if (bid.adomain) { - base.meta = { advertiserDomains: bid.adomain }; - } - - if (isBidANative(bid)) return {...base, native: formatNativeData(bid)}; - - const size = getSizeFromBid(bid, impressionData); - const creative = getCreativeFromBid(bid, impressionData); - - return { - ...base, - height: size.height, - width: size.width, - ad: creative.markup, - adUrl: creative.markupUrl, - vastXml: isVideo && !isStr(creative.markupUrl) ? creative.markup : null, - vastUrl: isVideo && isStr(creative.markupUrl) ? creative.markupUrl : null, - renderer: creative.renderer - }; +/** + * Split given PrebidJS Request in Dictionnary + * + * @param {Array} validBidRequests + * @returns {Dictionnary} + */ +function splitAdUnits(validBidRequests) { + return validBidRequests.reduce((adUnits, adUnit) => { + const bidderRequestId = adUnit.bidderRequestId; + if (!adUnits[bidderRequestId]) { + adUnits[bidderRequestId] = []; + } + adUnits[bidderRequestId].push(adUnit); + return adUnits; + }, {}); } -function formatNativeData({adm, price}) { +/** + * Build Ajax request Array + * + * @param {Array} validBidRequests + * @param {BidderRequest} bidderRequest + * @returns {Array} + */ +function buildRequests(validBidRequests, bidderRequest) { + const adUnits = splitAdUnits(validBidRequests); + const publisherPathConfig = config.getConfig('adot.publisherPath'); + const publisherPath = publisherPathConfig === undefined ? '' : '/' + publisherPathConfig; + const bidderUrl = BIDDER_URL.replace('{PUBLISHER_PATH}', publisherPath); + + return Object.keys(adUnits).map((requestId) => buildAjaxRequest(adUnits[requestId], bidderRequest, bidderUrl, requestId)); +} + +/** + * Build Native PrebidJS Response grom OpenRtb Response + * + * @param {OpenRtbBid} bid + * + * @returns {NativeAssets} Native PrebidJS + */ +function buildNativeBidData(bid) { + const { adm, price } = bid; const parsedAdm = tryParse(adm); - const {assets, link: {url, clicktrackers}, imptrackers, jstracker} = parsedAdm.native; - const placements = NATIVE_PLACEMENTS; - const placementIds = NATIVE_ID_MAPPING; + const { assets, link: { url, clicktrackers }, imptrackers, jstracker } = parsedAdm.native; return assets.reduce((acc, asset) => { - const placementName = placementIds[asset.id]; - const content = placementName && asset[placements[placementName].name]; + const placementName = NATIVE_ID_MAPPING[asset.id]; + const content = placementName && asset[NATIVE_PLACEMENTS[placementName].name]; if (!content) return acc; - acc[placementName] = content.text || content.value || {url: content.url, width: content.w, height: content.h}; + acc[placementName] = content.text || content.value || { url: content.url, width: content.w, height: content.h }; return acc; }, { clickUrl: url, @@ -571,57 +412,38 @@ function formatNativeData({adm, price}) { }); } -function getSizeFromBid(bid, impressionData) { - if (isNumber(bid.w) && isNumber(bid.h)) { - return { width: bid.w, height: bid.h }; - } - - if (isImpressionAVideo(impressionData.openRTB)) { - const { video } = impressionData.openRTB; - - if (isNumber(video.w) && isNumber(video.h)) { - return { width: video.w, height: video.h }; - } - } - - return { width: null, height: null }; -} - -function getCreativeFromBid(bid, impressionData) { - const shouldUseAdMarkup = !!bid.adm; - const price = bid.price; +/** + * Return Adot Renderer if given Bid is a video one + * + * @param {OpenRtbBid} bid + * @param {string} mediaType + * @returns {any|null} + */ +function buildRenderer(bid, mediaType) { + if (!(mediaType === VIDEO && + bid.ext && + bid.ext.adot && + bid.ext.adot.container && + bid.ext.adot.adUnitCode && + bid.ext.adot.video && + bid.ext.adot.video.type === OUTSTREAM)) return null; + + const container = bid.ext.adot.container + const adUnitCode = bid.ext.adot.adUnitCode - return { - markup: shouldUseAdMarkup ? replaceAuctionPrice(bid.adm, price) : null, - markupUrl: !shouldUseAdMarkup ? replaceAuctionPrice(bid.nurl, price) : null, - renderer: getRendererFromBid(bid, impressionData) - }; -} - -function getRendererFromBid(bid, impressionData) { - const isOutstreamImpression = isBidAVideo(bid) && - isImpressionAVideo(impressionData.openRTB) && - isOutstream(impressionData.openRTB.video); - - return isOutstreamImpression - ? buildOutstreamRenderer(impressionData) - : null; -} - -function buildOutstreamRenderer(impressionData) { const renderer = Renderer.install({ url: OUTSTREAM_VIDEO_PLAYER_URL, loaded: false, - adUnitCode: impressionData.internal.adUnitCode + adUnitCode: adUnitCode }); renderer.setRender((ad) => { ad.renderer.push(() => { - const container = impressionData.internal.container - ? document.querySelector(impressionData.internal.container) - : document.getElementById(impressionData.internal.adUnitCode); + const domContainer = container + ? document.querySelector(container) + : document.getElementById(adUnitCode); - const player = new window.VASTPlayer(container); + const player = new window.VASTPlayer(domContainer); player.on('ready', () => { player.adVolume = 0; @@ -641,54 +463,181 @@ function buildOutstreamRenderer(impressionData) { return renderer; } -function tryParse(data) { - try { - return JSON.parse(data); - } catch (err) { - logError(err); - return null; - } +/** + * Build PrebidJS response from OpenRtbBid + * + * @param {OpenRtbBid} bid + * @param {string} mediaType + * @returns {Object} + */ +function buildCreativeBidData(bid, mediaType) { + const adm = bid.adm ? replaceAuctionPrice(bid.adm, bid.price) : null; + const nurl = (!bid.adm && bid.nurl) ? replaceAuctionPrice(bid.nurl, bid.price) : null; + + return { + width: bid.ext.adot.size && bid.ext.adot.size.w, + height: bid.ext.adot.size && bid.ext.adot.size.h, + ad: adm, + adUrl: nurl, + vastXml: mediaType === VIDEO && !isStr(nurl) ? adm : null, + vastUrl: mediaType === VIDEO && isStr(nurl) ? nurl : null, + renderer: buildRenderer(bid, mediaType) + }; } -const adotBidderSpec = { - code: 'adot', - supportedMediaTypes: [BANNER, VIDEO, NATIVE], - isBidRequestValid(adUnit) { - const allowedBidderCodes = [this.code]; - - return isPlainObject(adUnit) && - allowedBidderCodes.indexOf(adUnit.bidder) !== -1 && - isStr(adUnit.adUnitCode) && - isStr(adUnit.bidderRequestId) && - isStr(adUnit.bidId) && - validateMediaTypes(adUnit.mediaTypes, this.supportedMediaTypes) && - validateParameters(adUnit.params, adUnit); - }, - buildRequests(adUnits, adUnitContext) { - if (!adUnits) return null; - - return groupBy(adUnits, 'bidderRequestId').map(group => { - const bidRequestId = group.id; - const adUnits = groupBy(group.values, 'bidId').map((group) => { - const length = group.values.length; - return length > 0 && group.values[length - 1] - }); +/** + * Return if given bid and imp are valid + * + * @param {OpenRtbBid} bid OpenRtb Bid + * @param {Imp} imp OpenRtb Imp + * @returns {boolean} + */ +function isBidImpInvalid(bid, imp) { + return !bid || !imp; +} + +/** + * Build PrebidJS Bid Response from given OpenRTB Bid + * + * @param {OpenRtbBid} bid + * @param {OpenRtbBidResponse} bidResponse + * @param {OpenRtbBid} imp + * @returns {PrebidJSResponse} + */ +function buildBidResponse(bid, bidResponse, imp) { + if (isBidImpInvalid(bid, imp)) return null; + const mediaType = bid.ext.adot.media_type; + const baseBid = { + requestId: bid.impid, + cpm: bid.price, + currency: bidResponse.cur, + ttl: BID_RESPONSE_TTL, + creativeId: bid.crid, + netRevenue: BID_RESPONSE_NET_REVENUE, + mediaType + }; - return createServerRequestFromAdUnits(adUnits, bidRequestId, adUnitContext) + if (bid.dealid) baseBid.dealId = bid.dealid; + if (bid.adomain) baseBid.meta = { advertiserDomains: bid.adomain }; + + if (mediaType === NATIVE) return { ...baseBid, native: buildNativeBidData(bid) }; + return { ...baseBid, ...buildCreativeBidData(bid, mediaType) }; +} + +/** + * Find OpenRtb Imp from request with same id that given bid + * + * @param {OpenRtbBid} bid + * @param {OpenRtbRequest} bidRequest + * @returns {Imp} OpenRtb Imp + */ +function getImpfromBid(bid, bidRequest) { + if (!bidRequest || !bidRequest.imp) return null; + const imps = bidRequest.imp; + return find(imps, (imp) => imp.id === bid.impid); +} + +/** + * Return if given response is valid + * + * @param {OpenRtbBidResponse} response + * @returns {boolean} + */ +function isValidResponse(response) { + return isPlainObject(response) && + isPlainObject(response.body) && + isStr(response.body.cur) && + isArray(response.body.seatbid); +} + +/** + * Return if given request is valid + * + * @param {OpenRtbRequest} request + * @returns {boolean} + */ +function isValidRequest(request) { + return isPlainObject(request) && + isPlainObject(request.data) && + isArray(request.data.imp); +} + +/** + * Interpret given OpenRtb Response to build PrebidJS Response + * + * @param {OpenRtbBidResponse} serverResponse + * @param {OpenRtbRequest} request + * @returns {PrebidJSResponse} + */ +function interpretResponse(serverResponse, request) { + if (!isValidResponse(serverResponse) || !isValidRequest(request)) return []; + + const bidsResponse = serverResponse.body; + const bidRequest = request.data; + + return bidsResponse.seatbid.reduce((pbsResponse, seatbid) => { + if (!seatbid || !isArray(seatbid.bid)) return pbsResponse; + seatbid.bid.forEach((bid) => { + const imp = getImpfromBid(bid, bidRequest); + const bidResponse = buildBidResponse(bid, bidsResponse, imp); + if (bidResponse) pbsResponse.push(bidResponse); }); - }, - interpretResponse(serverResponse, serverRequest) { - if (!validateServerRequest(serverRequest)) return []; - if (!validateServerResponse(serverResponse)) return []; - - const bidResponse = serverResponse.body; + return pbsResponse; + }, []); +} - return bidResponse.seatbid - .filter(seatBid => isPlainObject(seatBid) && isArray(seatBid.bid)) - .reduce((acc, seatBid) => acc.concat(seatBidsToAds(seatBid, bidResponse, serverRequest)), []); - } +/** + * Call Adunit getFloor function with given argument to get specific floor. + * Return 0 by default + * + * @param {AdUnit} adUnit + * @param {Array|string} size Adunit size or * + * @param {string} mediaType + * @param {string} currency USD by default + * + * @returns {number} Floor price + */ +function getFloor(adUnit, size, mediaType, currency) { + if (!isFn(adUnit.getFloor)) return 0; + + const floorResult = adUnit.getFloor({ currency, mediaType, size }); + + return floorResult.currency === currency ? floorResult.floor : 0; +} + +/** + * Call getFloor for each format and return the lower floor + * Return 0 by default + * + * interface Format { w: number; h: number } + * + * @param {AdUnit} adUnit + * @param {Array} formats Media formats + * @param {string} mediaType + * @param {string} currency USD by default + * + * @returns {number} Lower floor. + */ +function getMainFloor(adUnit, formats, mediaType, currency) { + if (!formats) return getFloor(adUnit, '*', mediaType, currency); + + return formats.reduce((bidFloor, format) => { + const floor = getFloor(adUnit, [format.w, format.h], mediaType, currency) + const maxFloor = bidFloor || Number.MAX_SAFE_INTEGER; + return floor !== 0 && floor < maxFloor ? floor : bidFloor; + }, null) || 0; +} + +/** + * Adot PrebidJS Adapter + */ +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, NATIVE, VIDEO], + isBidRequestValid, + buildRequests, + interpretResponse, + getFloor }; -registerBidder(adotBidderSpec); - -export {adotBidderSpec as spec}; +registerBidder(spec); diff --git a/modules/adotBidAdapter.md b/modules/adotBidAdapter.md index 894a592ec18..d1622e5f901 100644 --- a/modules/adotBidAdapter.md +++ b/modules/adotBidAdapter.md @@ -6,7 +6,7 @@ Adot Bidder Adapter is a module that enables the communication between the Prebi - Module name: Adot Bidder Adapter - Module type: Bidder Adapter -- Maintainer: `aurelien.giudici@adotmob.com` +- Maintainer: `alexandre.lorin@adotmob.com` - Supported media types: `banner`, `video`, `native` ## Example ad units @@ -34,9 +34,9 @@ const adUnit = { ### Video ad unit -#### Outstream video ad unit +#### Video ad unit -Adot Bidder Adapter accepts outstream video ad units using the following ad unit format: +Adot Bidder Adapter accepts video ad units using the following ad unit format: ```javascript const adUnit = { @@ -51,9 +51,9 @@ const adUnit = { // Content MIME types supported by the ad unit. mimes: ['video/mp4'], // Minimum accepted video ad duration (in seconds). - minDuration: 5, + minduration: 5, // Maximum accepted video ad duration (in seconds). - maxDuration: 35, + maxduration: 35, // Video protocols supported by the ad unit (see the OpenRTB 2.5 specifications, // section 5.8). protocols: [2, 3] @@ -61,45 +61,7 @@ const adUnit = { }, bids: [{ bidder: 'adot', - params: { - video: {} - } - }] -} -``` - -#### Instream video ad unit - -Adot Bidder Adapter accepts instream video ad units using the following ad unit format: - -```javascript -const adUnit = { - code: 'test-div', - mediaTypes: { - video: { - // Video context. Must be 'instream'. - context: 'instream', - // Video dimensions supported by the video ad unit. - // Each ad unit size is formatted as follows: [width, height]. - playerSize: [[300, 250]], - // Content MIME types supported by the ad unit. - mimes: ['video/mp4'], - // Minimum accepted video ad duration (in seconds). - minDuration: 5, - // Maximum accepted video ad duration (in seconds). - maxDuration: 35, - // Video protocols supported by the ad unit (see the OpenRTB 2.5 specifications, - // section 5.8). - protocols: [2, 3] - } - }, - bids: [{ - bidder: 'adot', - params: { - video: { - instreamContext: 'pre-roll' - } - } + params: {} }] } ``` diff --git a/modules/adplusBidAdapter.js b/modules/adplusBidAdapter.js new file mode 100644 index 00000000000..4707ca2ff5a --- /dev/null +++ b/modules/adplusBidAdapter.js @@ -0,0 +1,203 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import * as utils from '../src/utils.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { getStorageManager } from '../src/storageManager.js'; + +// #region Constants +export const BIDDER_CODE = 'adplus'; +export const ADPLUS_ENDPOINT = 'https://ssp.ad-plus.com.tr/server/headerBidding'; +export const DGID_CODE = 'adplus_dg_id'; +export const SESSION_CODE = 'adplus_s_id'; +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); +const COOKIE_EXP = 1000 * 60 * 60 * 24; // 1 day +// #endregion + +// #region Helpers +export function isValidUuid (uuid) { + return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test( + uuid + ); +} + +function getSessionId() { + let sid = storage.cookiesAreEnabled() && storage.getCookie(SESSION_CODE); + + if ( + !sid || !isValidUuid(sid) + ) { + sid = utils.generateUUID(); + setSessionId(sid); + } + + return sid; +} + +function setSessionId(sid) { + if (storage.cookiesAreEnabled()) { + const expires = new Date(Date.now() + COOKIE_EXP).toISOString(); + + storage.setCookie(SESSION_CODE, sid, expires); + } +} +// #endregion + +// #region Bid request validation +function isBidRequestValid(bid) { + if (!bid) { + utils.logError(BIDDER_CODE, 'bid, can not be empty', bid); + return false; + } + + if (!bid.params) { + utils.logError(BIDDER_CODE, 'bid.params is required.'); + return false; + } + + if (!bid.params.adUnitId || typeof bid.params.adUnitId !== 'string') { + utils.logError( + BIDDER_CODE, + 'bid.params.adUnitId is missing or has wrong type.' + ); + return false; + } + + if (!bid.params.inventoryId || typeof bid.params.inventoryId !== 'string') { + utils.logError( + BIDDER_CODE, + 'bid.params.inventoryId is missing or has wrong type.' + ); + return false; + } + + if ( + !bid.mediaTypes || + !bid.mediaTypes[BANNER] || + !utils.isArray(bid.mediaTypes[BANNER].sizes) || + bid.mediaTypes[BANNER].sizes.length <= 0 || + !utils.isArrayOfNums(bid.mediaTypes[BANNER].sizes[0]) + ) { + utils.logError(BIDDER_CODE, 'Wrong or missing size parameters.'); + return false; + } + + return true; +} +// #endregion + +// #region Building the bid requests +/** + * + * @param {object} bid + * @returns + */ +function createBidRequest(bid) { + // Developer Params + const { + inventoryId, + adUnitId, + extraData, + yearOfBirth, + gender, + categories, + latitude, + longitude, + sdkVersion, + } = bid.params; + + return { + method: 'GET', + url: ADPLUS_ENDPOINT, + data: utils.cleanObj({ + bidId: bid.bidId, + inventoryId, + adUnitId, + adUnitWidth: bid.mediaTypes[BANNER].sizes[0][0], + adUnitHeight: bid.mediaTypes[BANNER].sizes[0][1], + extraData, + yearOfBirth, + gender, + categories, + latitude, + longitude, + sdkVersion: sdkVersion || '1', + session: getSessionId(), + interstitial: 0, + token: typeof window.top === 'object' && window.top[DGID_CODE] ? window.top[DGID_CODE] : undefined, + secure: window.location.protocol === 'https:' ? 1 : 0, + screenWidth: screen.width, + screenHeight: screen.height, + language: window.navigator.language || 'en-US', + pageUrl: window.location.href, + domain: window.location.hostname, + referrer: window.location.referrer, + }), + }; +} + +function buildRequests(validBidRequests, bidderRequest) { + return validBidRequests.map((req) => createBidRequest(req)); +} +// #endregion + +// #region Interpreting Responses +/** + * + * @param {HeaderBiddingResponse} responseData + * @param { object } bidParams + * @returns + */ +function createAdResponse(responseData, bidParams) { + return { + requestId: responseData.requestID, + cpm: responseData.cpm, + currency: responseData.currency, + width: responseData.width, + height: responseData.height, + creativeId: responseData.creativeID, + dealId: responseData.dealID, + netRevenue: responseData.netRevenue, + ttl: responseData.ttl, + ad: responseData.ad, + mediaType: responseData.mediaType, + meta: { + advertiserDomains: responseData.advertiserDomains, + primaryCatId: utils.isArray(responseData.categoryIDs) && responseData.categoryIDs.length > 0 + ? responseData.categoryIDs[0] : undefined, + secondaryCatIds: responseData.categoryIDs, + }, + }; +} + +function interpretResponse(response, request) { + // In case of empty response + if ( + response.body == null || + !utils.isArray(response.body) || + response.body.length === 0 + ) { + return []; + } + const bids = response.body.map((bid) => createAdResponse(bid)); + return bids; +} +// #endregion + +// #region Bidder +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + isBidRequestValid, + buildRequests, + interpretResponse, + onTimeout(timeoutData) { + utils.logError('Adplus adapter timed out for the auction.', timeoutData); + }, + onBidWon(bid) { + utils.logInfo( + `Adplus adapter won the auction. Bid id: ${bid.bidId}, Ad Unit Id: ${bid.adUnitId}, Inventory Id: ${bid.inventoryId}` + ); + }, +}; + +registerBidder(spec); +// #endregion diff --git a/modules/adplusBidAdapter.md b/modules/adplusBidAdapter.md new file mode 100644 index 00000000000..dce9e4a312f --- /dev/null +++ b/modules/adplusBidAdapter.md @@ -0,0 +1,39 @@ +# Overview + +Module Name: AdPlus Bidder Adapter + +Module Type: Bidder Adapter + +Maintainer: adplus.destek@yaani.com.tr + +# Description + +AdPlus Prebid.js Bidder Adapter. Only banner formats are supported. + +About us : https://ssp.ad-plus.com.tr/ + +# Test Parameters + +```javascript +var adUnits = [ + { + code: "div-adplus", + mediaTypes: { + banner: { + sizes: [ + [300, 250], + ], + }, + }, + bids: [ + { + bidder: "adplus", + params: { + inventoryId: "-1", + adUnitId: "-3", + }, + }, + ], + }, +]; +``` diff --git a/modules/adpod.js b/modules/adpod.js index ddceed1c344..b7c459fd66f 100644 --- a/modules/adpod.js +++ b/modules/adpod.js @@ -13,23 +13,36 @@ */ import { - generateUUID, deepAccess, logWarn, logInfo, isArrayOfNums, isArray, isNumber, logError, groupBy, compareOn, - isPlainObject + compareOn, + deepAccess, + generateUUID, + groupBy, + isArray, + isArrayOfNums, + isNumber, + isPlainObject, + logError, + logInfo, + logWarn } from '../src/utils.js'; -import { addBidToAuction, doCallbacksIfTimedout, AUCTION_IN_PROGRESS, callPrebidCache, getPriceByGranularity, getPriceGranularity } from '../src/auction.js'; -import { checkAdUnitSetup } from '../src/prebid.js'; -import { checkVideoBidSetup } from '../src/video.js'; -import { setupBeforeHookFnOnce, module } from '../src/hook.js'; -import { store } from '../src/videoCache.js'; -import { config } from '../src/config.js'; -import { ADPOD } from '../src/mediaTypes.js'; -import Set from 'core-js-pure/features/set'; -import find from 'core-js-pure/features/array/find.js'; -import { auctionManager } from '../src/auctionManager.js'; +import { + addBidToAuction, + AUCTION_IN_PROGRESS, + callPrebidCache, + doCallbacksIfTimedout, + getPriceByGranularity, + getPriceGranularity +} from '../src/auction.js'; +import {checkAdUnitSetup} from '../src/prebid.js'; +import {checkVideoBidSetup} from '../src/video.js'; +import {module, setupBeforeHookFnOnce} from '../src/hook.js'; +import {store} from '../src/videoCache.js'; +import {config} from '../src/config.js'; +import {ADPOD} from '../src/mediaTypes.js'; +import {find, arrayFrom as from} from '../src/polyfill.js'; +import {auctionManager} from '../src/auctionManager.js'; import CONSTANTS from '../src/constants.json'; -const from = require('core-js-pure/features/array/from.js'); - const TARGETING_KEY_PB_CAT_DUR = 'hb_pb_cat_dur'; const TARGETING_KEY_CACHE_ID = 'hb_cache_id'; @@ -122,7 +135,7 @@ function getPricePartForAdpodKey(bid) { const adpodDealPrefix = config.getConfig(`adpod.dealTier.${bid.bidderCode}.prefix`); pricePart = (adpodDealPrefix) ? adpodDealPrefix + deepAccess(bid, 'video.dealTier') : deepAccess(bid, 'video.dealTier'); } else { - const granularity = getPriceGranularity(bid.mediaType); + const granularity = getPriceGranularity(bid); pricePart = getPriceByGranularity(granularity)(bid); } return pricePart @@ -223,10 +236,9 @@ function firePrebidCacheCall(auctionInstance, bidList, afterBidAdded) { * @param {*} auctionInstance running context of the auction * @param {Object} bidResponse incoming bid; if adpod, will be processed through hook function. If not adpod, returns to original function. * @param {Function} afterBidAdded callback function used when Prebid Cache responds - * @param {Object} bidderRequest copy of bid's associated bidderRequest object + * @param {Object} videoConfig mediaTypes.video from the bid's adUnit */ -export function callPrebidCacheHook(fn, auctionInstance, bidResponse, afterBidAdded, bidderRequest) { - let videoConfig = deepAccess(bidderRequest, 'mediaTypes.video'); +export function callPrebidCacheHook(fn, auctionInstance, bidResponse, afterBidAdded, videoConfig) { if (videoConfig && videoConfig.context === ADPOD) { let brandCategoryExclusion = config.getConfig('adpod.brandCategoryExclusion'); let adServerCatId = deepAccess(bidResponse, 'meta.adServerCatId'); @@ -250,7 +262,7 @@ export function callPrebidCacheHook(fn, auctionInstance, bidResponse, afterBidAd } } } else { - fn.call(this, auctionInstance, bidResponse, afterBidAdded, bidderRequest); + fn.call(this, auctionInstance, bidResponse, afterBidAdded, videoConfig); } } @@ -310,18 +322,17 @@ export function checkAdUnitSetupHook(fn, adUnits) { * (eg if range was [5, 15, 30] -> 2s is rounded to 5s; 17s is rounded back to 15s; 18s is rounded up to 30s) * - if the bid is above the range of the listed durations (and outside the buffer), reject the bid * - set the rounded duration value in the `bid.video.durationBucket` field for accepted bids - * @param {Object} bidderRequest copy of the bidderRequest object associated to bidResponse + * @param {Object} videoMediaType 'mediaTypes.video' associated to bidResponse * @param {Object} bidResponse incoming bidResponse being evaluated by bidderFactory * @returns {boolean} return false if bid duration is deemed invalid as per adUnit configuration; return true if fine */ -function checkBidDuration(bidderRequest, bidResponse) { +function checkBidDuration(videoMediaType, bidResponse) { const buffer = 2; let bidDuration = deepAccess(bidResponse, 'video.durationSeconds'); - let videoConfig = deepAccess(bidderRequest, 'mediaTypes.video'); - let adUnitRanges = videoConfig.durationRangeSec; + let adUnitRanges = videoMediaType.durationRangeSec; adUnitRanges.sort((a, b) => a - b); // ensure the ranges are sorted in numeric order - if (!videoConfig.requireExactDuration) { + if (!videoMediaType.requireExactDuration) { let max = Math.max(...adUnitRanges); if (bidDuration <= (max + buffer)) { let nextHighestRange = find(adUnitRanges, range => (range + buffer) >= bidDuration); @@ -346,12 +357,12 @@ function checkBidDuration(bidderRequest, bidResponse) { * If it's found to not be an adpod bid, it will return to original function via hook logic * @param {Function} fn reference to original function (used by hook logic) * @param {Object} bid incoming bid object - * @param {Object} bidRequest bidRequest object of associated bid + * @param {Object} adUnit adUnit object of associated bid * @param {Object} videoMediaType copy of the `bidRequest.mediaTypes.video` object; used in original function * @param {String} context value of the `bidRequest.mediaTypes.video.context` field; used in original function * @returns {boolean} this return is only used for adpod bids */ -export function checkVideoBidSetupHook(fn, bid, bidRequest, videoMediaType, context) { +export function checkVideoBidSetupHook(fn, bid, adUnit, videoMediaType, context) { if (context === ADPOD) { let result = true; let brandCategoryExclusion = config.getConfig('adpod.brandCategoryExclusion'); @@ -367,7 +378,7 @@ export function checkVideoBidSetupHook(fn, bid, bidRequest, videoMediaType, cont if (!deepAccess(bid, 'video.durationSeconds') || bid.video.durationSeconds <= 0) { result = false; } else { - let isBidGood = checkBidDuration(bidRequest, bid); + let isBidGood = checkBidDuration(videoMediaType, bid); if (!isBidGood) result = false; } } @@ -382,7 +393,7 @@ export function checkVideoBidSetupHook(fn, bid, bidRequest, videoMediaType, cont fn.bail(result); } else { - fn.call(this, bid, bidRequest, videoMediaType, context); + fn.call(this, bid, adUnit, videoMediaType, context); } } diff --git a/modules/adprimeBidAdapter.js b/modules/adprimeBidAdapter.js index 2b5a7e15af2..d64874c393e 100644 --- a/modules/adprimeBidAdapter.js +++ b/modules/adprimeBidAdapter.js @@ -1,10 +1,11 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import { isFn, deepAccess, logMessage } from '../src/utils.js'; +import { config } from '../src/config.js'; const BIDDER_CODE = 'adprime'; const AD_URL = 'https://delta.adprime.com/pbjs'; -const SYNC_URL = 'https://delta.adprime.com'; +const SYNC_URL = 'https://sync.adprime.com'; function isBidResponseValid(bid) { if (!bid.requestId || !bid.cpm || !bid.creativeId || @@ -150,7 +151,8 @@ export const spec = { }, getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { - let syncUrl = SYNC_URL + let syncType = syncOptions.iframeEnabled ? 'iframe' : 'image'; + let syncUrl = SYNC_URL + `/${syncType}?pbjs=1`; if (gdprConsent && gdprConsent.consentString) { if (typeof gdprConsent.gdprApplies === 'boolean') { syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; @@ -161,12 +163,15 @@ export const spec = { if (uspConsent && uspConsent.consentString) { syncUrl += `&ccpa_consent=${uspConsent.consentString}`; } + + const coppa = config.getConfig('coppa') ? 1 : 0; + syncUrl += `&coppa=${coppa}`; + return [{ - type: 'image', + type: syncType, url: syncUrl }]; } - }; registerBidder(spec); diff --git a/modules/adqueryBidAdapter.js b/modules/adqueryBidAdapter.js index ce31f64d705..348bdc90808 100644 --- a/modules/adqueryBidAdapter.js +++ b/modules/adqueryBidAdapter.js @@ -11,7 +11,7 @@ const ADQUERY_USER_SYNC_DOMAIN = ADQUERY_BIDDER_DOMAIN_PROTOCOL + '://' + ADQUER const ADQUERY_DEFAULT_CURRENCY = 'PLN'; const ADQUERY_NET_REVENUE = true; const ADQUERY_TTL = 360; -const storage = getStorageManager(ADQUERY_GVLID); +const storage = getStorageManager({gvlid: ADQUERY_GVLID, bidderCode: ADQUERY_BIDDER_CODE}); /** @type {BidderSpec} */ export const spec = { diff --git a/modules/adqueryIdSystem.js b/modules/adqueryIdSystem.js new file mode 100644 index 00000000000..85421bf588d --- /dev/null +++ b/modules/adqueryIdSystem.js @@ -0,0 +1,103 @@ +/** + * This module adds Adquery QID to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/adqueryIdSystem + * @requires module:modules/userId + */ + +import {ajax} from '../src/ajax.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {submodule} from '../src/hook.js'; +import * as utils from '../src/utils.js'; + +const MODULE_NAME = 'qid'; +const AU_GVLID = 902; + +export const storage = getStorageManager({gvlid: AU_GVLID, moduleName: 'qid'}); + +/** + * Param or default. + * @param {String} param + * @param {String} defaultVal + */ +function paramOrDefault(param, defaultVal, arg) { + if (utils.isFn(param)) { + return param(arg); + } else if (utils.isStr(param)) { + return param; + } + return defaultVal; +} + +/** @type {Submodule} */ +export const adqueryIdSubmodule = { + /** + * used to link submodule with config + * @type {string} + */ + name: MODULE_NAME, + + /** + * IAB TCF Vendor ID + * @type {string} + */ + gvlid: AU_GVLID, + + /** + * decode the stored id value for passing to bid requests + * @function + * @param {{value:string}} value + * @returns {{qid:Object}} + */ + decode(value) { + let qid = storage.getDataFromLocalStorage('qid'); + if (utils.isStr(qid)) { + return {qid: qid}; + } + return (value && typeof value['qid'] === 'string') ? { 'qid': value['qid'] } : undefined; + }, + /** + * performs action to obtain id and return a value in the callback's response argument + * @function + * @param {SubmoduleConfig} [config] + * @returns {IdResponse|undefined} + */ + getId(config) { + if (!utils.isPlainObject(config.params)) { + config.params = {}; + } + const url = paramOrDefault(config.params.url, + `https://bidder.adquery.io/prebid/qid`, + config.params.urlArg); + + const resp = function (callback) { + let qid = storage.getDataFromLocalStorage('qid'); + if (utils.isStr(qid)) { + const responseObj = {qid: qid}; + callback(responseObj); + } else { + const callbacks = { + success: response => { + let responseObj; + if (response) { + try { + responseObj = JSON.parse(response); + } catch (error) { + utils.logError(error); + } + } + callback(responseObj); + }, + error: error => { + utils.logError(`${MODULE_NAME}: ID fetch encountered an error`, error); + callback(); + } + }; + ajax(url, callbacks, undefined, {method: 'GET'}); + } + }; + return {callback: resp}; + } +}; + +submodule('userId', adqueryIdSubmodule); diff --git a/modules/adqueryIdSystem.md b/modules/adqueryIdSystem.md new file mode 100644 index 00000000000..3a49ffbe4da --- /dev/null +++ b/modules/adqueryIdSystem.md @@ -0,0 +1,35 @@ +# Adquery QID + +Adquery QID Module. For assistance setting up your module please contact us at [prebid@adquery.io](prebid@adquery.io). + +### Prebid Params + +Individual params may be set for the Adquery ID Submodule. At least one identifier must be set in the params. + +``` +pbjs.setConfig({ + usersync: { + userIds: [{ + name: 'qid', + storage: { + name: 'qid', + type: 'html5' + } + }] + } +}); +``` +## Parameter Descriptions for the `usersync` Configuration Section +The below parameters apply only to the Adquery User ID Module integration. + +| Param under usersync.userIds[] | Scope | Type | Description | Example | +| --- | --- | --- | --- | --- | +| name | Required | String | ID value for the Adquery ID module - `"qid"` | `"qid"` | +| storage | Required | Object | The publisher must specify the local storage in which to store the results of the call to get the user ID. | | +| storage.type | Required | String | This is where the results of the user ID will be stored. The recommended method is `localStorage` by specifying `html5`. | `"html5"` | +| storage.name | Required | String | The name of the html5 local storage where the user ID will be stored. | `"qid"` | +| storage.expires | Optional | Integer | How long (in days) the user ID information will be stored. | `365` | +| value | Optional | Object | Used only if the page has a separate mechanism for storing the Adquery ID. The value is an object containing the values to be sent to the adapters. In this scenario, no URL is called and nothing is added to local storage | `{"qid": "2abf9f001fcd81241b67"}` | +| params | Optional | Object | Used to store params for the id system | +| params.url | Optional | String | Set an alternate GET url for qid with this parameter | +| params.urlArg | Optional | Object | Optional url parameter for params.url | diff --git a/modules/adrelevantisBidAdapter.js b/modules/adrelevantisBidAdapter.js index 649031d1e3b..3d4de7c7b9d 100644 --- a/modules/adrelevantisBidAdapter.js +++ b/modules/adrelevantisBidAdapter.js @@ -1,14 +1,26 @@ -import { Renderer } from '../src/Renderer.js'; +import {Renderer} from '../src/Renderer.js'; import { - logError, convertTypes, convertCamelToUnderscore, isArray, deepClone, logWarn, logMessage, getBidRequest, deepAccess, - isStr, createTrackPixelHtml, isEmpty, transformBidderParamKeywords, chunk, isArrayOfNums + chunk, + convertCamelToUnderscore, + convertTypes, + createTrackPixelHtml, + deepAccess, + deepClone, + getBidRequest, + isArray, + isArrayOfNums, + isEmpty, + isStr, + logError, + logMessage, + logWarn, + transformBidderParamKeywords } from '../src/utils.js'; -import { config } from '../src/config.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import find from 'core-js-pure/features/array/find.js'; -import includes from 'core-js-pure/features/array/includes.js'; -import { OUTSTREAM, INSTREAM } from '../src/video.js'; +import {config} from '../src/config.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; +import {find, includes} from '../src/polyfill.js'; +import {INSTREAM, OUTSTREAM} from '../src/video.js'; const BIDDER_CODE = 'adrelevantis'; const URL = 'https://ssp.adrelevantis.com/prebid'; @@ -127,7 +139,7 @@ export const spec = { if (fpdcfg && fpdcfg.context) { let fdata = { keywords: fpdcfg.context.keywords || '', - category: fpdcfg.context.category || '' + category: fpdcfg.context.data.category || '' } payload.fpd = fdata; } diff --git a/modules/adrinoBidAdapter.js b/modules/adrinoBidAdapter.js new file mode 100644 index 00000000000..4520066c3e7 --- /dev/null +++ b/modules/adrinoBidAdapter.js @@ -0,0 +1,74 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {triggerPixel} from '../src/utils.js'; +import {NATIVE} from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'adrino'; +const REQUEST_METHOD = 'POST'; +const BIDDER_HOST = 'https://prd-prebid-bidder.adrino.io'; +const GVLID = 1072; + +export const spec = { + code: BIDDER_CODE, + gvlid: GVLID, + supportedMediaTypes: [NATIVE], + + isBidRequestValid: function (bid) { + return !!(bid.bidId) && + !!(bid.params) && + !!(bid.params.hash) && + (typeof bid.params.hash === 'string') && + !!(bid.mediaTypes) && + Object.keys(bid.mediaTypes).includes(NATIVE) && + (bid.bidder === BIDDER_CODE); + }, + + buildRequests: function (validBidRequests, bidderRequest) { + const bidRequests = []; + + for (let i = 0; i < validBidRequests.length; i++) { + let requestData = { + bidId: validBidRequests[i].bidId, + nativeParams: validBidRequests[i].nativeParams, + placementHash: validBidRequests[i].params.hash, + referer: bidderRequest.refererInfo.referer, + userAgent: navigator.userAgent, + } + + if (bidderRequest && bidderRequest.gdprConsent) { + requestData.gdprConsent = { + consentString: bidderRequest.gdprConsent.consentString, + consentRequired: bidderRequest.gdprConsent.gdprApplies + } + } + + bidRequests.push({ + method: REQUEST_METHOD, + url: BIDDER_HOST + '/bidder/bid/', + data: requestData, + options: { + contentType: 'application/json', + withCredentials: false, + } + }); + } + + return bidRequests; + }, + + interpretResponse: function (serverResponse, bidRequest) { + const response = serverResponse.body; + const bidResponses = []; + if (!response.noAd) { + bidResponses.push(response); + } + return bidResponses; + }, + + onBidWon: function (bid) { + if (bid['requestId']) { + triggerPixel(BIDDER_HOST + '/bidder/won/' + bid['requestId']); + } + } +}; + +registerBidder(spec); diff --git a/modules/adrinoBidAdapter.md b/modules/adrinoBidAdapter.md new file mode 100644 index 00000000000..5ec63a72736 --- /dev/null +++ b/modules/adrinoBidAdapter.md @@ -0,0 +1,45 @@ +# Overview + +``` +Module Name: Adrino Bidder Adapter +Module Type: Bidder Adapter +Maintainer: dev@adrino.pl +``` + +# Description + +Module connects to Adrino bidder to fetch bids. Only native format is supported. + +# Test Parameters + +``` +var adUnits = [ + code: '/12345678/prebid_native_example_1', + mediaTypes: { + native: { + image: { + required: true, + sizes: [[300, 210],[300,150],[140,100]] + }, + title: { + required: true + }, + sponsoredBy: { + required: false + }, + body: { + required: false + }, + icon: { + required: false + } + } + }, + bids: [{ + bidder: 'adrino', + params: { + hash: 'abcdef123456' + } + }] +]; +``` diff --git a/modules/adriverBidAdapter.js b/modules/adriverBidAdapter.js index 67e039e4692..5ab417520e9 100644 --- a/modules/adriverBidAdapter.js +++ b/modules/adriverBidAdapter.js @@ -1,13 +1,14 @@ // ADRIVER BID ADAPTER for Prebid 1.13 import { logInfo, getWindowLocation, getBidIdParameter, _each } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { getStorageManager } from '../src/storageManager.js'; const BIDDER_CODE = 'adriver'; const ADRIVER_BID_URL = 'https://pb.adriver.ru/cgi-bin/bid.cgi'; const TIME_TO_LIVE = 3000; +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); export const spec = { - code: BIDDER_CODE, /** @@ -98,6 +99,15 @@ export const spec = { }); }); + let userid = validBidRequests[0].userId; + let adrcidCookie = storage.getDataFromLocalStorage('adrcid') || validBidRequests[0].userId.adrcid; + + if (adrcidCookie) { + payload.adrcid = adrcidCookie; + payload.id5 = userid.id5id; + payload.sharedid = userid.pubcid; + payload.unifiedid = userid.tdid; + } const payloadString = JSON.stringify(payload); return { diff --git a/modules/adriverIdSystem.js b/modules/adriverIdSystem.js new file mode 100644 index 00000000000..6a492fac508 --- /dev/null +++ b/modules/adriverIdSystem.js @@ -0,0 +1,83 @@ +/** + * This module adds AdriverId to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/adriverIdSubmodule + * @requires module:modules/userId + */ + +import { logError, isPlainObject } from '../src/utils.js' +import { ajax } from '../src/ajax.js'; +import { submodule } from '../src/hook.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const MODULE_NAME = 'adriverId'; + +export const storage = getStorageManager(); + +/** @type {Submodule} */ +export const adriverIdSubmodule = { + /** + * used to link submodule with config + * @type {string} + */ + name: MODULE_NAME, + /** + * decode the stored id value for passing to bid requests + * @function + * @param {string} value + * @returns {{adriverId:string}} + */ + decode(value) { + return { adrcid: value } + }, + /** + * performs action to obtain id and return a value in the callback's response argument + * @function + * @param {SubmoduleConfig} [config] + * @param {ConsentData} [consentData] + * @returns {IdResponse|undefined} + */ + getId(config) { + if (!isPlainObject(config.params)) { + config.params = {}; + } + const url = 'https://ad.adriver.ru/cgi-bin/json.cgi?sid=1&ad=719473&bt=55&pid=3198680&bid=7189165&bn=7189165&tuid=1'; + const resp = function (callback) { + let creationDate = storage.getDataFromLocalStorage('adrcid_cd') || storage.getCookie('adrcid_cd'); + let cookie = storage.getDataFromLocalStorage('adrcid') || storage.getCookie('adrcid'); + + if (cookie && creationDate && ((new Date().getTime() - creationDate) < 86400000)) { + const responseObj = cookie; + callback(responseObj); + } else { + const callbacks = { + success: response => { + let responseObj; + if (response) { + try { + responseObj = JSON.parse(response).adrcid; + } catch (error) { + logError(error); + } + let now = new Date(); + now.setTime(now.getTime() + 86400 * 1825 * 1000); + storage.setCookie('adrcid', responseObj, now.toUTCString(), 'Lax'); + storage.setDataInLocalStorage('adrcid', responseObj); + storage.setCookie('adrcid_cd', new Date().getTime(), now.toUTCString(), 'Lax'); + storage.setDataInLocalStorage('adrcid_cd', new Date().getTime()); + } + callback(responseObj); + }, + error: error => { + logError(`${MODULE_NAME}: ID fetch encountered an error`, error); + callback(); + } + }; + ajax(url, callbacks, undefined, {method: 'GET'}); + } + }; + return {callback: resp}; + } +}; + +submodule('userId', adriverIdSubmodule); diff --git a/modules/adriverIdSystem.md b/modules/adriverIdSystem.md new file mode 100644 index 00000000000..797318ba977 --- /dev/null +++ b/modules/adriverIdSystem.md @@ -0,0 +1,19 @@ +# Overview + +Module Name: AdRiver Id System +Module Type: User Id System +Maintainer: support@adriver.ru + +# Description + +Adriver user identification system + +## Example configuration for publishers: + +pbjs.setConfig({ + userSync: { + userIds: [{ + name: 'adriverId' + }] + } +}); \ No newline at end of file diff --git a/modules/adtargetBidAdapter.js b/modules/adtargetBidAdapter.js index 0ad0177815a..a07b0de0f67 100644 --- a/modules/adtargetBidAdapter.js +++ b/modules/adtargetBidAdapter.js @@ -1,8 +1,8 @@ -import { deepAccess, isArray, chunk, _map, flatten, logError, parseSizesInput } from '../src/utils.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER, VIDEO } from '../src/mediaTypes.js'; -import { config } from '../src/config.js'; -import find from 'core-js-pure/features/array/find.js'; +import {_map, chunk, deepAccess, flatten, isArray, logError, parseSizesInput} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {config} from '../src/config.js'; +import {find} from '../src/polyfill.js'; const ENDPOINT = 'https://ghb.console.adtarget.com.tr/v2/auction/'; const BIDDER_CODE = 'adtarget'; diff --git a/modules/adtelligentBidAdapter.js b/modules/adtelligentBidAdapter.js index 44a9c90d438..f309ed4e96e 100644 --- a/modules/adtelligentBidAdapter.js +++ b/modules/adtelligentBidAdapter.js @@ -1,9 +1,9 @@ -import { deepAccess, isArray, chunk, _map, flatten, convertTypes, parseSizesInput } from '../src/utils.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { ADPOD, BANNER, VIDEO } from '../src/mediaTypes.js'; -import { config } from '../src/config.js'; -import { Renderer } from '../src/Renderer.js'; -import find from 'core-js-pure/features/array/find.js'; +import {_map, chunk, convertTypes, deepAccess, flatten, isArray, parseSizesInput} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {ADPOD, BANNER, VIDEO} from '../src/mediaTypes.js'; +import {config} from '../src/config.js'; +import {Renderer} from '../src/Renderer.js'; +import {find} from '../src/polyfill.js'; const subdomainSuffixes = ['', 1, 2]; const AUCTION_PATH = '/v2/auction/'; @@ -18,7 +18,6 @@ const HOST_GETTERS = { navelix: () => 'ghb.hb.navelix.com', appaloosa: () => 'ghb.hb.appaloosa.media', onefiftytwomedia: () => 'ghb.ads.152media.com', - mediafuse: () => 'ghb.hbmp.mediafuse.com', bidsxchange: () => 'ghb.hbd.bidsxchange.com', streamkey: () => 'ghb.hb.streamkey.net', } @@ -37,11 +36,7 @@ export const spec = { code: BIDDER_CODE, gvlid: 410, aliases: ['onefiftytwomedia', 'selectmedia', 'appaloosa', 'bidsxchange', 'streamkey', - { code: 'navelix', gvlid: 380 }, - { - code: 'mediafuse', - skipPbsAliasing: true - } + { code: 'navelix', gvlid: 380 } ], supportedMediaTypes: [VIDEO, BANNER], isBidRequestValid: function (bid) { diff --git a/modules/adtrueBidAdapter.js b/modules/adtrueBidAdapter.js index df848fba823..283e1273150 100644 --- a/modules/adtrueBidAdapter.js +++ b/modules/adtrueBidAdapter.js @@ -4,8 +4,8 @@ import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; import {getStorageManager} from '../src/storageManager.js'; -const storage = getStorageManager(); const BIDDER_CODE = 'adtrue'; +const storage = getStorageManager({bidderCode: BIDDER_CODE}); const ADTRUE_CURRENCY = 'USD'; const ENDPOINT_URL = 'https://hb.adtrue.com/prebid/auction'; const LOG_WARN_PREFIX = 'AdTrue: '; diff --git a/modules/advangelistsBidAdapter.js b/modules/advangelistsBidAdapter.js index 854c65b1f22..605e19cfc66 100755 --- a/modules/advangelistsBidAdapter.js +++ b/modules/advangelistsBidAdapter.js @@ -1,9 +1,8 @@ -import { isEmpty, deepAccess, isFn, parseSizesInput, generateUUID, parseUrl } from '../src/utils.js'; -import { config } from '../src/config.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { VIDEO, BANNER } from '../src/mediaTypes.js'; -import find from 'core-js-pure/features/array/find.js'; -import includes from 'core-js-pure/features/array/includes.js'; +import {deepAccess, generateUUID, isEmpty, isFn, parseSizesInput, parseUrl} from '../src/utils.js'; +import {config} from '../src/config.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {find, includes} from '../src/polyfill.js'; const ADAPTER_VERSION = '1.0'; const BIDDER_CODE = 'advangelists'; diff --git a/modules/adxcgBidAdapter.js b/modules/adxcgBidAdapter.js index a02812a1608..81872100cd1 100644 --- a/modules/adxcgBidAdapter.js +++ b/modules/adxcgBidAdapter.js @@ -1,410 +1,362 @@ -import { logWarn, isStr, deepAccess, inIframe, checkCookieSupport, timestamp, getBidIdParameter, parseSizesInput, buildUrl, logMessage, isArray, deepSetValue, isPlainObject, triggerPixel, replaceAuctionPrice, isFn } from '../src/utils.js'; -import {config} from '../src/config.js' -import {registerBidder} from '../src/adapters/bidderFactory.js' -import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js' -import includes from 'core-js-pure/features/array/includes.js' - -/** - * Adapter for requesting bids from adxcg.net - * updated to latest prebid repo on 2017.10.20 - * updated for gdpr compliance on 2018.05.22 -requires gdpr compliance module - * updated to pass aditional auction and impression level parameters. added pass for video targeting parameters - * updated to fix native support for image width/height and icon 2019.03.17 - * updated support for userid - pubcid,ttid 2019.05.28 - * updated to support prebid 3.0 - remove non https, move to banner.xx.sizes, remove utils.getTopWindowLocation,remove utils.getTopWindowUrl(),remove utils.getTopWindowReferrer() - * updated to support prebid 4.0 - standardized video params, updated video validation, add onBidWon, onTimeOut, use standardized getFloor - */ - -const BIDDER_CODE = 'adxcg' -const SUPPORTED_AD_TYPES = [BANNER, VIDEO, NATIVE] -const SOURCE = 'pbjs10' -const VIDEO_TARGETING = ['id', 'minduration', 'maxduration', 'startdelay', 'skippable', 'playback_method', 'frameworks'] -const USER_PARAMS_AUCTION = ['forcedDspIds', 'forcedCampaignIds', 'forcedCreativeIds', 'gender', 'dnt', 'language'] -const USER_PARAMS_BID = ['lineparam1', 'lineparam2', 'lineparam3'] -const BIDADAPTERVERSION = 'r20210330PB40' -const DEFAULT_MIN_FLOOR = 0; +// jshint esversion: 6, es3: false, node: true +'use strict'; + +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {NATIVE, BANNER, VIDEO} from '../src/mediaTypes.js'; +import { + mergeDeep, + _map, + deepAccess, + getDNT, + parseSizesInput, + deepSetValue, + isStr, + isArray, + isPlainObject, + parseUrl, + replaceAuctionPrice, triggerPixel +} from '../src/utils.js'; +import {config} from '../src/config.js'; + +const { getConfig } = config; + +const BIDDER_CODE = 'adxcg'; +const SECURE_BID_URL = 'https://pbc.adxcg.net/rtb/ortb/pbc?adExchangeId=1'; + +const NATIVE_ASSET_IDS = { 0: 'title', 2: 'icon', 3: 'image', 5: 'sponsoredBy', 4: 'body', 1: 'cta' }; +const NATIVE_PARAMS = { + title: { + id: 0, + name: 'title' + }, + icon: { + id: 2, + type: 1, + name: 'img' + }, + image: { + id: 3, + type: 3, + name: 'img' + }, + sponsoredBy: { + id: 5, + name: 'data', + type: 1 + }, + body: { + id: 4, + name: 'data', + type: 2 + }, + cta: { + id: 1, + type: 12, + name: 'data' + } +}; export const spec = { code: BIDDER_CODE, - supportedMediaTypes: SUPPORTED_AD_TYPES, - - /** - * Determines whether or not the given bid request is valid. - * - * @param {object} bid The bid params to validate. - * @return boolean True if this is a valid bid, and false otherwise. - */ - isBidRequestValid: function (bid) { - if (!bid || !bid.params) { - logWarn(BIDDER_CODE + ': Missing bid parameters'); - return false - } + supportedMediaTypes: [ NATIVE, BANNER, VIDEO ], + isBidRequestValid: (bid) => { + const params = bid.params || {}; + const { adzoneid } = params; + return !!(adzoneid); + }, + buildRequests: (validBidRequests, bidderRequest) => { + let app, site; - if (!isStr(bid.params.adzoneid)) { - logWarn(BIDDER_CODE + ': adzoneid must be specified as a string'); - return false - } + const commonFpd = getConfig('ortb2') || {}; + let { user } = commonFpd; + + if (typeof getConfig('app') === 'object') { + app = getConfig('app') || {}; + if (commonFpd.app) { + mergeDeep(app, commonFpd.app); + } + } else { + site = getConfig('site') || {}; + if (commonFpd.site) { + mergeDeep(site, commonFpd.site); + } - if (isBannerRequest(bid)) { - const banneroAdUnit = deepAccess(bid, 'mediaTypes.banner'); - if (!banneroAdUnit.sizes) { - logWarn(BIDDER_CODE + ': banner sizes must be specified'); - return false; + if (!site.page) { + site.page = bidderRequest.refererInfo.referer; + site.domain = parseUrl(bidderRequest.refererInfo.referer).hostname; } } - if (isVideoRequest(bid)) { - // prebid 4.0 use standardized Video parameters - const videoAdUnit = deepAccess(bid, 'mediaTypes.video'); + const device = getConfig('device') || {}; + device.w = device.w || window.innerWidth; + device.h = device.h || window.innerHeight; + device.ua = device.ua || navigator.userAgent; + device.dnt = getDNT() ? 1 : 0; + device.language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; + + const tid = validBidRequests[0].transactionId; + const test = setOnAny(validBidRequests, 'params.test'); + const currency = getConfig('currency.adServerCurrency'); + const cur = currency && [ currency ]; + const eids = setOnAny(validBidRequests, 'userIdAsEids'); + const schain = setOnAny(validBidRequests, 'schain'); + + const imp = validBidRequests.map((bid, id) => { + const floorInfo = bid.getFloor ? bid.getFloor({ + currency: currency || 'USD' + }) : {}; + const bidfloor = floorInfo.floor; + const bidfloorcur = floorInfo.currency; + const { adzoneid } = bid.params; + + const imp = { + id: id + 1, + tagid: adzoneid, + secure: 1, + bidfloor, + bidfloorcur, + ext: { + } + }; - if (!Array.isArray(videoAdUnit.playerSize)) { - logWarn(BIDDER_CODE + ': video playerSize must be an array of integers'); - return false; - } + const assets = _map(bid.nativeParams, (bidParams, key) => { + const props = NATIVE_PARAMS[key]; + const asset = { + required: bidParams.required & 1, + }; + if (props) { + asset.id = props.id; + let wmin, hmin, w, h; + let aRatios = bidParams.aspect_ratios; + + if (aRatios && aRatios[0]) { + aRatios = aRatios[0]; + wmin = aRatios.min_width || 0; + hmin = aRatios.ratio_height * wmin / aRatios.ratio_width | 0; + } - if (!videoAdUnit.context) { - logWarn(BIDDER_CODE + ': video context must be specified'); - return false; - } + if (bidParams.sizes) { + const sizes = flatten(bidParams.sizes); + w = sizes[0]; + h = sizes[1]; + } - if (!Array.isArray(videoAdUnit.mimes) || videoAdUnit.mimes.length === 0) { - logWarn(BIDDER_CODE + ': video mimes must be an array of strings'); - return false; + asset[props.name] = { + len: bidParams.len, + type: props.type, + wmin, + hmin, + w, + h + }; + + return asset; + } + }).filter(Boolean); + + if (assets.length) { + imp.native = { + request: JSON.stringify({assets: assets}) + }; } - if (!Array.isArray(videoAdUnit.protocols) || videoAdUnit.protocols.length === 0) { - logWarn(BIDDER_CODE + ': video protocols must be an array of integers'); - return false; + const bannerParams = deepAccess(bid, 'mediaTypes.banner'); + + if (bannerParams && bannerParams.sizes) { + const sizes = parseSizesInput(bannerParams.sizes); + const format = sizes.map(size => { + const [ width, height ] = size.split('x'); + const w = parseInt(width, 10); + const h = parseInt(height, 10); + return { w, h }; + }); + + imp.banner = { + format + }; } - } - return true - }, + const videoParams = deepAccess(bid, 'mediaTypes.video'); + if (videoParams) { + imp.video = videoParams; + } - /** - * Make a server request from the list of BidRequests. - * - * an array of validBidRequests - * Info describing the request to the server. - */ - buildRequests: function (validBidRequests, bidderRequest) { - let dt = new Date(); - let ratio = window.devicePixelRatio || 1; - let iobavailable = window && window.IntersectionObserver && window.IntersectionObserverEntry && window.IntersectionObserverEntry.prototype && 'intersectionRatio' in window.IntersectionObserverEntry.prototype - - let bt = config.getConfig('bidderTimeout'); - if (window.PREBID_TIMEOUT) { - bt = Math.min(window.PREBID_TIMEOUT, bt); - } + return imp; + }); - let referrer = deepAccess(bidderRequest, 'refererInfo.referer'); - let page = deepAccess(bidderRequest, 'refererInfo.canonicalUrl') || config.getConfig('pageUrl') || deepAccess(window, 'location.href'); - - // add common parameters - let beaconParams = { - renderformat: 'javascript', - ver: BIDADAPTERVERSION, - secure: '1', - source: SOURCE, - uw: window.screen.width, - uh: window.screen.height, - dpr: ratio, - bt: bt, - isinframe: inIframe(), - cookies: checkCookieSupport() ? '1' : '0', - tz: dt.getTimezoneOffset(), - dt: timestamp(), - iob: iobavailable ? '1' : '0', - pbjs: '$prebid.version$', - rndid: Math.floor(Math.random() * (999999 - 100000 + 1)) + 100000, - ref: encodeURIComponent(referrer), - url: encodeURIComponent(page) + const request = { + id: bidderRequest.auctionId, + site, + app, + user, + geo: { utcoffset: new Date().getTimezoneOffset() }, + device, + source: { tid, fd: 1 }, + ext: { + prebid: { + channel: { + name: 'pbjs', + version: '$prebid.version$' + } + } + }, + cur, + imp }; - if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies) { - beaconParams.gdpr = bidderRequest.gdprConsent.gdprApplies ? '1' : '0'; - beaconParams.gdpr_consent = bidderRequest.gdprConsent.consentString; - } - - if (isStr(deepAccess(validBidRequests, '0.userId.pubcid'))) { - beaconParams.pubcid = validBidRequests[0].userId.pubcid; + if (test) { + request.is_debug = !!test; + request.test = 1; } - - if (isStr(deepAccess(validBidRequests, '0.userId.tdid'))) { - beaconParams.tdid = validBidRequests[0].userId.tdid; + if (deepAccess(bidderRequest, 'gdprConsent.gdprApplies') !== undefined) { + deepSetValue(request, 'user.ext.consent', bidderRequest.gdprConsent.consentString); + deepSetValue(request, 'regs.ext.gdpr', bidderRequest.gdprConsent.gdprApplies & 1); } - if (isStr(deepAccess(validBidRequests, '0.userId.id5id.uid'))) { - beaconParams.id5id = validBidRequests[0].userId.id5id.uid; + if (bidderRequest.uspConsent) { + deepSetValue(request, 'regs.ext.us_privacy', bidderRequest.uspConsent); } - if (isStr(deepAccess(validBidRequests, '0.userId.idl_env'))) { - beaconParams.idl_env = validBidRequests[0].userId.idl_env; + if (eids) { + deepSetValue(request, 'user.ext.eids', eids); } - let biddercustom = config.getConfig(BIDDER_CODE); - if (biddercustom) { - Object.keys(biddercustom) - .filter(param => includes(USER_PARAMS_AUCTION, param)) - .forEach(param => beaconParams[param] = encodeURIComponent(biddercustom[param])) + if (schain) { + deepSetValue(request, 'source.ext.schain', schain); } - // per impression parameters - let adZoneIds = []; - let prebidBidIds = []; - let sizes = []; - let bidfloors = []; - - validBidRequests.forEach((bid, index) => { - adZoneIds.push(getBidIdParameter('adzoneid', bid.params)); - prebidBidIds.push(bid.bidId); - - let bidfloor = getFloor(bid); - bidfloors.push(bidfloor); - - // copy all custom parameters impression level parameters not supported above - let customBidParams = getBidIdParameter('custom', bid.params) || {} - if (customBidParams) { - Object.keys(customBidParams) - .filter(param => includes(USER_PARAMS_BID, param)) - .forEach(param => beaconParams[param + '.' + index] = encodeURIComponent(customBidParams[param])) - } - - if (isBannerRequest(bid)) { - sizes.push(parseSizesInput(bid.mediaTypes.banner.sizes).join('|')); - } - - if (isNativeRequest(bid)) { - sizes.push('0x0'); - } - - if (isVideoRequest(bid)) { - if (bid.params.video) { - Object.keys(bid.params.video) - .filter(param => includes(VIDEO_TARGETING, param)) - .forEach(param => beaconParams['video.' + param + '.' + index] = encodeURIComponent(bid.params.video[param])) - } - // copy video standarized params - beaconParams['video.context' + '.' + index] = deepAccess(bid, 'mediaTypes.video.context'); - sizes.push(parseSizesInput(bid.mediaTypes.video.playerSize).join('|')); - beaconParams['video.mimes' + '.' + index] = deepAccess(bid, 'mediaTypes.video.mimes').join(','); - beaconParams['video.protocols' + '.' + index] = deepAccess(bid, 'mediaTypes.video.protocols').join(','); - } - }) - - beaconParams.adzoneid = adZoneIds.join(','); - beaconParams.format = sizes.join(','); - beaconParams.prebidBidIds = prebidBidIds.join(','); - beaconParams.bidfloors = bidfloors.join(','); - - let adxcgRequestUrl = buildUrl({ - protocol: 'https', - hostname: 'hbps.adxcg.net', - pathname: '/get/adi', - search: beaconParams - }); - - logMessage(`calling adi adxcg`); return { - contentType: 'text/plain', - method: 'GET', - url: adxcgRequestUrl, - withCredentials: true + method: 'POST', + url: SECURE_BID_URL, + data: JSON.stringify(request), + options: { + contentType: 'application/json' + }, + bids: validBidRequests }; }, - /** - * Unpack the response from the server into a list of bids. - * - * @param {*} serverResponse A successful response from the server. - * @return {bidRequests[]} An array of bids which were nested inside the server. - */ - interpretResponse: - function (serverResponse) { - logMessage(`interpretResponse adxcg`); - let bidsAll = []; - - if (!serverResponse || !serverResponse.body || !isArray(serverResponse.body.seatbid) || !serverResponse.body.seatbid.length) { - logWarn(BIDDER_CODE + ': empty bid response'); - return bidsAll; - } - - serverResponse.body.seatbid.forEach((bids) => { - bids.bid.forEach((serverResponseOneItem) => { - let bid = {} - // parse general fields - bid.requestId = serverResponseOneItem.impid; - bid.cpm = serverResponseOneItem.price; - bid.creativeId = parseInt(serverResponseOneItem.crid); - bid.currency = serverResponseOneItem.currency ? serverResponseOneItem.currency : 'USD'; - bid.netRevenue = serverResponseOneItem.netRevenue ? serverResponseOneItem.netRevenue : true; - bid.ttl = serverResponseOneItem.ttl ? serverResponseOneItem.ttl : 300; - bid.width = serverResponseOneItem.w; - bid.height = serverResponseOneItem.h; - bid.burl = serverResponseOneItem.burl || ''; - - if (serverResponseOneItem.dealid != null && serverResponseOneItem.dealid.trim().length > 0) { - bid.dealId = serverResponseOneItem.dealid; - } + interpretResponse: function(serverResponse, { bids }) { + if (!serverResponse.body) { + return; + } + const { seatbid, cur } = serverResponse.body; + + const bidResponses = flatten(seatbid.map(seat => seat.bid)).reduce((result, bid) => { + result[bid.impid - 1] = bid; + return result; + }, []); + + return bids.map((bid, id) => { + const bidResponse = bidResponses[id]; + if (bidResponse) { + const mediaType = deepAccess(bidResponse, 'ext.crType'); + const result = { + requestId: bid.bidId, + cpm: bidResponse.price, + creativeId: bidResponse.crid, + ttl: bidResponse.ttl ? bidResponse.ttl : 300, + netRevenue: bid.netRevenue === 'net', + currency: cur, + burl: bid.burl || '', + mediaType: mediaType, + width: bidResponse.w, + height: bidResponse.h, + dealId: bidResponse.dealid, + }; + + deepSetValue(result, 'meta.mediaType', mediaType); + if (isArray(bidResponse.adomain)) { + deepSetValue(result, 'meta.advertiserDomains', bidResponse.adomain); + } - if (serverResponseOneItem.ext.crType === 'banner') { - bid.ad = serverResponseOneItem.adm; - } else if (serverResponseOneItem.ext.crType === 'video') { - bid.vastUrl = serverResponseOneItem.nurl; - bid.vastXml = serverResponseOneItem.adm; - bid.mediaType = 'video'; - } else if (serverResponseOneItem.ext.crType === 'native') { - bid.mediaType = 'native'; - bid.native = parseNative(JSON.parse(serverResponseOneItem.adm)); - } else { - logWarn(BIDDER_CODE + ': unknown or undefined crType'); + if (isPlainObject(bidResponse.ext)) { + if (isStr(bidResponse.ext.mediaType)) { + deepSetValue(result, 'meta.mediaType', mediaType); } - - // prebid 4.0 meta taxonomy - if (isArray(serverResponseOneItem.adomain)) { - deepSetValue(bid, 'meta.advertiserDomains', serverResponseOneItem.adomain); + if (isStr(bidResponse.ext.advertiser_id)) { + deepSetValue(result, 'meta.advertiserId', bidResponse.ext.advertiser_id); } - if (isArray(serverResponseOneItem.cat)) { - deepSetValue(bid, 'meta.secondaryCatIds', serverResponseOneItem.cat); + if (isStr(bidResponse.ext.advertiser_name)) { + deepSetValue(result, 'meta.advertiserName', bidResponse.ext.advertiser_name); } - if (isPlainObject(serverResponseOneItem.ext)) { - if (isStr(serverResponseOneItem.ext.advertiser_id)) { - deepSetValue(bid, 'meta.mediaType', serverResponseOneItem.ext.mediaType); - } - if (isStr(serverResponseOneItem.ext.advertiser_id)) { - deepSetValue(bid, 'meta.advertiserId', serverResponseOneItem.ext.advertiser_id); - } - if (isStr(serverResponseOneItem.ext.advertiser_name)) { - deepSetValue(bid, 'meta.advertiserName', serverResponseOneItem.ext.advertiser_name); - } - if (isStr(serverResponseOneItem.ext.agency_name)) { - deepSetValue(bid, 'meta.agencyName', serverResponseOneItem.ext.agency_name); - } + if (isStr(bidResponse.ext.agency_name)) { + deepSetValue(result, 'meta.agencyName', bidResponse.ext.agency_name); } - bidsAll.push(bid) - }) - }) - return bidsAll - }, + } + if (mediaType === BANNER) { + result.ad = bidResponse.adm; + } else if (mediaType === NATIVE) { + result.native = parseNative(bidResponse); + result.width = 0; + result.height = 0; + } else if (mediaType === VIDEO) { + result.vastUrl = bidResponse.nurl; + result.vastXml = bidResponse.adm; + } - onBidWon: (bid) => { - if (bid.burl) { - triggerPixel(replaceAuctionPrice(bid.burl, bid.originalCpm)); - } + return result; + } + }).filter(Boolean); }, + getUserSyncs: (syncOptions, responses, gdprConsent, uspConsent) => { + const syncs = []; + let syncUrl = config.getConfig('adxcg.usersyncUrl'); + + let query = []; + if (syncOptions.pixelEnabled && syncUrl) { + if (gdprConsent) { + query.push('gdpr=' + (gdprConsent.gdprApplies & 1)); + query.push('gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || '')); + } + if (uspConsent) { + query.push('us_privacy=' + encodeURIComponent(uspConsent)); + } - onTimeout(timeoutData) { - if (timeoutData == null) { - return; + syncs.push({ + type: 'image', + url: syncUrl + (query.length ? '?' + query.join('&') : '') + }); } - - let beaconParams = { - A: timeoutData.bidder, - bid: timeoutData.bidId, - a: timeoutData.adUnitCode, - cn: timeoutData.timeout, - aud: timeoutData.auctionId, - }; - let adxcgRequestUrl = buildUrl({ - protocol: 'https', - hostname: 'hbps.adxcg.net', - pathname: '/event/timeout.gif', - search: beaconParams - }); - logWarn(BIDDER_CODE + ': onTimeout called'); - triggerPixel(adxcgRequestUrl); + return syncs; }, - - getUserSyncs: function (syncOptions, serverResponses, gdprConsent) { - let params = ''; - if (gdprConsent && 'gdprApplies' in gdprConsent) { - if (gdprConsent.consentString) { - if (typeof gdprConsent.gdprApplies === 'boolean') { - params += `?gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; - } else { - params += `?gdpr=0&gdpr_consent=${gdprConsent.consentString}`; - } - } - } - - if (syncOptions.iframeEnabled) { - return [{ - type: 'iframe', - url: 'https://cdn.adxcg.net/pb-sync.html' + params - }]; + onBidWon: (bid) => { + // for native requests we put the nurl as an imp tracker, otherwise if the auction takes place on prebid server + // the server JS adapter puts the nurl in the adm as a tracking pixel and removes the attribute + if (bid.nurl) { + triggerPixel(replaceAuctionPrice(bid.nurl, bid.originalCpm)) } } -} +}; -function isVideoRequest(bid) { - return bid.mediaType === 'video' || !!deepAccess(bid, 'mediaTypes.video'); -} +registerBidder(spec); -function isBannerRequest(bid) { - return bid.mediaType === 'banner' || !!deepAccess(bid, 'mediaTypes.banner'); -} - -function isNativeRequest(bid) { - return bid.mediaType === 'native' || !!deepAccess(bid, 'mediaTypes.native'); -} - -function getFloor(bid) { - if (!isFn(bid.getFloor)) { - return deepAccess(bid, 'params.floor', DEFAULT_MIN_FLOOR); - } - - try { - const floor = bid.getFloor({ - currency: 'EUR', - mediaType: '*', - size: '*', - bidRequest: bid - }); - return floor.floor; - } catch (e) { - logWarn(BIDDER_CODE + ': call to getFloor failed:' + e.message); - return DEFAULT_MIN_FLOOR; - } -} - -function parseNative(nativeResponse) { - let bidNative = {}; - bidNative = { - clickUrl: nativeResponse.link.url, - impressionTrackers: nativeResponse.imptrackers, - clickTrackers: nativeResponse.clktrackers, - javascriptTrackers: nativeResponse.jstrackers +function parseNative(bid) { + const { assets, link, imptrackers, jstracker } = JSON.parse(bid.adm); + const result = { + clickUrl: link.url, + clickTrackers: link.clicktrackers || undefined, + impressionTrackers: imptrackers || undefined, + javascriptTrackers: jstracker ? [ jstracker ] : undefined }; - - nativeResponse.assets.forEach(asset => { - if (asset.title && asset.title.text) { - bidNative.title = asset.title.text; - } - - if (asset.img && asset.img.url) { - bidNative.image = { - url: asset.img.url, - height: asset.img.h, - width: asset.img.w - }; - } - - if (asset.icon && asset.icon.url) { - bidNative.icon = { - url: asset.icon.url, - height: asset.icon.h, - width: asset.icon.w - }; - } - - if (asset.data && asset.data.label === 'DESC' && asset.data.value) { - bidNative.body = asset.data.value; + assets.forEach(asset => { + const kind = NATIVE_ASSET_IDS[asset.id]; + const content = kind && asset[NATIVE_PARAMS[kind].name]; + if (content) { + result[kind] = content.text || content.value || { url: content.url, width: content.w, height: content.h }; } + }); + return result; +} - if (asset.data && asset.data.label === 'SPONSORED' && asset.data.value) { - bidNative.sponsoredBy = asset.data.value; +function setOnAny(collection, key) { + for (let i = 0, result; i < collection.length; i++) { + result = deepAccess(collection[i], key); + if (result) { + return result; } - }) - return bidNative; + } } -registerBidder(spec) +function flatten(arr) { + return [].concat(...arr); +} diff --git a/modules/adxcgBidAdapter.md b/modules/adxcgBidAdapter.md index 8eccdb11dee..1e4ef9cd6f9 100644 --- a/modules/adxcgBidAdapter.md +++ b/modules/adxcgBidAdapter.md @@ -34,31 +34,34 @@ Module that connects to an Adxcg.com zone to fetch bids. code: 'native-ad-div', mediaTypes: { native: { - image: { + image: { sendId: false, - required: true, - sizes: [80, 80] + required: false, + sizes: [127, 83] }, icon: { - sendId: true, - }, - brand: { + sizes: [80, 80], + required: false, sendId: true, }, title: { sendId: false, - required: true, + required: false, len: 75 }, body: { sendId: false, - required: true, + required: false, len: 200 }, - sponsoredBy: { + cta: { sendId: false, required: false, - len: 20 + len: 75 + }, + sponsoredBy: { + sendId: false, + required: false } } }, @@ -73,21 +76,19 @@ Module that connects to an Adxcg.com zone to fetch bids. code: 'video-div', mediaTypes: { video: { - playerSize: [640, 480], - context: 'instream', - mimes: ['video/mp4'], - protocols: [5, 6, 8], - playback_method: ['auto_play_sound_off'] - } + playerSize: [640, 480], + context: 'instream', + mimes: ['video/mp4'], + protocols: [2, 3, 5, 6, 8], + playback_method: ['auto_play_sound_off'], + maxduration: 100, + skip: 1 + } }, bids: [{ bidder: 'adxcg', params: { - adzoneid: '20', - video: { - maxduration: 100, - skippable: true - } + adzoneid: '20' } }] } diff --git a/modules/adxpremiumAnalyticsAdapter.js b/modules/adxpremiumAnalyticsAdapter.js index 3e30de14052..9066c26fb00 100644 --- a/modules/adxpremiumAnalyticsAdapter.js +++ b/modules/adxpremiumAnalyticsAdapter.js @@ -1,9 +1,9 @@ -import { logError, logInfo, deepClone } from '../src/utils.js'; -import { ajax } from '../src/ajax.js'; +import {deepClone, logError, logInfo} from '../src/utils.js'; +import {ajax} from '../src/ajax.js'; import adapter from '../src/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; -import includes from 'core-js-pure/features/array/includes.js'; +import {includes} from '../src/polyfill.js'; const analyticsType = 'endpoint'; const defaultUrl = 'https://adxpremium.services/graphql'; diff --git a/modules/adyoulikeBidAdapter.js b/modules/adyoulikeBidAdapter.js index 334309aec5c..1a0074e668b 100644 --- a/modules/adyoulikeBidAdapter.js +++ b/modules/adyoulikeBidAdapter.js @@ -1,13 +1,15 @@ -import { deepAccess, buildUrl, parseSizesInput } from '../src/utils.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { config } from '../src/config.js'; -import find from 'core-js-pure/features/array/find.js'; +import {buildUrl, deepAccess, parseSizesInput} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {config} from '../src/config.js'; +import {createEidsArray} from './userId/eids.js'; +import {find} from '../src/polyfill.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; const VERSION = '1.0'; const BIDDER_CODE = 'adyoulike'; const DEFAULT_DC = 'hb-api'; const CURRENCY = 'USD'; +const GVLID = 259; const NATIVE_IMAGE = { image: { @@ -35,6 +37,7 @@ const NATIVE_IMAGE = { export const spec = { code: BIDDER_CODE, + gvlid: GVLID, supportedMediaTypes: [BANNER, NATIVE, VIDEO], aliases: ['ayl'], // short code /** @@ -58,6 +61,7 @@ export const spec = { * @return ServerRequest Info describing the request to the server. */ buildRequests: function (bidRequests, bidderRequest) { + let hasVideo = false; const payload = { Version: VERSION, Bids: bidRequests.reduce((accumulator, bidReq) => { @@ -85,6 +89,7 @@ export const spec = { accumulator[bidReq.bidId].Native = nativeReq; } if (mediatype === VIDEO) { + hasVideo = true; accumulator[bidReq.bidId].Video = bidReq.mediaTypes.video; const size = bidReq.mediaTypes.video.playerSize; @@ -97,17 +102,21 @@ export const spec = { PageRefreshed: getPageRefreshed() }; - if (bidderRequest && bidderRequest.gdprConsent) { + if (bidderRequest.gdprConsent) { payload.gdprConsent = { consentString: bidderRequest.gdprConsent.consentString, consentRequired: (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') ? bidderRequest.gdprConsent.gdprApplies : null }; } - if (bidderRequest && bidderRequest.uspConsent) { + if (bidderRequest.uspConsent) { payload.uspConsent = bidderRequest.uspConsent; } + if (deepAccess(bidderRequest, 'userId')) { + payload.userId = createEidsArray(bidderRequest.userId); + } + const data = JSON.stringify(payload); const options = { withCredentials: true @@ -115,7 +124,7 @@ export const spec = { return { method: 'POST', - url: createEndpoint(bidRequests, bidderRequest), + url: createEndpoint(bidRequests, bidderRequest, hasVideo), data, options }; @@ -175,11 +184,13 @@ function getCanonicalUrl() { /* Get mediatype from bidRequest */ function getMediatype(bidRequest) { + if (deepAccess(bidRequest, 'mediaTypes.banner')) { + return BANNER; + } if (deepAccess(bidRequest, 'mediaTypes.video')) { return VIDEO; - } else if (deepAccess(bidRequest, 'mediaTypes.banner')) { - return BANNER; - } else if (deepAccess(bidRequest, 'mediaTypes.native')) { + } + if (deepAccess(bidRequest, 'mediaTypes.native')) { return NATIVE; } } @@ -208,12 +219,13 @@ function getPageRefreshed() { } /* Create endpoint url */ -function createEndpoint(bidRequests, bidderRequest) { +function createEndpoint(bidRequests, bidderRequest, hasVideo) { let host = getHostname(bidRequests); + const endpoint = hasVideo ? '/hb-api/prebid-video/v1' : '/hb-api/prebid/v1'; return buildUrl({ protocol: 'https', host: `${DEFAULT_DC}${host}.omnitagjs.com`, - pathname: '/hb-api/prebid/v1', + pathname: endpoint, search: createEndpointQS(bidderRequest) }); } @@ -340,7 +352,7 @@ function getTrackers(eventsArray, jsTrackers) { function getVideoAd(response) { var adJson = {}; - if (typeof response.Ad === 'string') { + if (typeof response.Ad === 'string' && response.Ad.indexOf('\/\*PREBID\*\/') > 0) { adJson = JSON.parse(response.Ad.match(/\/\*PREBID\*\/(.*)\/\*PREBID\*\//)[1]); return deepAccess(adJson, 'Content.MainVideo.Vast'); } @@ -424,7 +436,7 @@ function getNativeAssets(response, nativeConfig) { const icurl = getImageUrl(adJson, deepAccess(adJson, 'Content.Preview.Sponsor.Logo.Resource'), iconSize[0], iconSize[1]); - if (url) { + if (icurl) { native[key] = { url: icurl, width: iconSize[0], @@ -473,13 +485,15 @@ function createBid(response, bidRequests) { meta: response.Meta || { advertiserDomains: [] } }; - if (request && request.Native) { + // retreive video response if present + const vast64 = response.Vast || getVideoAd(response); + if (vast64) { + bid.vastXml = window.atob(vast64); + bid.mediaType = 'video'; + } else if (request.Native) { + // format Native response if Native was requested bid.native = getNativeAssets(response, request.Native); bid.mediaType = 'native'; - } else if (request && request.Video) { - const vast64 = response.Vast || getVideoAd(response); - bid.vastXml = vast64 ? window.atob(vast64) : ''; - bid.mediaType = 'video'; } else { bid.width = response.Width; bid.height = response.Height; diff --git a/modules/afpBidAdapter.js b/modules/afpBidAdapter.js index 68941ff17c9..6565942bcc8 100644 --- a/modules/afpBidAdapter.js +++ b/modules/afpBidAdapter.js @@ -1,7 +1,7 @@ -import includes from 'core-js-pure/features/array/includes.js' -import { registerBidder } from '../src/adapters/bidderFactory.js' -import { Renderer } from '../src/Renderer.js' -import { BANNER, VIDEO } from '../src/mediaTypes.js' +import {includes} from '../src/polyfill.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {Renderer} from '../src/Renderer.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; export const IS_DEV = location.hostname === 'localhost' export const BIDDER_CODE = 'afp' diff --git a/modules/airgridRtdProvider.js b/modules/airgridRtdProvider.js index f5403cca3eb..b2e78a7df78 100644 --- a/modules/airgridRtdProvider.js +++ b/modules/airgridRtdProvider.js @@ -16,7 +16,7 @@ const SUBMODULE_NAME = 'airgrid'; const AG_TCF_ID = 782; export const AG_AUDIENCE_IDS_KEY = 'edkt_matched_audience_ids' -export const storage = getStorageManager(AG_TCF_ID, SUBMODULE_NAME); +export const storage = getStorageManager({gvlid: AG_TCF_ID, moduleName: SUBMODULE_NAME}); /** * Attach script tag to DOM diff --git a/modules/akamaiDapRtdProvider.js b/modules/akamaiDapRtdProvider.js index d143a53fbf4..aca984d39c8 100644 --- a/modules/akamaiDapRtdProvider.js +++ b/modules/akamaiDapRtdProvider.js @@ -15,7 +15,7 @@ const MODULE_NAME = 'realTimeData'; const SUBMODULE_NAME = 'dap'; export const SEGMENTS_STORAGE_KEY = 'akamaiDapSegments'; -export const storage = getStorageManager(null, SUBMODULE_NAME); +export const storage = getStorageManager({gvlid: null, moduleName: SUBMODULE_NAME}); /** * Lazy merge objects. diff --git a/modules/amxBidAdapter.js b/modules/amxBidAdapter.js index d48245e9604..d1754936d7f 100644 --- a/modules/amxBidAdapter.js +++ b/modules/amxBidAdapter.js @@ -5,7 +5,7 @@ import { config } from '../src/config.js'; import { getStorageManager } from '../src/storageManager.js'; const BIDDER_CODE = 'amx'; -const storage = getStorageManager(737, BIDDER_CODE); +const storage = getStorageManager({gvlid: 737, bidderCode: BIDDER_CODE}); const SIMPLE_TLD_TEST = /\.com?\.\w{2,4}$/; const DEFAULT_ENDPOINT = 'https://prebid.a-mo.net/a/c'; const VERSION = 'pba1.3.1'; diff --git a/modules/aniviewBidAdapter.js b/modules/aniviewBidAdapter.js index 96bdf153e3f..7760aa2b47b 100644 --- a/modules/aniviewBidAdapter.js +++ b/modules/aniviewBidAdapter.js @@ -309,7 +309,7 @@ function getUserSyncs(syncOptions, serverResponses) { export const spec = { code: BIDDER_CODE, gvlid: GVLID, - aliases: ['avantisvideo', 'selectmediavideo', 'vidcrunch', 'openwebvideo'], + aliases: ['avantisvideo', 'selectmediavideo', 'vidcrunch', 'openwebvideo', 'didnavideo', 'ottadvisors'], supportedMediaTypes: [VIDEO, BANNER], isBidRequestValid, buildRequests, diff --git a/modules/appnexusBidAdapter.js b/modules/appnexusBidAdapter.js index c02eeccaea6..2758fc2d03a 100644 --- a/modules/appnexusBidAdapter.js +++ b/modules/appnexusBidAdapter.js @@ -1,13 +1,37 @@ -import { convertCamelToUnderscore, isArray, isNumber, isPlainObject, logError, logInfo, deepAccess, logMessage, convertTypes, isStr, getParameterByName, deepClone, chunk, logWarn, getBidRequest, createTrackPixelHtml, isEmpty, transformBidderParamKeywords, getMaxValueFromArray, fill, getMinValueFromArray, isArrayOfNums, isFn } from '../src/utils.js'; -import { Renderer } from '../src/Renderer.js'; -import { config } from '../src/config.js'; -import { registerBidder, getIabSubCategory } from '../src/adapters/bidderFactory.js'; -import { BANNER, NATIVE, VIDEO, ADPOD } from '../src/mediaTypes.js'; -import { auctionManager } from '../src/auctionManager.js'; -import find from 'core-js-pure/features/array/find.js'; -import includes from 'core-js-pure/features/array/includes.js'; -import { OUTSTREAM, INSTREAM } from '../src/video.js'; -import { getStorageManager } from '../src/storageManager.js'; +import { + chunk, + convertCamelToUnderscore, + convertTypes, + createTrackPixelHtml, + deepAccess, + deepClone, + fill, + getBidRequest, + getMaxValueFromArray, + getMinValueFromArray, + getParameterByName, + isArray, + isArrayOfNums, + isEmpty, + isFn, + isNumber, + isPlainObject, + isStr, + logError, + logInfo, + logMessage, + logWarn, + transformBidderParamKeywords +} from '../src/utils.js'; +import {Renderer} from '../src/Renderer.js'; +import {config} from '../src/config.js'; +import {getIabSubCategory, registerBidder} from '../src/adapters/bidderFactory.js'; +import {ADPOD, BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; +import {auctionManager} from '../src/auctionManager.js'; +import {find, includes} from '../src/polyfill.js'; +import {INSTREAM, OUTSTREAM} from '../src/video.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {bidderSettings} from '../src/bidderSettings.js'; const BIDDER_CODE = 'appnexus'; const URL = 'https://ib.adnxs.com/ut/v3/prebid'; @@ -60,7 +84,7 @@ const SCRIPT_TAG_START = ' 0) { + aucKeywords.forEach(deleteValues); + } + + payload.keywords = aucKeywords; + } + if (config.getConfig('adpod.brandCategoryExclusion')) { payload.brand_category_uniqueness = true; } @@ -259,6 +293,13 @@ export const spec = { addUserId(eids, deepAccess(bidRequests[0], `userId.idl_env`), 'liveramp.com', null); addUserId(eids, deepAccess(bidRequests[0], `userId.tdid`), 'adserver.org', 'TDID'); addUserId(eids, deepAccess(bidRequests[0], `userId.uid2.id`), 'uidapi.com', 'UID2'); + if (bidRequests[0].userId.pubProvidedId) { + bidRequests[0].userId.pubProvidedId.forEach(ppId => { + ppId.uids.forEach(uid => { + eids.push({ source: ppId.source, id: uid.id }); + }); + }); + } if (eids.length) { payload.eids = eids; @@ -293,7 +334,8 @@ export const spec = { serverResponse.tags.forEach(serverBid => { const rtbBid = getRtbBid(serverBid); if (rtbBid) { - if (rtbBid.cpm !== 0 && includes(this.supportedMediaTypes, rtbBid.ad_type)) { + const cpmCheck = (bidderSettings.get(bidderRequest.bidderCode, 'allowZeroCpmBids') === true) ? rtbBid.cpm >= 0 : rtbBid.cpm > 0; + if (cpmCheck && includes(this.supportedMediaTypes, rtbBid.ad_type)) { const bid = newBid(serverBid, rtbBid, bidderRequest); bid.mediaType = parseMediaType(rtbBid); bids.push(bid); @@ -348,11 +390,20 @@ export const spec = { }, transformBidParams: function (params, isOpenRtb) { + let conversionFn = transformBidderParamKeywords; + if (isOpenRtb === true) { + let s2sConfig = config.getConfig('s2sConfig'); + let s2sEndpointUrl = deepAccess(s2sConfig, 'endpoint.p1Consent'); + if (s2sEndpointUrl && s2sEndpointUrl.match('/openrtb2/prebid')) { + conversionFn = convertKeywordsToString; + } + } + params = convertTypes({ 'member': 'string', 'invCode': 'string', 'placementId': 'number', - 'keywords': transformBidderParamKeywords, + 'keywords': conversionFn, 'publisherId': 'number' }, params); @@ -598,6 +649,22 @@ function newBid(serverBid, rtbBid, bidderRequest) { bid.meta = Object.assign({}, bid.meta, { advertiserId: rtbBid.advertiser_id }); } + // temporary function; may remove at later date if/when adserver fully supports dchain + function setupDChain(rtbBid) { + let dchain = { + ver: '1.0', + complete: 0, + nodes: [{ + bsid: rtbBid.buyer_member_id.toString() + }], + }; + + return dchain; + } + if (rtbBid.buyer_member_id) { + bid.meta = Object.assign({}, bid.meta, {dchain: setupDChain(rtbBid)}); + } + if (rtbBid.brand_id) { bid.meta = Object.assign({}, bid.meta, { brandId: rtbBid.brand_id }); } @@ -1129,4 +1196,31 @@ function getBidFloor(bid) { return null; } +// keywords: { 'genre': ['rock', 'pop'], 'pets': ['dog'] } goes to 'genre=rock,genre=pop,pets=dog' +function convertKeywordsToString(keywords) { + let result = ''; + Object.keys(keywords).forEach(key => { + // if 'text' or '' + if (isStr(keywords[key])) { + if (keywords[key] !== '') { + result += `${key}=${keywords[key]},` + } else { + result += `${key},`; + } + } else if (isArray(keywords[key])) { + if (keywords[key][0] === '') { + result += `${key},` + } else { + keywords[key].forEach(val => { + result += `${key}=${val},` + }); + } + } + }); + + // remove last trailing comma + result = result.substring(0, result.length - 1); + return result; +} + registerBidder(spec); diff --git a/modules/apstreamBidAdapter.js b/modules/apstreamBidAdapter.js index f2d4189f237..b69fffb8b6b 100644 --- a/modules/apstreamBidAdapter.js +++ b/modules/apstreamBidAdapter.js @@ -8,7 +8,7 @@ const CONSTANTS = { BIDDER_CODE: 'apstream', GVLID: 394 }; -const storage = getStorageManager(CONSTANTS.GVLID, CONSTANTS.BIDDER_CODE); +const storage = getStorageManager({gvlid: CONSTANTS.GVLID, bidderCode: CONSTANTS.BIDDER_CODE}); var dsuModule = (function() { 'use strict'; diff --git a/modules/asealBidAdapter.js b/modules/asealBidAdapter.js new file mode 100644 index 00000000000..559afefa94b --- /dev/null +++ b/modules/asealBidAdapter.js @@ -0,0 +1,58 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; + +export const BIDDER_CODE = 'aseal'; +const SUPPORTED_AD_TYPES = [BANNER]; +export const API_ENDPOINT = 'https://tkprebid.aotter.net/prebid/adapter'; +export const HEADER_AOTTER_VERSION = 'prebid_0.0.1'; + +export const spec = { + code: BIDDER_CODE, + aliases: ['aotter', 'trek'], + supportedMediaTypes: SUPPORTED_AD_TYPES, + + isBidRequestValid: (bid) => !!bid.params.placeUid && typeof bid.params.placeUid === 'string', + + buildRequests: (validBidRequests, bidderRequest) => { + if (validBidRequests.length === 0) { + return []; + } + + const clientId = + config.getConfig('aseal.clientId') || ''; + + const data = { + bids: validBidRequests, + refererInfo: bidderRequest.refererInfo, + }; + + const options = { + contentType: 'application/json', + withCredentials: true, + customHeaders: { + 'x-aotter-clientid': clientId, + 'x-aotter-version': HEADER_AOTTER_VERSION, + }, + }; + + return [{ + method: 'POST', + url: API_ENDPOINT, + data, + options, + }]; + }, + + interpretResponse: (serverResponse, bidRequest) => { + if (!Array.isArray(serverResponse.body)) { + return []; + } + + const bidResponses = serverResponse.body; + + return bidResponses; + }, +}; + +registerBidder(spec); diff --git a/modules/asealBidAdapter.md b/modules/asealBidAdapter.md new file mode 100644 index 00000000000..d13b802f736 --- /dev/null +++ b/modules/asealBidAdapter.md @@ -0,0 +1,52 @@ +# Overview + +``` +Module Name: Aseal Bid Adapter +Module Type: Bidder Adapter +Maintainer: tech-service@aotter.net +``` + +# Description + +Module that connects to Aseal server for bids. +Supported Ad Formats: + +- Banner + +# Configuration + +Following configuration is required: + +```js +pbjs.setConfig({ + aseal: { + clientId: "YOUR_CLIENT_ID" + } +}); +``` + +# Ad Unit Example + +```js +var adUnits = [ + { + code: "banner-div", + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600] + ] + } + }, + bids: [ + { + bidder: "aseal", + params: { + placeUid: "f4a74f73-9a74-4a87-91c9-545c6316c23d" + } + } + ] + } +]; +``` diff --git a/modules/automatadBidAdapter.js b/modules/automatadBidAdapter.js index 2cfcfbe98b4..726bbef9bd6 100644 --- a/modules/automatadBidAdapter.js +++ b/modules/automatadBidAdapter.js @@ -5,7 +5,7 @@ import {ajax} from '../src/ajax.js' const BIDDER = 'automatad' -const ENDPOINT_URL = 'https://rtb2.automatad.com/ortb2' +const ENDPOINT_URL = 'https://bid.atmtd.com' const DEFAULT_BID_TTL = 30 const DEFAULT_CURRENCY = 'USD' @@ -57,7 +57,7 @@ export const spec = { const payloadString = JSON.stringify(openrtbRequest) return { method: 'POST', - url: ENDPOINT_URL + '/resp', + url: ENDPOINT_URL + '/request', data: payloadString, options: { contentType: 'application/json', @@ -72,6 +72,7 @@ export const spec = { const response = (serverResponse || {}).body if (response && response.seatbid && response.seatbid[0].bid && response.seatbid[0].bid.length) { + var bidid = response.bidid response.seatbid.forEach(bidObj => { bidObj.bid.forEach(bid => { bidResponses.push({ @@ -88,6 +89,7 @@ export const spec = { height: bid.h, netRevenue: DEFAULT_NET_REVENUE, nurl: bid.nurl, + bidId: bidid }) }) }) @@ -97,11 +99,9 @@ export const spec = { return bidResponses }, - getUserSyncs: function(syncOptions, serverResponse) { - return [{ - type: 'iframe', - url: 'https://rtb2.automatad.com/ortb2/async_usersync' - }] + onTimeout: function(timeoutData) { + const timeoutUrl = ENDPOINT_URL + '/timeout' + ajax(timeoutUrl, null, JSON.stringify(timeoutData)) }, onBidWon: function(bid) { if (!bid.nurl) { return } @@ -116,6 +116,9 @@ export const spec = { ).replace( /\$\{AUCTION_CURRENCY\}/, winCurr + ).replace( + /\$\{AUCTON_BID_ID\}/, + bid.bidId ).replace( /\$\{AUCTION_ID\}/, bid.auctionId diff --git a/modules/axonixBidAdapter.js b/modules/axonixBidAdapter.js index 7cd8f63bd2a..a790a89a0c1 100644 --- a/modules/axonixBidAdapter.js +++ b/modules/axonixBidAdapter.js @@ -177,7 +177,7 @@ export const spec = { const { nurl } = bid || {}; if (bid.nurl) { - triggerPixel(replaceAuctionPrice(nurl, bid.cpm)); + triggerPixel(replaceAuctionPrice(nurl, bid.originalCpm || bid.cpm)); }; } } diff --git a/modules/beachfrontBidAdapter.js b/modules/beachfrontBidAdapter.js index a882a796851..1c341e4dc51 100644 --- a/modules/beachfrontBidAdapter.js +++ b/modules/beachfrontBidAdapter.js @@ -1,12 +1,21 @@ -import { logWarn, deepAccess, isArray, parseSizesInput, isFn, parseUrl, getUniqueIdentifierStr } from '../src/utils.js'; -import { config } from '../src/config.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { Renderer } from '../src/Renderer.js'; -import { VIDEO, BANNER } from '../src/mediaTypes.js'; -import find from 'core-js-pure/features/array/find.js'; -import includes from 'core-js-pure/features/array/includes.js'; - -const ADAPTER_VERSION = '1.18'; +import { + deepAccess, + deepClone, + deepSetValue, + getUniqueIdentifierStr, + isArray, + isFn, + logWarn, + parseSizesInput, + parseUrl +} from '../src/utils.js'; +import {config} from '../src/config.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {Renderer} from '../src/Renderer.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {find, includes} from '../src/polyfill.js'; + +const ADAPTER_VERSION = '1.19'; const ADAPTER_NAME = 'BFIO_PREBID'; const OUTSTREAM = 'outstream'; const CURRENCY = 'USD'; @@ -360,6 +369,7 @@ function createVideoRequestData(bid, bidderRequest) { let tagid = getVideoBidParam(bid, 'tagid'); let topLocation = getTopWindowLocation(bidderRequest); let eids = getEids(bid); + let ortb2 = deepClone(config.getConfig('ortb2')); let payload = { isPrebid: true, appId: appId, @@ -378,6 +388,7 @@ function createVideoRequestData(bid, bidderRequest) { displaymanagerver: ADAPTER_VERSION }], site: { + ...deepAccess(ortb2, 'site', {}), page: topLocation.href, domain: topLocation.hostname }, @@ -389,39 +400,32 @@ function createVideoRequestData(bid, bidderRequest) { js: 1, geo: {} }, - regs: { - ext: {} - }, - source: { - ext: {} - }, - user: { - ext: {} - }, + app: deepAccess(ortb2, 'app'), + user: deepAccess(ortb2, 'user'), cur: [CURRENCY] }; if (bidderRequest && bidderRequest.uspConsent) { - payload.regs.ext.us_privacy = bidderRequest.uspConsent; + deepSetValue(payload, 'regs.ext.us_privacy', bidderRequest.uspConsent); } if (bidderRequest && bidderRequest.gdprConsent) { let { gdprApplies, consentString } = bidderRequest.gdprConsent; - payload.regs.ext.gdpr = gdprApplies ? 1 : 0; - payload.user.ext.consent = consentString; + deepSetValue(payload, 'regs.ext.gdpr', gdprApplies ? 1 : 0); + deepSetValue(payload, 'user.ext.consent', consentString); } if (bid.schain) { - payload.source.ext.schain = bid.schain; + deepSetValue(payload, 'source.ext.schain', bid.schain); } if (eids.length > 0) { - payload.user.ext.eids = eids; + deepSetValue(payload, 'user.ext.eids', eids); } let connection = navigator.connection || navigator.webkitConnection; if (connection && connection.effectiveType) { - payload.device.connectiontype = connection.effectiveType; + deepSetValue(payload, 'device.connectiontype', connection.effectiveType); } return payload; @@ -439,8 +443,10 @@ function createBannerRequestData(bids, bidderRequest) { sizes: getBannerSizes(bid) }; }); + let ortb2 = deepClone(config.getConfig('ortb2')); let payload = { slots: slots, + ortb2: ortb2, page: topLocation.href, domain: topLocation.hostname, search: topLocation.search, diff --git a/modules/beopBidAdapter.js b/modules/beopBidAdapter.js index a6bc8a5687d..2e74170fcaf 100644 --- a/modules/beopBidAdapter.js +++ b/modules/beopBidAdapter.js @@ -36,7 +36,7 @@ export const spec = { */ buildRequests: function(validBidRequests, bidderRequest) { const slots = validBidRequests.map(beOpRequestSlotsMaker); - let pageUrl = deepAccess(bidderRequest, 'refererInfo.canonicalUrl') || config.getConfig('pageUrl') || deepAccess(window, 'location.href'); + let pageUrl = deepAccess(window, 'location.href') || deepAccess(bidderRequest, 'refererInfo.canonicalUrl') || config.getConfig('pageUrl'); let fpd = config.getLegacyFpd(config.getConfig('ortb2')); let gdpr = bidderRequest.gdprConsent; let firstSlot = slots[0]; @@ -99,16 +99,18 @@ export const spec = { } function buildTrackingParams(data, info, value) { + const accountId = data.params.accountId; return { - pid: data.params.accountId, + pid: accountId === undefined ? data.ad.match(/account: \“([a-f\d]{24})\“/)[1] : accountId, nid: data.params.networkId, nptnid: data.params.networkPartnerId, - bid: data.bidId, + bid: data.bidId || data.requestId, sl_n: data.adUnitCode, aid: data.auctionId, se_ca: 'bid', se_ac: info, - se_va: value + se_va: value, + url: window.location.href }; } diff --git a/modules/betweenBidAdapter.js b/modules/betweenBidAdapter.js index b2f63488e12..04dccf563e6 100644 --- a/modules/betweenBidAdapter.js +++ b/modules/betweenBidAdapter.js @@ -1,12 +1,12 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import { getAdUnitSizes, parseSizesInput } from '../src/utils.js'; import { getRefererInfo } from '../src/refererDetection.js'; +import {includes} from '../src/polyfill.js' const BIDDER_CODE = 'between'; let ENDPOINT = 'https://ads.betweendigital.com/adjson?t=prebid'; const CODE_TYPES = ['inpage', 'preroll', 'midroll', 'postroll']; -const includes = require('core-js-pure/features/array/includes.js'); export const spec = { code: BIDDER_CODE, aliases: ['btw'], diff --git a/modules/bidViewability.js b/modules/bidViewability.js index c3b72cda8d4..837eccd00c1 100644 --- a/modules/bidViewability.js +++ b/modules/bidViewability.js @@ -2,13 +2,13 @@ // GPT API is used to find when a bid is viewable, https://developers.google.com/publisher-tag/reference#googletag.events.impressionviewableevent // Does not work with other than GPT integration -import { config } from '../src/config.js'; +import {config} from '../src/config.js'; import * as events from '../src/events.js'; -import { EVENTS } from '../src/constants.json'; -import { logWarn, isFn, triggerPixel } from '../src/utils.js'; -import { getGlobal } from '../src/prebidGlobal.js'; -import adapterManager, { gdprDataHandler, uspDataHandler } from '../src/adapterManager.js'; -import find from 'core-js-pure/features/array/find.js'; +import CONSTANTS from '../src/constants.json'; +import {isFn, logWarn, triggerPixel} from '../src/utils.js'; +import {getGlobal} from '../src/prebidGlobal.js'; +import adapterManager, {gdprDataHandler, uspDataHandler} from '../src/adapterManager.js'; +import {find} from '../src/polyfill.js'; const MODULE_NAME = 'bidViewability'; const CONFIG_ENABLED = 'enabled'; @@ -70,12 +70,12 @@ export let impressionViewableHandler = (globalModuleConfig, slot, event) => { // trigger respective bidder's onBidViewable handler adapterManager.callBidViewableBidder(respectiveBid.bidder, respectiveBid); // emit the BID_VIEWABLE event with bid details, this event can be consumed by bidders and analytics pixels - events.emit(EVENTS.BID_VIEWABLE, respectiveBid); + events.emit(CONSTANTS.EVENTS.BID_VIEWABLE, respectiveBid); } }; export let init = () => { - events.on(EVENTS.AUCTION_INIT, () => { + events.on(CONSTANTS.EVENTS.AUCTION_INIT, () => { // read the config for the module const globalModuleConfig = config.getConfig(MODULE_NAME) || {}; // do nothing if module-config.enabled is not set to true diff --git a/modules/bidViewabilityIO.js b/modules/bidViewabilityIO.js index d936fb4aeec..ff7ec70e32c 100644 --- a/modules/bidViewabilityIO.js +++ b/modules/bidViewabilityIO.js @@ -1,7 +1,7 @@ import { logMessage } from '../src/utils.js'; import { config } from '../src/config.js'; import * as events from '../src/events.js'; -import { EVENTS } from '../src/constants.json'; +import CONSTANTS from '../src/constants.json'; const MODULE_NAME = 'bidViewabilityIO'; const CONFIG_ENABLED = 'enabled'; @@ -42,7 +42,7 @@ export let getViewableOptions = (bid) => { export let markViewed = (bid, entry, observer) => { return () => { observer.unobserve(entry.target); - events.emit(EVENTS.BID_VIEWABLE, bid); + events.emit(CONSTANTS.EVENTS.BID_VIEWABLE, bid); _logMessage(`id: ${entry.target.getAttribute('id')} code: ${bid.adUnitCode} was viewed`); } } @@ -77,7 +77,7 @@ export let init = () => { if (conf[MODULE_NAME][CONFIG_ENABLED] && CLIENT_SUPPORTS_IO) { // if the module is enabled and the browser supports Intersection Observer, // then listen to AD_RENDER_SUCCEEDED to setup IO's for supported mediaTypes - events.on(EVENTS.AD_RENDER_SUCCEEDED, ({doc, bid, id}) => { + events.on(CONSTANTS.EVENTS.AD_RENDER_SUCCEEDED, ({doc, bid, id}) => { if (isSupportedMediaType(bid)) { let viewable = new IntersectionObserver(viewCallbackFactory(bid), getViewableOptions(bid)); let element = document.getElementById(bid.adUnitCode); diff --git a/modules/big-richmediaBidAdapter.js b/modules/big-richmediaBidAdapter.js new file mode 100644 index 00000000000..cd8b2462eb8 --- /dev/null +++ b/modules/big-richmediaBidAdapter.js @@ -0,0 +1,117 @@ +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {config} from '../src/config.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {spec as baseAdapter} from './appnexusBidAdapter.js'; // eslint-disable-line prebid/validate-imports + +const BIDDER_CODE = 'big-richmedia'; + +const metadataByRequestId = {}; + +export const spec = { + version: '1.4.0', + code: BIDDER_CODE, + gvlid: baseAdapter.GVLID, // use base adapter gvlid + supportedMediaTypes: [ BANNER, VIDEO ], + + /** + * Determines whether or not the given bid request is valid. + * + * @param {object} bid The bid to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (bid) { + if (!baseAdapter.isBidRequestValid) { return true; } + return baseAdapter.isBidRequestValid(bid); + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} bidRequests A non-empty list of bid requests which should be sent to the Server. + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function (bidRequests, bidderRequest) { + if (!baseAdapter.buildRequests) { return []; } + + const publisherId = config.getConfig('bigRichmedia.publisherId'); + if (typeof publisherId !== 'string') { return []; } + + bidRequests.forEach(bidRequest => { + if (bidRequest.params.format === 'skin' && bidRequest.mediaTypes.banner) { + bidRequest.mediaTypes.banner.sizes.push([1800, 1000]); + } + metadataByRequestId[bidRequest.bidId] = { placementId: bidRequest.adUnitCode, bidder: bidRequest.bidder }; + }); + return baseAdapter.buildRequests(bidRequests, bidderRequest); + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function (serverResponse, params) { + const publisherId = config.getConfig('bigRichmedia.publisherId'); + if (typeof publisherId !== 'string') { return []; } + + const bids = baseAdapter.interpretResponse(serverResponse, params); + bids.forEach(bid => { + const { placementId, bidder } = metadataByRequestId[bid.requestId] || {}; + const { width = 1, height = 1, ad, creativeId = '', cpm, vastXml, vastUrl } = bid; + const bidRequest = params.bidderRequest.bids.find(({ bidId }) => bidId === bid.requestId); + const format = (bidRequest && bidRequest.params && bidRequest.params.format) || 'video-sticky-footer'; + const isReplayable = bidRequest && bidRequest.params && bidRequest.params.isReplayable; + const customSelector = bidRequest && bidRequest.params && bidRequest.params.customSelector; + const renderParams = { + adm: ad, + vastXml, + vastUrl, + width, + height, + placementId, + bidId: bid.requestId, + creativeId: `${creativeId}`, + bidder, + cpm, + format, + customSelector, + isReplayable + }; + const encoded = window.btoa(JSON.stringify(renderParams)); + bid.ad = ` + `; + + if (bid.mediaType !== 'banner') { // in case this is a video + bid.mediaType = 'banner'; + delete bid.renderer; + delete bid.vastUrl; + delete bid.vastXml; + bid.width = 1; + bid.height = 1; + } + }); + return bids; + }, + + getUserSyncs: function (syncOptions, responses, gdprConsent) { + if (!baseAdapter.getUserSyncs) { return []; } + return baseAdapter.getUserSyncs(syncOptions, responses, gdprConsent); + }, + + transformBidParams: function (params, isOpenRtb) { + if (!baseAdapter.transformBidParams) { return params; } + return baseAdapter.transformBidParams(params, isOpenRtb); + }, + + /** + * Add element selector to javascript tracker to improve native viewability + * @param {Bid} bid + */ + onBidWon: function (bid) { + if (!baseAdapter.onBidWon) { return; } + baseAdapter.onBidWon(bid); + } +} + +registerBidder(spec); diff --git a/modules/big-richmediaBidAdapter.md b/modules/big-richmediaBidAdapter.md new file mode 100644 index 00000000000..26f77e527fb --- /dev/null +++ b/modules/big-richmediaBidAdapter.md @@ -0,0 +1,82 @@ +# Overview + +``` +Module Name: BI.Garage Rich Media +Module Type: Bidder Adapter +Maintainer: mediaconsortium-develop@bi.garage.co.jp +``` + +# Description + +Module which renders richmedia demand from a Xandr seat + +### Global configuration + +```javascript +pbjs.setConfig({ + debug: false, + // …, + bigRichmedia: { + publisherId: 'A7FN99NZ98F5ZD4G', // Required + }, +}); +``` + +# AdUnit Configuration +```javascript +var adUnits = [ + // Skin adUnit + { + code: 'banner-div', + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]] + } + }, + bids: [{ + bidder: 'big-richmedia', + params: { + placementId: 12345, + format: 'skin' // This will automatically add 1800x1000 size to banner mediaType + } + }] + }, + // Video outstream adUnit + { + code: 'video-outstream', + sizes: [[300, 250]], + mediaTypes: { + video: { + playerSize: [[300, 250]], + context: 'outstream', + // Certain ORTB 2.5 video values can be read from the mediatypes object; below are examples of supported params. + // To note - appnexus supports additional values for our system that are not part of the ORTB spec. If you want + // to use these values, they will have to be declared in the bids[].params.video object instead using the appnexus syntax. + // Between the corresponding values of the mediaTypes.video and params.video objects, the properties in params.video will + // take precedence if declared; eg in the example below, the `skippable: true` setting will be used instead of the `skip: 0`. + minduration: 1, + maxduration: 60, + skip: 0, // 1 - true, 0 - false + skipafter: 5, + playbackmethod: [2], // note - we only support options 1-4 at this time + api: [1,2,3] // note - option 6 is not supported at this time + } + }, + bids: [ + { + bidder: 'big-richmedia', + params: { + placementId: 12345, + video: { + skippable: true, + playback_method: 'auto_play_sound_off' + }, + format: 'video-sticky-footer', // or 'video-sticky-top' + isReplayable: true // Default to false - choose if the video should be replayable or not. + customSelector: '#nav-bar' // custom selector for navbar + } + } + ] + } +]; +``` diff --git a/modules/bizzclickBidAdapter.js b/modules/bizzclickBidAdapter.js index 38195f8f9d9..6223626834d 100644 --- a/modules/bizzclickBidAdapter.js +++ b/modules/bizzclickBidAdapter.js @@ -88,11 +88,13 @@ export const spec = { host: location.host }, source: { - tid: bidRequest.transactionId + tid: bidRequest.transactionId, + ext: { + schain: {} + } }, regs: { coppa: config.getConfig('coppa') === true ? 1 : 0, - ext: {} }, user: { ext: {} @@ -115,7 +117,7 @@ export const spec = { } if (bidRequest.schain) { - data.source.ext.schain = bidRequest.schain; + deepSetValue(data, 'source.ext.schain', bidRequest.schain); } let connection = navigator.connection || navigator.webkitConnection; diff --git a/modules/bliinkBidAdapter.js b/modules/bliinkBidAdapter.js index 70349f95cde..45b6c46c2df 100644 --- a/modules/bliinkBidAdapter.js +++ b/modules/bliinkBidAdapter.js @@ -1,7 +1,6 @@ // eslint-disable-next-line prebid/validate-imports // eslint-disable-next-line prebid/validate-imports -import {registerBidder} from 'src/adapters/bidderFactory.js' - +import {registerBidder} from '../src/adapters/bidderFactory.js' export const BIDDER_CODE = 'bliink' export const BLIINK_ENDPOINT_ENGINE = 'https://engine.bliink.io/delivery' export const BLIINK_ENDPOINT_ENGINE_VAST = 'https://engine.bliink.io/vast' @@ -174,6 +173,8 @@ export const buildRequests = (_, bidderRequest) => { pageUrl: bidderRequest.refererInfo.referer, pageDescription: getMetaValue(META_DESCRIPTION), keywords: getKeywords().join(','), + gdpr: false, + gdpr_consent: '', pageTitle: document.title, } diff --git a/modules/bluebillywigBidAdapter.js b/modules/bluebillywigBidAdapter.js index 03fb0b92c8f..d362dfa5fdb 100644 --- a/modules/bluebillywigBidAdapter.js +++ b/modules/bluebillywigBidAdapter.js @@ -1,10 +1,10 @@ -import { deepAccess, deepSetValue, deepClone, logWarn, logError } from '../src/utils.js'; -import find from 'core-js-pure/features/array/find.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { VIDEO } from '../src/mediaTypes.js'; -import { config } from '../src/config.js'; -import { Renderer } from '../src/Renderer.js'; -import { createEidsArray } from './userId/eids.js'; +import {deepAccess, deepClone, deepSetValue, logError, logWarn} from '../src/utils.js'; +import {find} from '../src/polyfill.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {VIDEO} from '../src/mediaTypes.js'; +import {config} from '../src/config.js'; +import {Renderer} from '../src/Renderer.js'; +import {createEidsArray} from './userId/eids.js'; const DEV_MODE = window.location.search.match(/bbpbs_debug=true/); diff --git a/modules/brandmetricsRtdProvider.js b/modules/brandmetricsRtdProvider.js new file mode 100644 index 00000000000..60d3c98f15e --- /dev/null +++ b/modules/brandmetricsRtdProvider.js @@ -0,0 +1,168 @@ +/** + * This module adds brandmetrics provider to the real time data module + * The {@link module:modules/realTimeData} module is required + * The module will load load the brandmetrics script and set survey- targeting to ad units of specific bidders. + * @module modules/brandmetricsRtdProvider + * @requires module:modules/realTimeData + */ +import { config } from '../src/config.js' +import { submodule } from '../src/hook.js' +import { deepSetValue, mergeDeep, logError, deepAccess } from '../src/utils.js' +import {loadExternalScript} from '../src/adloader.js' +const MODULE_NAME = 'brandmetrics' +const MODULE_CODE = MODULE_NAME +const RECEIVED_EVENTS = [] +const GVL_ID = 422 +const TCF_PURPOSES = [1, 7] + +function init (config, userConsent) { + const hasConsent = checkConsent(userConsent) + + if (hasConsent) { + const moduleConfig = getMergedConfig(config) + initializeBrandmetrics(moduleConfig.params.scriptId) + } + return hasConsent +} + +/** + * Checks TCF and USP consents + * @param {Object} userConsent + * @returns {boolean} + */ +function checkConsent (userConsent) { + let consent = false + + if (userConsent && userConsent.gdpr && userConsent.gdpr.gdprApplies) { + const gdpr = userConsent.gdpr + + if (gdpr.vendorData) { + const vendor = gdpr.vendorData.vendor + const purpose = gdpr.vendorData.purpose + + let vendorConsent = false + if (vendor.consents) { + vendorConsent = vendor.consents[GVL_ID] + } + + if (vendor.legitimateInterests) { + vendorConsent = vendorConsent || vendor.legitimateInterests[GVL_ID] + } + + const purposes = TCF_PURPOSES.map(id => { + return (purpose.consents && purpose.consents[id]) || (purpose.legitimateInterests && purpose.legitimateInterests[id]) + }) + const purposesValid = purposes.filter(p => p === true).length === TCF_PURPOSES.length + consent = vendorConsent && purposesValid + } + } else if (userConsent.usp) { + const usp = userConsent.usp + consent = usp[1] !== 'N' && usp[2] !== 'Y' + } + + return consent +} + +/** +* Add event- listeners to hook in to brandmetrics events +* @param {Object} reqBidsConfigObj +* @param {function} callback +*/ +function processBrandmetricsEvents (reqBidsConfigObj, moduleConfig, callback) { + const callBidTargeting = (event) => { + if (event.available && event.conf) { + const targetingConf = event.conf.displayOption || {} + if (targetingConf.type === 'pbjs') { + setBidderTargeting(reqBidsConfigObj, moduleConfig, targetingConf.targetKey || 'brandmetrics_survey', event.survey.measurementId) + } + } + callback() + } + + if (RECEIVED_EVENTS.length > 0) { + callBidTargeting(RECEIVED_EVENTS[RECEIVED_EVENTS.length - 1]) + } else { + window._brandmetrics = window._brandmetrics || [] + window._brandmetrics.push({ + cmd: '_addeventlistener', + val: { + event: 'surveyloaded', + reEmitLast: true, + handler: (ev) => { + RECEIVED_EVENTS.push(ev) + if (RECEIVED_EVENTS.length === 1) { + // Call bid targeting only for the first received event, if called subsequently, last event from the RECEIVED_EVENTS array is used + callBidTargeting(ev) + } + }, + } + }) + } +} + +/** + * Sets bid targeting of specific bidders + * @param {Object} reqBidsConfigObj + * @param {string} key Targeting key + * @param {string} val Targeting value + */ +function setBidderTargeting (reqBidsConfigObj, moduleConfig, key, val) { + const bidders = deepAccess(moduleConfig, 'params.bidders') + if (bidders && bidders.length > 0) { + const ortb2 = {} + deepSetValue(ortb2, 'ortb2.user.ext.data.' + key, val) + config.setBidderConfig({ + bidders: bidders, + config: ortb2 + }) + } +} + +/** + * Add the brandmetrics script to the page. + * @param {string} scriptId - The script- id provided by brandmetrics or brandmetrics partner + */ +function initializeBrandmetrics(scriptId) { + if (scriptId) { + const path = 'https://cdn.brandmetrics.com/survey/script/' + const file = scriptId + '.js' + const url = path + file + + loadExternalScript(url, MODULE_CODE) + } +} + +/** + * Merges a provided config with default values + * @param {Object} customConfig + * @returns + */ +function getMergedConfig(customConfig) { + return mergeDeep({ + waitForIt: false, + params: { + bidders: [], + scriptId: undefined, + } + }, customConfig) +} + +/** @type {RtdSubmodule} */ +export const brandmetricsSubmodule = { + name: MODULE_NAME, + getBidRequestData: function (reqBidsConfigObj, callback, customConfig) { + try { + const moduleConfig = getMergedConfig(customConfig) + if (moduleConfig.waitForIt) { + processBrandmetricsEvents(reqBidsConfigObj, moduleConfig, callback) + } else { + callback() + } + } catch (e) { + logError(e) + } + }, + init: init +} + +submodule('realTimeData', brandmetricsSubmodule) diff --git a/modules/brandmetricsRtdProvider.md b/modules/brandmetricsRtdProvider.md new file mode 100644 index 00000000000..89ee6bb75cf --- /dev/null +++ b/modules/brandmetricsRtdProvider.md @@ -0,0 +1,40 @@ +# Brandmetrics Real-time Data Submodule +This module is intended to be used by brandmetrics (https://brandmetrics.com) partners and sets targeting keywords to bids if the browser is eligeble to see a brandmetrics survey. +The module hooks in to brandmetrics events and requires a brandmetrics script to be running. The module can optionally load and initialize brandmetrics by providing the 'scriptId'- parameter. + +## Usage +Compile the Brandmetrics RTD module into your Prebid build: +``` +gulp build --modules=rtdModule,brandmetricsRtdProvider +``` + +> Note that the global RTD module, `rtdModule`, is a prerequisite of the Brandmetrics RTD module. + +Enable the Brandmetrics RTD in your Prebid configuration, using the below format: + +```javascript +pbjs.setConfig({ + ..., + realTimeData: { + auctionDelay: 500, // auction delay + dataProviders: [{ + name: 'brandmetrics', + waitForIt: true // should be true if there's an `auctionDelay`, + params: { + scriptId: '00000000-0000-0000-0000-000000000000', + bidders: ['ozone'] + } + }] + }, + ... +}) +``` + +## Parameters +| Name | Type | Description | Default | +| ----------------- | -------------------- | ------------------ | ------------------ | +| name | String | This should always be `brandmetrics` | - | +| waitForIt | Boolean | Should be `true` if there's an `auctionDelay` defined (recommended) | `false` | +| params | Object | | - | +| params.bidders | String[] | An array of bidders which should receive targeting keys. | `[]` | +| params.scriptId | String | A script- id GUID if the brandmetrics- script should be initialized. | `undefined` | diff --git a/modules/bridgewellBidAdapter.js b/modules/bridgewellBidAdapter.js index 5d545b6f722..b141763af8e 100644 --- a/modules/bridgewellBidAdapter.js +++ b/modules/bridgewellBidAdapter.js @@ -1,7 +1,7 @@ -import { _each, inIframe, deepSetValue } from '../src/utils.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER, NATIVE } from '../src/mediaTypes.js'; -import find from 'core-js-pure/features/array/find.js'; +import {_each, deepSetValue, inIframe} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, NATIVE} from '../src/mediaTypes.js'; +import {find} from '../src/polyfill.js'; const BIDDER_CODE = 'bridgewell'; const REQUEST_ENDPOINT = 'https://prebid.scupio.com/recweb/prebid.aspx?cb='; diff --git a/modules/browsiRtdProvider.js b/modules/browsiRtdProvider.js index a1943afda8d..15f2d58010d 100644 --- a/modules/browsiRtdProvider.js +++ b/modules/browsiRtdProvider.js @@ -15,14 +15,15 @@ * @property {?string} keyName */ -import { deepClone, logError, isGptPubadsDefined, isNumber, isFn, deepSetValue } from '../src/utils.js'; +import {deepClone, deepSetValue, isFn, isGptPubadsDefined, isNumber, logError, logInfo, generateUUID} from '../src/utils.js'; import {submodule} from '../src/hook.js'; import {ajaxBuilder} from '../src/ajax.js'; import {loadExternalScript} from '../src/adloader.js'; import {getStorageManager} from '../src/storageManager.js'; -import find from 'core-js-pure/features/array/find.js'; +import {find, includes} from '../src/polyfill.js'; import {getGlobal} from '../src/prebidGlobal.js'; -import includes from 'core-js-pure/features/array/includes.js'; +import * as events from '../src/events.js'; +import CONSTANTS from '../src/constants.json'; const storage = getStorageManager(); @@ -107,6 +108,7 @@ export function setData(data) { } function getRTD(auc) { + logInfo(`Browsi RTD provider is fetching data for ${auc}`); try { const _bp = (_browsiData && _browsiData.p) || {}; return auc.reduce((rp, uc) => { @@ -332,13 +334,23 @@ export const browsiSubmodule = { getBidRequestData: setBidRequestsData }; -function getTargetingData(uc) { +function getTargetingData(uc, c, us, a) { const targetingData = getRTD(uc); + const auctionId = a.auctionId uc.forEach(auc => { if (isNumber(_ic[auc])) { _ic[auc] = _ic[auc] + 1; } + const transactionId = a.adUnits.find(adUnit => adUnit.code === auc).transactionId; + events.emit(CONSTANTS.EVENTS.BILLABLE_EVENT, { + vendor: 'browsi', + type: 'adRequest', + billingId: generateUUID(), + transactionId: transactionId, + auctionId: auctionId + }) }); + logInfo('Browsi RTD provider returned targeting data', targetingData, 'for', uc) return targetingData; } diff --git a/modules/ccxBidAdapter.js b/modules/ccxBidAdapter.js index 38bc99f1d83..65d1ced30e2 100644 --- a/modules/ccxBidAdapter.js +++ b/modules/ccxBidAdapter.js @@ -3,8 +3,8 @@ import { registerBidder } from '../src/adapters/bidderFactory.js' import { config } from '../src/config.js' import { getStorageManager } from '../src/storageManager.js'; -const storage = getStorageManager(); const BIDDER_CODE = 'ccx' +const storage = getStorageManager({bidderCode: BIDDER_CODE}); const BID_URL = 'https://delivery.clickonometrics.pl/ortb/prebid/bid' const SUPPORTED_VIDEO_PROTOCOLS = [2, 3, 5, 6] const SUPPORTED_VIDEO_MIMES = ['video/mp4', 'video/x-flv'] diff --git a/modules/cleanmedianetBidAdapter.js b/modules/cleanmedianetBidAdapter.js index 3c2d3c51bf5..3fda9917715 100644 --- a/modules/cleanmedianetBidAdapter.js +++ b/modules/cleanmedianetBidAdapter.js @@ -1,9 +1,9 @@ -import { getDNT, inIframe, isArray, isNumber, logError, deepAccess, logWarn } from '../src/utils.js'; +import {deepAccess, getDNT, inIframe, isArray, isNumber, logError, logWarn} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; import {Renderer} from '../src/Renderer.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; -import includes from 'core-js-pure/features/array/includes.js'; +import {includes} from '../src/polyfill.js'; export const helper = { getTopWindowDomain: function (url) { diff --git a/modules/colossussspBidAdapter.js b/modules/colossussspBidAdapter.js index 5e7c58f28ad..94265617d8f 100644 --- a/modules/colossussspBidAdapter.js +++ b/modules/colossussspBidAdapter.js @@ -1,10 +1,12 @@ import { getWindowTop, deepAccess, logMessage } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { ajax } from '../src/ajax.js'; +import { config } from '../src/config.js'; const BIDDER_CODE = 'colossusssp'; const G_URL = 'https://colossusssp.com/?c=o&m=multi'; -const G_URL_SYNC = 'https://colossusssp.com/?c=o&m=cookie'; +const G_URL_SYNC = 'https://sync.colossusssp.com'; function isBidResponseValid(bid) { if (!bid.requestId || !bid.cpm || !bid.creativeId || !bid.ttl || !bid.currency) { @@ -46,7 +48,10 @@ export const spec = { * @return boolean True if this is a valid bid, and false otherwise. */ isBidRequestValid: (bid) => { - return Boolean(bid.bidId && bid.params && !isNaN(bid.params.placement_id)); + const validPlacamentId = bid.params && !isNaN(bid.params.placement_id); + const validGroupId = bid.params && !isNaN(bid.params.group_id); + + return Boolean(bid.bidId && (validPlacamentId || validGroupId)); }, /** @@ -60,13 +65,13 @@ export const spec = { const location = winTop.location; let placements = []; let request = { - 'deviceWidth': winTop.screen.width, - 'deviceHeight': winTop.screen.height, - 'language': (navigator && navigator.language) ? navigator.language : '', - 'secure': location.protocol === 'https:' ? 1 : 0, - 'host': location.host, - 'page': location.pathname, - 'placements': placements, + deviceWidth: winTop.screen.width, + deviceHeight: winTop.screen.height, + language: (navigator && navigator.language) ? navigator.language : '', + secure: location.protocol === 'https:' ? 1 : 0, + host: location.host, + page: location.pathname, + placements: placements, }; if (bidderRequest) { @@ -84,6 +89,7 @@ export const spec = { let traff = bid.params.traffic || BANNER let placement = { placementId: bid.params.placement_id, + groupId: bid.params.group_id, bidId: bid.bidId, sizes: bid.mediaTypes[traff].sizes, traffic: traff, @@ -170,11 +176,33 @@ export const spec = { return response; }, - getUserSyncs: () => { + getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { + let syncType = syncOptions.iframeEnabled ? 'html' : 'hms.gif'; + let syncUrl = G_URL_SYNC + `/${syncType}?pbjs=1`; + if (gdprConsent && gdprConsent.consentString) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + syncUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; + } + } + if (uspConsent && uspConsent.consentString) { + syncUrl += `&ccpa_consent=${uspConsent.consentString}`; + } + + const coppa = config.getConfig('coppa') ? 1 : 0; + syncUrl += `&coppa=${coppa}`; + return [{ - type: 'image', - url: G_URL_SYNC + type: syncType, + url: syncUrl }]; + }, + + onBidWon: (bid) => { + if (bid.nurl) { + ajax(bid.nurl, null); + } } }; diff --git a/modules/colossussspBidAdapter.md b/modules/colossussspBidAdapter.md index 8797c648c95..4187dfbf36e 100644 --- a/modules/colossussspBidAdapter.md +++ b/modules/colossussspBidAdapter.md @@ -26,5 +26,19 @@ Module that connects to Colossus SSP demand sources traffic: 'banner' } }] - ]; + }, { + code: 'placementid_1', + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]] + } + }, + bids: [{ + bidder: 'colossusssp', + params: { + group_id: 0, + traffic: 'banner' + } + }] + }]; ``` diff --git a/modules/compassBidAdapter.js b/modules/compassBidAdapter.js new file mode 100644 index 00000000000..77f918276bc --- /dev/null +++ b/modules/compassBidAdapter.js @@ -0,0 +1,208 @@ +import { isFn, deepAccess, logMessage, logError } from '../src/utils.js'; + +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; + +const BIDDER_CODE = 'compass'; +const AD_URL = 'https://sa-lb.deliverimp.com/pbjs'; +const SYNC_URL = 'https://sa-cs.deliverimp.com'; + +function isBidResponseValid(bid) { + if (!bid.requestId || !bid.cpm || !bid.creativeId || !bid.ttl || !bid.currency) { + return false; + } + + switch (bid.mediaType) { + case BANNER: + return Boolean(bid.width && bid.height && bid.ad); + case VIDEO: + return Boolean(bid.vastUrl || bid.vastXml); + case NATIVE: + return Boolean(bid.native && bid.native.impressionTrackers && bid.native.impressionTrackers.length); + default: + return false; + } +} + +function getPlacementReqData(bid) { + const { params, bidId, mediaTypes } = bid; + const schain = bid.schain || {}; + const { placementId, endpointId } = params; + const bidfloor = getBidFloor(bid); + + const placement = { + bidId, + schain, + bidfloor + }; + + if (placementId) { + placement.placementId = placementId; + placement.type = 'publisher'; + } else if (endpointId) { + placement.endpointId = endpointId; + placement.type = 'network'; + } + + if (mediaTypes && mediaTypes[BANNER]) { + placement.adFormat = BANNER; + placement.sizes = mediaTypes[BANNER].sizes; + } else if (mediaTypes && mediaTypes[VIDEO]) { + placement.adFormat = VIDEO; + placement.playerSize = mediaTypes[VIDEO].playerSize; + placement.minduration = mediaTypes[VIDEO].minduration; + placement.maxduration = mediaTypes[VIDEO].maxduration; + placement.mimes = mediaTypes[VIDEO].mimes; + placement.protocols = mediaTypes[VIDEO].protocols; + placement.startdelay = mediaTypes[VIDEO].startdelay; + placement.placement = mediaTypes[VIDEO].placement; + placement.skip = mediaTypes[VIDEO].skip; + placement.skipafter = mediaTypes[VIDEO].skipafter; + placement.minbitrate = mediaTypes[VIDEO].minbitrate; + placement.maxbitrate = mediaTypes[VIDEO].maxbitrate; + placement.delivery = mediaTypes[VIDEO].delivery; + placement.playbackmethod = mediaTypes[VIDEO].playbackmethod; + placement.api = mediaTypes[VIDEO].api; + placement.linearity = mediaTypes[VIDEO].linearity; + } else if (mediaTypes && mediaTypes[NATIVE]) { + placement.native = mediaTypes[NATIVE]; + placement.adFormat = NATIVE; + } + + return placement; +} + +function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return deepAccess(bid, 'params.bidfloor', 0); + } + + try { + const bidFloor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*', + }); + return bidFloor.floor; + } catch (err) { + logError(err); + return 0; + } +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: (bid = {}) => { + const { params, bidId, mediaTypes } = bid; + let valid = Boolean(bidId && params && (params.placementId || params.endpointId)); + + if (mediaTypes && mediaTypes[BANNER]) { + valid = valid && Boolean(mediaTypes[BANNER] && mediaTypes[BANNER].sizes); + } else if (mediaTypes && mediaTypes[VIDEO]) { + valid = valid && Boolean(mediaTypes[VIDEO] && mediaTypes[VIDEO].playerSize); + } else if (mediaTypes && mediaTypes[NATIVE]) { + valid = valid && Boolean(mediaTypes[NATIVE]); + } else { + valid = false; + } + return valid; + }, + + buildRequests: (validBidRequests = [], bidderRequest = {}) => { + let deviceWidth = 0; + let deviceHeight = 0; + + let winLocation; + try { + const winTop = window.top; + deviceWidth = winTop.screen.width; + deviceHeight = winTop.screen.height; + winLocation = winTop.location; + } catch (e) { + logMessage(e); + winLocation = window.location; + } + + const refferUrl = bidderRequest.refererInfo && bidderRequest.refererInfo.referer; + let refferLocation; + try { + refferLocation = refferUrl && new URL(refferUrl); + } catch (e) { + logMessage(e); + } + + let location = refferLocation || winLocation; + const language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; + const host = location.host; + const page = location.pathname; + const secure = location.protocol === 'https:' ? 1 : 0; + const placements = []; + const request = { + deviceWidth, + deviceHeight, + language, + secure, + host, + page, + placements, + coppa: config.getConfig('coppa') === true ? 1 : 0, + ccpa: bidderRequest.uspConsent || undefined, + gdpr: bidderRequest.gdprConsent || undefined, + tmax: config.getConfig('bidderTimeout') + }; + + const len = validBidRequests.length; + for (let i = 0; i < len; i++) { + const bid = validBidRequests[i]; + placements.push(getPlacementReqData(bid)); + } + + return { + method: 'POST', + url: AD_URL, + data: request + }; + }, + + interpretResponse: (serverResponse) => { + let response = []; + for (let i = 0; i < serverResponse.body.length; i++) { + let resItem = serverResponse.body[i]; + if (isBidResponseValid(resItem)) { + const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; + resItem.meta = { ...resItem.meta, advertiserDomains }; + + response.push(resItem); + } + } + return response; + }, + + getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { + let syncType = syncOptions.iframeEnabled ? 'iframe' : 'image'; + let syncUrl = SYNC_URL + `/${syncType}?pbjs=1`; + if (gdprConsent && gdprConsent.consentString) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + syncUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; + } + } + if (uspConsent && uspConsent.consentString) { + syncUrl += `&ccpa_consent=${uspConsent.consentString}`; + } + + const coppa = config.getConfig('coppa') ? 1 : 0; + syncUrl += `&coppa=${coppa}`; + + return [{ + type: syncType, + url: syncUrl + }]; + } +}; + +registerBidder(spec); diff --git a/modules/compassBidAdapter.md b/modules/compassBidAdapter.md new file mode 100644 index 00000000000..18d52c12384 --- /dev/null +++ b/modules/compassBidAdapter.md @@ -0,0 +1,79 @@ +# Overview + +``` +Module Name: Compass Bidder Adapter +Module Type: Compass Bidder Adapter +Maintainer: sa-support@brightcom.com +``` + +# Description + +Connects to Compass exchange for bids. +Compass bid adapter supports Banner, Video (instream and outstream) and Native. + +# Test Parameters +``` + var adUnits = [ + // Will return static test banner + { + code: 'adunit1', + mediaTypes: { + banner: { + sizes: [ [300, 250], [320, 50] ], + } + }, + bids: [ + { + bidder: 'compass', + params: { + placementId: 'testBanner', + } + } + ] + }, + { + code: 'addunit2', + mediaTypes: { + video: { + playerSize: [ [640, 480] ], + context: 'instream', + minduration: 5, + maxduration: 60, + } + }, + bids: [ + { + bidder: 'compass', + params: { + placementId: 'testVideo', + } + } + ] + }, + { + code: 'addunit3', + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids: [ + { + bidder: 'compass', + params: { + placementId: 'testNative', + } + } + ] + } + ]; +``` \ No newline at end of file diff --git a/modules/concertBidAdapter.js b/modules/concertBidAdapter.js index 9a55e9cef1d..99e2492fb94 100644 --- a/modules/concertBidAdapter.js +++ b/modules/concertBidAdapter.js @@ -166,7 +166,7 @@ export const spec = { registerBidder(spec); -const storage = getStorageManager(); +const storage = getStorageManager({bidderCode: BIDDER_CODE}); /** * Check or generate a UID for the current user. diff --git a/modules/connectIdSystem.js b/modules/connectIdSystem.js index ac44a8b5a2d..2da2eda4c77 100644 --- a/modules/connectIdSystem.js +++ b/modules/connectIdSystem.js @@ -7,8 +7,8 @@ import {ajax} from '../src/ajax.js'; import {submodule} from '../src/hook.js'; -import {logError, formatQS} from '../src/utils.js'; -import includes from 'core-js-pure/features/array/includes.js'; +import {formatQS, logError} from '../src/utils.js'; +import {includes} from '../src/polyfill.js'; const MODULE_NAME = 'connectId'; const VENDOR_ID = 25; diff --git a/modules/consentManagement.js b/modules/consentManagement.js index 65ffc9a4def..65e0d6e92eb 100644 --- a/modules/consentManagement.js +++ b/modules/consentManagement.js @@ -1,15 +1,13 @@ - /** * This module adds GDPR consentManagement support to prebid.js. It interacts with * supported CMPs (Consent Management Platforms) to grab the user's consent information * and make it available for any GDPR supported adapters to read/pass this information to * their system. */ -import { logInfo, isFn, getAdUnitSizes, logWarn, isStr, isPlainObject, logError, isNumber } from '../src/utils.js'; -import { config } from '../src/config.js'; -import { gdprDataHandler } from '../src/adapterManager.js'; -import includes from 'core-js-pure/features/array/includes.js'; -import strIncludes from 'core-js-pure/features/string/includes.js'; +import {getAdUnitSizes, isFn, isNumber, isPlainObject, isStr, logError, logInfo, logWarn} from '../src/utils.js'; +import {config} from '../src/config.js'; +import {gdprDataHandler} from '../src/adapterManager.js'; +import {includes} from '../src/polyfill.js'; const DEFAULT_CMP = 'iab'; const DEFAULT_CONSENT_TIMEOUT = 10000; @@ -249,7 +247,7 @@ function lookupIabConsent(cmpSuccess, cmpError, hookConfig) { function readPostMessageResponse(event) { let cmpDataPkgName = `${apiName}Return`; - let json = (typeof event.data === 'string' && strIncludes(event.data, cmpDataPkgName)) ? JSON.parse(event.data) : event.data; + let json = (typeof event.data === 'string' && includes(event.data, cmpDataPkgName)) ? JSON.parse(event.data) : event.data; if (json[cmpDataPkgName] && json[cmpDataPkgName].callId) { let payload = json[cmpDataPkgName]; // TODO - clean up this logic (move listeners?); we have duplicate messages responses because 2 eventlisteners are active from the 2 cmp requests running in parallel @@ -289,6 +287,7 @@ export function requestBidsHook(fn, reqBidsConfigObj) { if (!includes(Object.keys(cmpCallMap), userCMP)) { logWarn(`CMP framework (${userCMP}) is not a supported framework. Aborting consentManagement module and resuming auction.`); + gdprDataHandler.setConsentData(null); return hookConfig.nextFn.apply(hookConfig.context, hookConfig.args); } @@ -371,7 +370,13 @@ function processCmpData(consentObject, hookConfig) { * General timeout callback when interacting with CMP takes too long. */ function cmpTimedOut(hookConfig) { - cmpFailed('CMP workflow exceeded timeout threshold.', hookConfig); + if (cmpVersion === 2) { + logWarn(`No response from CMP, continuing auction...`) + storeConsentData(undefined); + exitModule(null, hookConfig) + } else { + cmpFailed('CMP workflow exceeded timeout threshold.', hookConfig); + } } /** @@ -446,6 +451,7 @@ function exitModule(errMsg, hookConfig, extraArgs) { nextFn.apply(context, args); } else { logError(errMsg + ' Canceling auction as per consentManagement config.', extraArgs); + gdprDataHandler.setConsentData(null); if (typeof hookConfig.bidsBackHandler === 'function') { hookConfig.bidsBackHandler(); } else { @@ -465,7 +471,7 @@ export function resetConsentData() { consentData = undefined; userCMP = undefined; cmpVersion = 0; - gdprDataHandler.setConsentData(null); + gdprDataHandler.reset(); } /** @@ -503,6 +509,7 @@ export function setConsentConfig(config) { gdprScope = config.defaultGdprScope === true; logInfo('consentManagement module has been activated...'); + gdprDataHandler.enable(); if (userCMP === 'static') { if (isPlainObject(config.consentData)) { diff --git a/modules/consentManagementUsp.js b/modules/consentManagementUsp.js index 4a4c4ae0a55..75462221403 100644 --- a/modules/consentManagementUsp.js +++ b/modules/consentManagementUsp.js @@ -186,6 +186,7 @@ export function requestBidsHook(fn, reqBidsConfigObj) { if (!uspCallMap[consentAPI]) { logWarn(`USP framework (${consentAPI}) is not a supported framework. Aborting consentManagement module and resuming auction.`); + uspDataHandler.setConsentData(null); return hookConfig.nextFn.apply(hookConfig.context, hookConfig.args); } @@ -276,6 +277,7 @@ function exitModule(errMsg, hookConfig, extraArgs) { if (errMsg) { logWarn(errMsg + ' Resuming auction without consent data as per consentManagement config.', extraArgs); + uspDataHandler.setConsentData(null) // let core know that no consent data is available } nextFn.apply(context, args); } @@ -287,7 +289,7 @@ function exitModule(errMsg, hookConfig, extraArgs) { export function resetConsentData() { consentData = undefined; consentAPI = undefined; - uspDataHandler.setConsentData(null); + uspDataHandler.reset(); } /** @@ -315,6 +317,7 @@ export function setConsentConfig(config) { } logInfo('USPAPI consentManagement module has been activated...'); + uspDataHandler.enable(); if (consentAPI === 'static') { if (isPlainObject(config.consentData) && isPlainObject(config.consentData.getUSPData)) { diff --git a/modules/consumableBidAdapter.md b/modules/consumableBidAdapter.md index 2189494ebd4..ba472899c49 100644 --- a/modules/consumableBidAdapter.md +++ b/modules/consumableBidAdapter.md @@ -4,7 +4,7 @@ Module Name: Consumable Bid Adapter Module Type: Consumable Adapter -Maintainer: naffis@consumable.com +Maintainer: prebid@consumable.com # Description diff --git a/modules/conversantBidAdapter.js b/modules/conversantBidAdapter.js index 92b5d47277e..7ee8b1b7681 100644 --- a/modules/conversantBidAdapter.js +++ b/modules/conversantBidAdapter.js @@ -2,11 +2,12 @@ import { logWarn, isStr, deepAccess, isArray, getBidIdParameter, deepSetValue, i import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {getStorageManager} from '../src/storageManager.js'; +import { config } from '../src/config.js'; const GVLID = 24; -export const storage = getStorageManager(GVLID); const BIDDER_CODE = 'conversant'; +export const storage = getStorageManager({gvlid: GVLID, bidderCode: BIDDER_CODE}); const URL = 'https://web.hb.ad.cpe.dotomi.com/cvx/client/hb/ortb/25'; export const spec = { @@ -76,6 +77,9 @@ export const spec = { displaymanager: 'Prebid.js', displaymanagerver: '$prebid.version$' }; + if (bid.ortb2Imp) { + mergeDeep(imp, bid.ortb2Imp); + } copyOptProperty(bid.params.tag_id, imp, 'tagid'); @@ -132,6 +136,12 @@ export const spec = { let userExt = {}; + // pass schain object if it is present + const schain = deepAccess(validBidRequests, '0.schain'); + if (schain) { + deepSetValue(payload, 'source.ext.schain', schain); + } + if (bidderRequest) { // Add GDPR flag and consent string if (bidderRequest.gdprConsent) { @@ -167,6 +177,9 @@ export const spec = { payload.user = {ext: userExt}; } + const firstPartyData = config.getConfig('ortb2') || {}; + mergeDeep(payload, firstPartyData); + return { method: 'POST', url: bidurl, diff --git a/modules/craftBidAdapter.js b/modules/craftBidAdapter.js index 812ec53d686..61ca4f929e7 100644 --- a/modules/craftBidAdapter.js +++ b/modules/craftBidAdapter.js @@ -1,16 +1,24 @@ -import { logError, convertTypes, convertCamelToUnderscore, isArray, deepAccess, getBidRequest, isEmpty, transformBidderParamKeywords } from '../src/utils.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { auctionManager } from '../src/auctionManager.js'; -import find from 'core-js-pure/features/array/find.js'; -import includes from 'core-js-pure/features/array/includes.js'; -import { getStorageManager } from '../src/storageManager.js'; +import { + convertCamelToUnderscore, + convertTypes, + deepAccess, + getBidRequest, + isArray, + isEmpty, + logError, + transformBidderParamKeywords +} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; +import {auctionManager} from '../src/auctionManager.js'; +import {find, includes} from '../src/polyfill.js'; +import {getStorageManager} from '../src/storageManager.js'; import {ajax} from '../src/ajax.js'; const BIDDER_CODE = 'craft'; const URL_BASE = 'https://gacraft.jp/prebid-v3'; const TTL = 360; -const storage = getStorageManager(); +const storage = getStorageManager({bidderCode: BIDDER_CODE}); export const spec = { code: BIDDER_CODE, diff --git a/modules/criteoBidAdapter.js b/modules/criteoBidAdapter.js index a4ec99e4fa8..75d41d970a9 100644 --- a/modules/criteoBidAdapter.js +++ b/modules/criteoBidAdapter.js @@ -1,11 +1,11 @@ -import { isArray, getUniqueIdentifierStr, parseUrl, deepAccess, logWarn, logError, logInfo } from '../src/utils.js'; +import {deepAccess, getUniqueIdentifierStr, isArray, logError, logInfo, logWarn, parseUrl} from '../src/utils.js'; import {loadExternalScript} from '../src/adloader.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; -import find from 'core-js-pure/features/array/find.js'; -import { verify } from 'criteo-direct-rsa-validate/build/verify.js'; // ref#2 -import { getStorageManager } from '../src/storageManager.js'; +import {find} from '../src/polyfill.js'; +import {verify} from 'criteo-direct-rsa-validate/build/verify.js'; // ref#2 +import {getStorageManager} from '../src/storageManager.js'; const GVLID = 91; export const ADAPTER_VERSION = 34; @@ -13,7 +13,7 @@ const BIDDER_CODE = 'criteo'; const CDB_ENDPOINT = 'https://bidder.criteo.com/cdb'; const PROFILE_ID_INLINE = 207; export const PROFILE_ID_PUBLISHERTAG = 185; -const storage = getStorageManager(GVLID); +const storage = getStorageManager({gvlid: GVLID, bidderCode: BIDDER_CODE}); const LOG_PREFIX = 'Criteo: '; /* @@ -24,7 +24,7 @@ const LOG_PREFIX = 'Criteo: '; Unminified source code can be found in the privately shared repo: https://github.com/Prebid-org/prebid-js-external-js-criteo/blob/master/dist/prod.js */ const FAST_BID_VERSION_PLACEHOLDER = '%FAST_BID_VERSION%'; -export const FAST_BID_VERSION_CURRENT = 113; +export const FAST_BID_VERSION_CURRENT = 117; const FAST_BID_VERSION_LATEST = 'latest'; const FAST_BID_VERSION_NONE = 'none'; const PUBLISHER_TAG_URL_TEMPLATE = 'https://static.criteo.net/js/ld/publishertag.prebid' + FAST_BID_VERSION_PLACEHOLDER + '.js'; @@ -281,6 +281,7 @@ function checkNativeSendId(bidRequest) { */ function buildCdbRequest(context, bidRequests, bidderRequest) { let networkId; + let schain; const request = { publisher: { url: context.url, @@ -288,6 +289,7 @@ function buildCdbRequest(context, bidRequests, bidderRequest) { }, slots: bidRequests.map(bidRequest => { networkId = bidRequest.params.networkId || networkId; + schain = bidRequest.schain || schain; const slot = { impid: bidRequest.adUnitCode, transactionid: bidRequest.transactionId, @@ -310,9 +312,9 @@ function buildCdbRequest(context, bidRequests, bidderRequest) { if (!checkNativeSendId(bidRequest)) { logWarn(LOG_PREFIX + 'all native assets containing URL should be sent as placeholders with sendId(icon, image, clickUrl, displayUrl, privacyLink, privacyIcon)'); } - slot.sizes = parseSizes(retrieveBannerSizes(bidRequest), parseNativeSize); + slot.sizes = parseSizes(deepAccess(bidRequest, 'mediaTypes.banner.sizes'), parseNativeSize); } else { - slot.sizes = parseSizes(retrieveBannerSizes(bidRequest), parseSize); + slot.sizes = parseSizes(deepAccess(bidRequest, 'mediaTypes.banner.sizes'), parseSize); } if (hasVideoMediaType(bidRequest)) { const video = { @@ -344,6 +346,13 @@ function buildCdbRequest(context, bidRequests, bidderRequest) { if (networkId) { request.publisher.networkid = networkId; } + if (schain) { + request.source = { + ext: { + schain: schain + } + } + }; request.user = { ext: bidderRequest.userExt }; @@ -366,11 +375,10 @@ function buildCdbRequest(context, bidRequests, bidderRequest) { return request; } -function retrieveBannerSizes(bidRequest) { - return deepAccess(bidRequest, 'mediaTypes.banner.sizes') || bidRequest.sizes; -} - function parseSizes(sizes, parser) { + if (sizes == undefined) { + return []; + } if (Array.isArray(sizes[0])) { // is there several sizes ? (ie. [[728,90],[200,300]]) return sizes.map(size => parser(size)); } diff --git a/modules/criteoIdSystem.js b/modules/criteoIdSystem.js index c716a3c9cd6..c73c4422a77 100644 --- a/modules/criteoIdSystem.js +++ b/modules/criteoIdSystem.js @@ -13,7 +13,7 @@ import { getStorageManager } from '../src/storageManager.js'; const gvlid = 91; const bidderCode = 'criteo'; -export const storage = getStorageManager(gvlid, bidderCode); +export const storage = getStorageManager({gvlid: gvlid, moduleName: bidderCode}); const bididStorageKey = 'cto_bidid'; const bundleStorageKey = 'cto_bundle'; @@ -33,15 +33,37 @@ function getFromAllStorages(key) { return storage.getCookie(key) || storage.getDataFromLocalStorage(key); } -function saveOnAllStorages(key, value) { +function saveOnAllStorages(key, value, hostname) { if (key && value) { - storage.setCookie(key, value, expirationString); storage.setDataInLocalStorage(key, value); + setCookieOnAllDomains(key, value, expirationString, hostname, true); } } -function deleteFromAllStorages(key) { - storage.setCookie(key, '', pastDateString); +function setCookieOnAllDomains(key, value, expiration, hostname, stopOnSuccess) { + const subDomains = hostname.split('.'); + for (let i = 0; i < subDomains.length; ++i) { + // Try to write the cookie on this subdomain (we want it to be stored only on the TLD+1) + const domain = subDomains.slice(subDomains.length - i - 1, subDomains.length).join('.'); + + try { + storage.setCookie(key, value, expiration, null, '.' + domain); + + if (stopOnSuccess) { + // Try to read the cookie to check if we wrote it + const ck = storage.getCookie(key); + if (ck && ck === value) { + break; + } + } + } catch (error) { + + } + } +} + +function deleteFromAllStorages(key, hostname) { + setCookieOnAllDomains(key, '', pastDateString, hostname, true); storage.removeDataFromLocalStorage(key); } @@ -89,15 +111,15 @@ function callCriteoUserSync(parsedCriteoData, gdprString, callback) { const urlsToCall = typeof jsonResponse.acwsUrl === 'string' ? [jsonResponse.acwsUrl] : jsonResponse.acwsUrl; urlsToCall.forEach(url => triggerPixel(url)); } else if (jsonResponse.bundle) { - saveOnAllStorages(bundleStorageKey, jsonResponse.bundle); + saveOnAllStorages(bundleStorageKey, jsonResponse.bundle, domain); } if (jsonResponse.bidId) { - saveOnAllStorages(bididStorageKey, jsonResponse.bidId); + saveOnAllStorages(bididStorageKey, jsonResponse.bidId, domain); const criteoId = { criteoId: jsonResponse.bidId }; callback(criteoId); } else { - deleteFromAllStorages(bididStorageKey); + deleteFromAllStorages(bididStorageKey, domain); callback(); } }, diff --git a/modules/currency.js b/modules/currency.js index dc77ee21430..a59a9880af1 100644 --- a/modules/currency.js +++ b/modules/currency.js @@ -1,7 +1,7 @@ import { logInfo, logWarn, logError, logMessage } from '../src/utils.js'; import { getGlobal } from '../src/prebidGlobal.js'; import { createBid } from '../src/bidfactory.js'; -import { STATUS } from '../src/constants.json'; +import CONSTANTS from '../src/constants.json'; import { ajax } from '../src/ajax.js'; import { config } from '../src/config.js'; import { getHook } from '../src/hook.js'; @@ -20,6 +20,25 @@ export var currencyRates = {}; var bidderCurrencyDefault = {}; var defaultRates; +export const ready = (() => { + let isDone, resolver, promise; + function reset() { + isDone = false; + resolver = null; + promise = new Promise((resolve) => { + resolver = resolve; + if (isDone) resolve(); + }) + } + function done() { + isDone = true; + if (resolver != null) { resolver() } + } + reset(); + + return {done, reset, promise: () => promise} +})(); + /** * Configuration function for currency * @param {string} [config.adServerCurrency = 'USD'] @@ -138,11 +157,15 @@ function initCurrency(url) { logInfo('currencyRates set to ' + JSON.stringify(currencyRates)); currencyRatesLoaded = true; processBidResponseQueue(); + ready.done(); } catch (e) { errorSettingsRates('Failed to parse currencyRates response: ' + response); } }, - error: errorSettingsRates + error: function (...args) { + errorSettingsRates(...args); + ready.done(); + } } ); } @@ -197,6 +220,8 @@ export function addBidResponseHook(fn, adUnitCode, bid) { bidResponseQueue.push(wrapFunction(fn, this, [adUnitCode, bid])); if (!currencySupportEnabled || currencyRatesLoaded) { processBidResponseQueue(); + } else { + fn.bail(ready.promise()); } } @@ -219,10 +244,7 @@ function wrapFunction(fn, context, params) { } } catch (e) { logWarn('Returning NO_BID, getCurrencyConversion threw error: ', e); - params[1] = createBid(STATUS.NO_BID, { - bidder: bid.bidderCode || bid.bidder, - bidId: bid.requestId - }); + params[1] = createBid(CONSTANTS.STATUS.NO_BID, bid.getIdentifiers()); } } return fn.apply(context, params); diff --git a/modules/cwireBidAdapter.js b/modules/cwireBidAdapter.js index 9e082dffbf4..c0a24b49a3c 100644 --- a/modules/cwireBidAdapter.js +++ b/modules/cwireBidAdapter.js @@ -1,22 +1,22 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; -import { getRefererInfo } from '../src/refererDetection.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {getRefererInfo} from '../src/refererDetection.js'; +import {getStorageManager} from '../src/storageManager.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; -import { OUTSTREAM } from '../src/video.js'; +import {OUTSTREAM} from '../src/video.js'; import { - isArray, - isNumber, - generateUUID, - parseSizesInput, deepAccess, + generateUUID, + getBidIdParameter, getParameterByName, getValue, - getBidIdParameter, + isArray, + isNumber, logError, logWarn, + parseSizesInput, } from '../src/utils.js'; -import { Renderer } from '../src/Renderer.js'; -import find from 'core-js-pure/features/array/find.js'; +import {Renderer} from '../src/Renderer.js'; +import {find} from '../src/polyfill.js'; // ------------------------------------ const BIDDER_CODE = 'cwire'; @@ -26,8 +26,9 @@ export const RENDERER_URL = 'https://cdn.cwi.re/prebid/renderer/LATEST/renderer. export const CW_PAGE_VIEW_ID = generateUUID(); const LS_CWID_KEY = 'cw_cwid'; const CW_GROUPS_QUERY = 'cwgroups'; +const CW_CREATIVE_QUERY = 'cwcreative'; -const storage = getStorageManager(); +const storage = getStorageManager({bidderCode: BIDDER_CODE}); /** * ------------------------------------ @@ -90,14 +91,17 @@ export const mapSlotsData = function(validBidRequests) { const slots = []; validBidRequests.forEach(bid => { const bidObj = {}; + // get testing / debug params + let cwcreative = getValue(bid.params, 'cwcreative'); + let refgroups = getValue(bid.params, 'refgroups'); + let cwapikey = getValue(bid.params, 'cwapikey'); + // get the pacement and page ids let placementId = getValue(bid.params, 'placementId'); let pageId = getValue(bid.params, 'pageId'); - let adUnitElementId = getValue(bid.params, 'adUnitElementId'); // get the rest of the auction/bid/transaction info bidObj.auctionId = getBidIdParameter('auctionId', bid); bidObj.adUnitCode = getBidIdParameter('adUnitCode', bid); - bidObj.adUnitElementId = adUnitElementId; bidObj.bidId = getBidIdParameter('bidId', bid); bidObj.bidderRequestId = getBidIdParameter('bidderRequestId', bid); bidObj.placementId = placementId; @@ -105,6 +109,9 @@ export const mapSlotsData = function(validBidRequests) { bidObj.mediaTypes = getBidIdParameter('mediaTypes', bid); bidObj.transactionId = getBidIdParameter('transactionId', bid); bidObj.sizes = getSlotSizes(bid); + bidObj.cwcreative = cwcreative; + bidObj.refgroups = refgroups; + bidObj.cwapikey = cwapikey; slots.push(bidObj); }); @@ -123,11 +130,6 @@ export const spec = { isBidRequestValid: function(bid) { bid.params = bid.params || {}; - // if ad unit elemt id not provided - use adUnitCode by default - if (!bid.params.adUnitElementId) { - bid.params.adUnitElementId = bid.code; - } - if (!bid.params.placementId || !isNumber(bid.params.placementId)) { logError('placementId not provided or invalid'); return false; @@ -141,6 +143,21 @@ export const spec = { return true; }, + /** + * ------------------------------------ + * itterate trough slots array and try + * to extract first occurence of a given + * key, if not found - return null + * ------------------------------------ + */ + getFirstValueOrNull: function(slots, key) { + const found = slots.find((item) => { + return (typeof item[key] !== 'undefined'); + }); + + return (found) ? found[key] : null; + }, + /** * ------------------------------------ * Make a server request from the @@ -161,8 +178,19 @@ export const spec = { let refgroups = []; + const cwCreativeId = parseInt(getQueryVariable(CW_CREATIVE_QUERY), 10) || null; + const cwCreativeIdFromConfig = this.getFirstValueOrNull(slots, 'cwcreative'); + const refGroupsFromConfig = this.getFirstValueOrNull(slots, 'refgroups'); + const cwApiKeyFromConfig = this.getFirstValueOrNull(slots, 'cwapikey'); const rgQuery = getQueryVariable(CW_GROUPS_QUERY); + + if (refGroupsFromConfig !== null) { + refgroups = refGroupsFromConfig.split(','); + } + if (rgQuery !== null) { + // override if query param is present + refgroups = []; refgroups = rgQuery.split(','); } @@ -171,7 +199,9 @@ export const spec = { const payload = { cwid: localStorageCWID, refgroups, + cwcreative: cwCreativeId || cwCreativeIdFromConfig, slots: slots, + cwapikey: cwApiKeyFromConfig, httpRef: referer || '', pageViewId: CW_PAGE_VIEW_ID, }; diff --git a/modules/cwireBidAdapter.md b/modules/cwireBidAdapter.md index fc0889e05ad..b42c7a02489 100644 --- a/modules/cwireBidAdapter.md +++ b/modules/cwireBidAdapter.md @@ -2,7 +2,7 @@ Module Name: C-WIRE Bid Adapter Module Type: Adagio Adapter -Maintainer: dragan@cwire.ch +Maintainer: publishers@cwire.ch ## Description @@ -17,7 +17,10 @@ Below, the list of C-WIRE params and where they can be set. | ---------- | ------------- | ------------- | ---- | ---------| | pageId | | x | number | YES | | placementId | | x | number | YES | -| adUnitElementId | | x | string | NO | +| refgroups | | x | string | NO | +| cwcreative | | x | integer | NO | +| cwapikey | | x | string | NO | + ### adUnit configuration @@ -35,9 +38,11 @@ var adUnits = [ params: { pageId: 1422, // required - number placementId: 2211521, // required - number - adUnitElementId: 'other_div', // optional, div id to write to, if not set it will default to ad unit code + cwcreative: 42, // optional - id of creative to force + refgroups: 'test-user', // optional - name of group or coma separated list of groups to force + cwapikey: 'api_key_xyz', // optional - api key for integration testing } }] } ]; -``` \ No newline at end of file +``` diff --git a/modules/dacIdSystem.js b/modules/dacIdSystem.js new file mode 100644 index 00000000000..73b5c7420cf --- /dev/null +++ b/modules/dacIdSystem.js @@ -0,0 +1,57 @@ +/** + * This module adds dacId to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/dacIdSystem + * @requires module:modules/userId + */ + +import { submodule } from '../src/hook.js'; +import { getStorageManager } from '../src/storageManager.js'; + +export const storage = getStorageManager(); + +export const cookieKey = '_a1_f'; + +export const dacIdSystemSubmodule = { + /** + * used to link submodule with config + * @type {string} + */ + name: 'dacId', + + /** + * performs action to obtain id + * @function + * @returns { {id: {dacId: string}} | undefined } + */ + getId: function() { + const newId = storage.getCookie(cookieKey); + if (!newId) { + return undefined; + } + const result = { + dacId: newId + } + return {id: result}; + }, + + /** + * decode the stored id value for passing to bid requests + * @function + * @param { {dacId: string} } value + * @returns { {dacId: {id: string} } | undefined } + */ + decode: function(value) { + if (value && typeof value === 'object') { + const result = {}; + if (value.dacId) { + result.id = value.dacId + } + return {dacId: result}; + } + return undefined; + }, + +} + +submodule('userId', dacIdSystemSubmodule); diff --git a/modules/dacIdSystem.md b/modules/dacIdSystem.md new file mode 100644 index 00000000000..b422d0a536d --- /dev/null +++ b/modules/dacIdSystem.md @@ -0,0 +1,28 @@ +## DAC User ID Submodule + +DAC ID, provided by [D.A.Consortium Inc.](https://www.dac.co.jp/), is ID for ad targeting by using 1st party cookie. +Please contact D.A.Consortium Inc. before using this ID. + +## Building Prebid with DAC ID Support + +First, make sure to add the DAC ID submodule to your Prebid.js package with: + +``` +gulp build --modules=dacIdSystem +``` + +The following configuration parameters are available: + +```javascript +pbjs.setConfig({ + userSync: { + userIds: [{ + name: 'dacId' + }] + } +}); +``` + +| Param under userSync.userIds[] | Scope | Type | Description | Example | +| --- | --- | --- | --- | --- | +| name | Required | String | The name of this module. | `"dacId"` | diff --git a/modules/dailyhuntBidAdapter.js b/modules/dailyhuntBidAdapter.js new file mode 100644 index 00000000000..ffa84ff88fd --- /dev/null +++ b/modules/dailyhuntBidAdapter.js @@ -0,0 +1,435 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import * as mediaTypes from '../src/mediaTypes.js'; +import {_map, deepAccess, isEmpty} from '../src/utils.js'; +import {ajax} from '../src/ajax.js'; +import {find} from '../src/polyfill.js'; +import {INSTREAM, OUTSTREAM} from '../src/video.js'; + +const BIDDER_CODE = 'dailyhunt'; +const BIDDER_ALIAS = 'dh'; +const SUPPORTED_MEDIA_TYPES = [mediaTypes.BANNER, mediaTypes.NATIVE, mediaTypes.VIDEO]; + +const PROD_PREBID_ENDPOINT_URL = 'https://pbs.dailyhunt.in/openrtb2/auction?partner='; +const PROD_PREBID_TEST_ENDPOINT_URL = 'https://qa-pbs-van.dailyhunt.in/openrtb2/auction?partner='; + +const ORTB_NATIVE_TYPE_MAPPING = { + img: { + '3': 'image', + '1': 'icon' + }, + data: { + '1': 'sponsoredBy', + '2': 'body', + '3': 'rating', + '4': 'likes', + '5': 'downloads', + '6': 'price', + '7': 'salePrice', + '8': 'phone', + '9': 'address', + '10': 'body2', + '11': 'displayUrl', + '12': 'cta' + } +} + +const ORTB_NATIVE_PARAMS = { + title: { + id: 0, + name: 'title' + }, + icon: { + id: 1, + type: 1, + name: 'img' + }, + image: { + id: 2, + type: 3, + name: 'img' + }, + sponsoredBy: { + id: 3, + name: 'data', + type: 1 + }, + body: { + id: 4, + name: 'data', + type: 2 + }, + cta: { + id: 5, + type: 12, + name: 'data' + }, + body2: { + id: 4, + name: 'data', + type: 10 + }, +}; + +// Encode URI. +const _encodeURIComponent = function (a) { + let b = window.encodeURIComponent(a); + b = b.replace(/'/g, '%27'); + return b; +} + +// Extract key from collections. +const extractKeyInfo = (collection, key) => { + for (let i = 0, result; i < collection.length; i++) { + result = deepAccess(collection[i].params, key); + if (result) { + return result; + } + } + return undefined +} + +// Flattern Array. +const flatten = (arr) => { + return [].concat(...arr); +} + +const createOrtbRequest = (validBidRequests, bidderRequest) => { + let device = createOrtbDeviceObj(validBidRequests); + let user = createOrtbUserObj(validBidRequests) + let site = createOrtbSiteObj(validBidRequests, bidderRequest.refererInfo.referer) + return { + id: bidderRequest.auctionId, + imp: [], + site, + device, + user, + }; +} + +const createOrtbDeviceObj = (validBidRequests) => { + let device = { ...extractKeyInfo(validBidRequests, `device`) }; + device.ua = navigator.userAgent; + return device; +} + +const createOrtbUserObj = (validBidRequests) => ({ ...extractKeyInfo(validBidRequests, `user`) }) + +const createOrtbSiteObj = (validBidRequests, page) => { + let site = { ...extractKeyInfo(validBidRequests, `site`), page }; + let publisher = createOrtbPublisherObj(validBidRequests); + if (!site.publisher) { + site.publisher = publisher + } + return site +} + +const createOrtbPublisherObj = (validBidRequests) => ({ ...extractKeyInfo(validBidRequests, `publisher`) }) + +// get bidFloor Function for different creatives +function getBidFloor(bid, creative) { + let floorInfo = typeof (bid.getFloor) == 'function' ? bid.getFloor({ currency: 'USD', mediaType: creative, size: '*' }) : {}; + return Math.floor(floorInfo.floor || (bid.params.bidfloor ? bid.params.bidfloor : 0.0)); +} + +const createOrtbImpObj = (bid) => { + let params = bid.params + let testMode = !!bid.params.test_mode + + // Validate Banner Request. + let bannerObj = deepAccess(bid.mediaTypes, `banner`); + let nativeObj = deepAccess(bid.mediaTypes, `native`); + let videoObj = deepAccess(bid.mediaTypes, `video`); + + let imp = { + id: bid.bidId, + ext: { + dailyhunt: { + placement_id: params.placement_id, + publisher_id: params.publisher_id, + partner: params.partner_name + } + } + }; + + // Test Mode Campaign. + if (testMode) { + imp.ext.test_mode = testMode; + } + + if (bannerObj) { + imp.banner = { + ...createOrtbImpBannerObj(bid, bannerObj) + } + imp.bidfloor = getBidFloor(bid, 'banner'); + } else if (nativeObj) { + imp.native = { + ...createOrtbImpNativeObj(bid, nativeObj) + } + imp.bidfloor = getBidFloor(bid, 'native'); + } else if (videoObj) { + imp.video = { + ...createOrtbImpVideoObj(bid, videoObj) + } + imp.bidfloor = getBidFloor(bid, 'video'); + } + return imp; +} + +const createOrtbImpBannerObj = (bid, bannerObj) => { + let format = []; + bannerObj.sizes.forEach(size => format.push({ w: size[0], h: size[1] })) + + return { + id: 'banner-' + bid.bidId, + format + } +} + +const createOrtbImpNativeObj = (bid, nativeObj) => { + const assets = _map(bid.nativeParams, (bidParams, key) => { + const props = ORTB_NATIVE_PARAMS[key]; + const asset = { + required: bidParams.required & 1, + }; + if (props) { + let h = 0; + let w = 0; + + asset.id = props.id; + + if (bidParams.sizes) { + const sizes = flatten(bidParams.sizes); + w = sizes[0]; + h = sizes[1]; + } + + asset[props.name] = { + len: bidParams.len ? bidParams.len : 20, + type: props.type, + w, + h + }; + + return asset; + } + }).filter(Boolean); + let request = { + assets, + ver: '1,0' + } + return { request: JSON.stringify(request) }; +} + +const createOrtbImpVideoObj = (bid, videoObj) => { + let obj = {}; + let params = bid.params + if (!isEmpty(bid.params.video)) { + obj = { + topframe: 1, + skip: params.video.skippable || 0, + linearity: params.video.linearity || 1, + minduration: params.video.minduration || 5, + maxduration: params.video.maxduration || 60, + mimes: params.video.mimes || ['video/mp4'], + protocols: getProtocols(params.video), + w: params.video.playerSize[0][0], + h: params.video.playerSize[0][1], + }; + } else { + obj = { + mimes: ['video/mp4'], + }; + } + obj.ext = { + ...videoObj, + } + return obj; +} + +export function getProtocols({protocols}) { + let defaultValue = [2, 3, 5, 6, 7, 8]; + let listProtocols = [ + {key: 'VAST_1_0', value: 1}, + {key: 'VAST_2_0', value: 2}, + {key: 'VAST_3_0', value: 3}, + {key: 'VAST_1_0_WRAPPER', value: 4}, + {key: 'VAST_2_0_WRAPPER', value: 5}, + {key: 'VAST_3_0_WRAPPER', value: 6}, + {key: 'VAST_4_0', value: 7}, + {key: 'VAST_4_0_WRAPPER', value: 8} + ]; + if (protocols) { + return listProtocols.filter(p => { + return protocols.indexOf(p.key) !== -1 + }).map(p => p.value); + } else { + return defaultValue; + } +} + +const createServerRequest = (ortbRequest, validBidRequests, isTestMode = 'false') => ({ + method: 'POST', + url: isTestMode === 'true' ? PROD_PREBID_TEST_ENDPOINT_URL + validBidRequests[0].params.partner_name : PROD_PREBID_ENDPOINT_URL + validBidRequests[0].params.partner_name, + data: JSON.stringify(ortbRequest), + options: { + contentType: 'application/json', + withCredentials: true + }, + bids: validBidRequests +}) + +const createPrebidBannerBid = (bid, bidResponse) => ({ + requestId: bid.bidId, + cpm: bidResponse.price.toFixed(2), + creativeId: bidResponse.crid, + width: bidResponse.w, + height: bidResponse.h, + ttl: 360, + netRevenue: bid.netRevenue === 'net', + currency: 'USD', + ad: bidResponse.adm, + mediaType: 'banner', + winUrl: bidResponse.nurl, + adomain: bidResponse.adomain +}) + +const createPrebidNativeBid = (bid, bidResponse) => ({ + requestId: bid.bidId, + cpm: bidResponse.price.toFixed(2), + creativeId: bidResponse.crid, + currency: 'USD', + ttl: 360, + netRevenue: bid.netRevenue === 'net', + native: parseNative(bidResponse), + mediaType: 'native', + winUrl: bidResponse.nurl, + width: bidResponse.w, + height: bidResponse.h, + adomain: bidResponse.adomain +}) + +const parseNative = (bid) => { + let adm = JSON.parse(bid.adm) + const { assets, link, imptrackers, jstracker } = adm.native; + const result = { + clickUrl: _encodeURIComponent(link.url), + clickTrackers: link.clicktrackers || [], + impressionTrackers: imptrackers || [], + javascriptTrackers: jstracker ? [ jstracker ] : [] + }; + assets.forEach(asset => { + if (!isEmpty(asset.title)) { + result.title = asset.title.text + } else if (!isEmpty(asset.img)) { + result[ORTB_NATIVE_TYPE_MAPPING.img[asset.img.type]] = { + url: asset.img.url, + height: asset.img.h, + width: asset.img.w + } + } else if (!isEmpty(asset.data)) { + result[ORTB_NATIVE_TYPE_MAPPING.data[asset.data.type]] = asset.data.value + } + }); + + return result; +} + +const createPrebidVideoBid = (bid, bidResponse) => { + let videoBid = { + requestId: bid.bidId, + cpm: bidResponse.price.toFixed(2), + creativeId: bidResponse.crid, + width: bidResponse.w, + height: bidResponse.h, + ttl: 360, + netRevenue: bid.netRevenue === 'net', + currency: 'USD', + mediaType: 'video', + winUrl: bidResponse.nurl, + adomain: bidResponse.adomain + }; + + let videoContext = bid.mediaTypes.video.context; + switch (videoContext) { + case OUTSTREAM: + videoBid.vastXml = bidResponse.adm; + break; + case INSTREAM: + videoBid.videoCacheKey = bidResponse.ext.bidder.cacheKey; + videoBid.vastUrl = bidResponse.ext.bidder.vastUrl; + break; + } + return videoBid; +} + +const getQueryVariable = (variable) => { + let query = window.location.search.substring(1); + let vars = query.split('&'); + for (var i = 0; i < vars.length; i++) { + let pair = vars[i].split('='); + if (decodeURIComponent(pair[0]) == variable) { + return decodeURIComponent(pair[1]); + } + } + return false; +} + +export const spec = { + code: BIDDER_CODE, + + aliases: [BIDDER_ALIAS], + + supportedMediaTypes: SUPPORTED_MEDIA_TYPES, + + isBidRequestValid: bid => !!bid.params.placement_id && !!bid.params.publisher_id && !!bid.params.partner_name, + + buildRequests: function (validBidRequests, bidderRequest) { + let serverRequests = []; + + // ORTB Request. + let ortbReq = createOrtbRequest(validBidRequests, bidderRequest); + + validBidRequests.forEach((bid) => { + let imp = createOrtbImpObj(bid) + ortbReq.imp.push(imp); + }); + + serverRequests.push({ ...createServerRequest(ortbReq, validBidRequests, getQueryVariable('dh_test')) }); + + return serverRequests; + }, + + interpretResponse: function (serverResponse, request) { + const { seatbid } = serverResponse.body; + let bids = request.bids; + let prebidResponse = []; + + let seatBids = seatbid[0].bid; + + seatBids.forEach(ortbResponseBid => { + let bidId = ortbResponseBid.impid; + let actualBid = find(bids, (bid) => bid.bidId === bidId); + let bidMediaType = ortbResponseBid.ext.prebid.type + switch (bidMediaType) { + case mediaTypes.BANNER: + prebidResponse.push(createPrebidBannerBid(actualBid, ortbResponseBid)); + break; + case mediaTypes.NATIVE: + prebidResponse.push(createPrebidNativeBid(actualBid, ortbResponseBid)); + break; + case mediaTypes.VIDEO: + prebidResponse.push(createPrebidVideoBid(actualBid, ortbResponseBid)); + break; + } + }) + return prebidResponse; + }, + + onBidWon: function(bid) { + ajax(bid.winUrl, null, null, { + method: 'GET' + }) + } +} + +registerBidder(spec); diff --git a/modules/dailyhuntBidAdapter.md b/modules/dailyhuntBidAdapter.md index acfd20a4de0..a08b66fb826 100644 --- a/modules/dailyhuntBidAdapter.md +++ b/modules/dailyhuntBidAdapter.md @@ -99,3 +99,7 @@ Dailyhunt bid adapter supports Banner, Native and Video. } ]; ``` + +## latest commit has all the required support for latest version of prebid above 6.x +## Dailyhunt adapter was there till 4.x and then removed in version 5.x of prebid. +## this doc has been also submitted to https://github.com/prebid/prebid.github.io \ No newline at end of file diff --git a/modules/datablocksBidAdapter.js b/modules/datablocksBidAdapter.js index 43039e070c3..b240db1dd25 100644 --- a/modules/datablocksBidAdapter.js +++ b/modules/datablocksBidAdapter.js @@ -4,7 +4,7 @@ import { config } from '../src/config.js'; import { BANNER, NATIVE } from '../src/mediaTypes.js'; import { getStorageManager } from '../src/storageManager.js'; import { ajax } from '../src/ajax.js'; -export const storage = getStorageManager(); +export const storage = getStorageManager({bidderCode: 'datablocks'}); const NATIVE_ID_MAP = {}; const NATIVE_PARAMS = { diff --git a/modules/dchain.js b/modules/dchain.js new file mode 100644 index 00000000000..fbe78fc5c86 --- /dev/null +++ b/modules/dchain.js @@ -0,0 +1,149 @@ +import {includes} from '../src/polyfill.js'; +import {config} from '../src/config.js'; +import {getHook} from '../src/hook.js'; +import {_each, deepAccess, deepClone, hasOwn, isArray, isPlainObject, isStr, logError, logWarn} from '../src/utils.js'; + +const shouldBeAString = ' should be a string'; +const shouldBeAnObject = ' should be an object'; +const shouldBeAnArray = ' should be an Array'; +const shouldBeValid = ' is not a valid dchain property'; +const MODE = { + STRICT: 'strict', + RELAXED: 'relaxed', + OFF: 'off' +}; +const MODES = []; // an array of modes +_each(MODE, mode => MODES.push(mode)); + +export function checkDchainSyntax(bid, mode) { + let dchainObj = deepClone(bid.meta.dchain); + let failPrefix = 'Detected something wrong in bid.meta.dchain object for bid:'; + let failMsg = ''; + const dchainPropList = ['ver', 'complete', 'nodes', 'ext']; + + function appendFailMsg(msg) { + failMsg += '\n' + msg; + } + + function printFailMsg() { + if (mode === MODE.STRICT) { + logError(failPrefix, bid, '\n', dchainObj, failMsg); + } else { + logWarn(failPrefix, bid, `\n`, dchainObj, failMsg); + } + } + + let dchainProps = Object.keys(dchainObj); + dchainProps.forEach(prop => { + if (!includes(dchainPropList, prop)) { + appendFailMsg(`dchain.${prop}` + shouldBeValid); + } + }); + + if (dchainObj.complete !== 0 && dchainObj.complete !== 1) { + appendFailMsg(`dchain.complete should be 0 or 1`); + } + + if (!isStr(dchainObj.ver)) { + appendFailMsg(`dchain.ver` + shouldBeAString); + } + + if (hasOwn(dchainObj, 'ext')) { + if (!isPlainObject(dchainObj.ext)) { + appendFailMsg(`dchain.ext` + shouldBeAnObject); + } + } + + if (!isArray(dchainObj.nodes)) { + appendFailMsg(`dchain.nodes` + shouldBeAnArray); + printFailMsg(); + if (mode === MODE.STRICT) return false; + } else { + const nodesPropList = ['asi', 'bsid', 'rid', 'name', 'domain', 'ext']; + dchainObj.nodes.forEach((node, index) => { + if (!isPlainObject(node)) { + appendFailMsg(`dchain.nodes[${index}]` + shouldBeAnObject); + } else { + let nodeProps = Object.keys(node); + nodeProps.forEach(prop => { + if (!includes(nodesPropList, prop)) { + appendFailMsg(`dchain.nodes[${index}].${prop}` + shouldBeValid); + } + + if (prop === 'ext') { + if (!isPlainObject(node.ext)) { + appendFailMsg(`dchain.nodes[${index}].ext` + shouldBeAnObject); + } + } else { + if (!isStr(node[prop])) { + appendFailMsg(`dchain.nodes[${index}].${prop}` + shouldBeAString); + } + } + }); + } + }); + } + + if (failMsg.length > 0) { + printFailMsg(); + if (mode === MODE.STRICT) { + return false; + } + } + return true; +} + +function isValidDchain(bid) { + let mode = MODE.STRICT; + const dchainConfig = config.getConfig('dchain'); + + if (dchainConfig && isStr(dchainConfig.validation) && MODES.indexOf(dchainConfig.validation) != -1) { + mode = dchainConfig.validation; + } + + if (mode === MODE.OFF) { + return true; + } else { + return checkDchainSyntax(bid, mode); + } +} + +export function addBidResponseHook(fn, adUnitCode, bid) { + const basicDchain = { + ver: '1.0', + complete: 0, + nodes: [] + }; + + if (deepAccess(bid, 'meta.networkId') && deepAccess(bid, 'meta.networkName')) { + basicDchain.nodes.push({ name: bid.meta.networkName, bsid: bid.meta.networkId.toString() }); + } + basicDchain.nodes.push({ name: bid.bidderCode }); + + let bidDchain = deepAccess(bid, 'meta.dchain'); + if (bidDchain && isPlainObject(bidDchain)) { + let result = isValidDchain(bid); + + if (result) { + // extra check in-case mode is OFF and there is a setup issue + if (isArray(bidDchain.nodes)) { + bid.meta.dchain.nodes.push({ asi: bid.bidderCode }); + } else { + logWarn('bid.meta.dchain.nodes did not exist or was not an array; did not append prebid dchain.', bid); + } + } else { + // remove invalid dchain + delete bid.meta.dchain; + } + } else { + bid.meta.dchain = basicDchain; + } + + fn(adUnitCode, bid); +} + +export function init() { + getHook('addBidResponse').before(addBidResponseHook, 35); +} + +init(); diff --git a/modules/dchain.md b/modules/dchain.md new file mode 100644 index 00000000000..f01b3483f3c --- /dev/null +++ b/modules/dchain.md @@ -0,0 +1,45 @@ +# dchain module + +Refer: +- https://iabtechlab.com/buyers-json-demand-chain/ + +## Sample code for dchain setConfig and dchain object +``` +pbjs.setConfig({ + "dchain": { + "validation": "strict" + } +}); +``` + +``` +bid.meta.dchain: { + "complete": 0, + "ver": "1.0", + "ext": {...}, + "nodes": [{ + "asi": "abc", + "bsid": "123", + "rid": "d4e5f6", + "name": "xyz", + "domain": "mno", + "ext": {...} + }, ...] +} +``` + +## Workflow +The dchain module is not enabled by default as it may not be necessary for all publishers. +If required, dchain module can be included as following +``` + $ gulp build --modules=dchain,pubmaticBidAdapter,openxBidAdapter,rubiconBidAdapter,sovrnBidAdapter +``` + +The dchain module will validate a bidder's dchain object (if it was defined). Bidders should assign their dchain object into `bid.meta` field. If the dchain object is valid, it will remain in the bid object for later use. + +If it was not defined, the dchain will create a default dchain object for prebid. + +## Validation modes +- ```strict```: It is the default validation mode. In this mode, dchain object will not be accpeted from adapters if it is invalid. Errors are thrown for invalid dchain object. +- ```relaxed```: In this mode, errors are thrown for an invalid dchain object but the invalid dchain object is still accepted from adapters. +- ```off```: In this mode, no validations are performed and dchain object is accepted as is from adapters. \ No newline at end of file diff --git a/modules/debugging/bidInterceptor.js b/modules/debugging/bidInterceptor.js new file mode 100644 index 00000000000..2a179641424 --- /dev/null +++ b/modules/debugging/bidInterceptor.js @@ -0,0 +1,229 @@ +import { + deepAccess, + deepClone, + deepEqual, + delayExecution, + prefixLog, + mergeDeep +} from '../../src/utils.js'; +const { logMessage, logWarn, logError } = prefixLog('DEBUG:'); + +/** + * @typedef {Number|String|boolean|null|undefined} Scalar + */ + +export function BidInterceptor(opts = {}) { + ({setTimeout: this.setTimeout = window.setTimeout.bind(window)} = opts); + this.rules = []; +} + +Object.assign(BidInterceptor.prototype, { + DEFAULT_RULE_OPTIONS: { + delay: 0 + }, + serializeConfig(ruleDefs) { + function isSerializable(ruleDef, i) { + const serializable = deepEqual(ruleDef, JSON.parse(JSON.stringify(ruleDef)), {checkTypes: true}); + if (!serializable && !deepAccess(ruleDef, 'options.suppressWarnings')) { + logWarn(`Bid interceptor rule definition #${i + 1} is not serializable and will be lost after a refresh. Rule definition: `, ruleDef); + } + return serializable; + } + return ruleDefs.filter(isSerializable); + }, + updateConfig(config) { + this.rules = (config.intercept || []).map((ruleDef, i) => this.rule(ruleDef, i + 1)) + }, + /** + * @typedef {Object} RuleOptions + * @property {Number} [delay=0] delay between bid interception and mocking of response (to simulate network delay) + * @property {boolean} [suppressWarnings=false] if enabled, do not warn about unserializable rules + * + * @typedef {Object} Rule + * @property {Number} no rule number (used only as an identifier for logging) + * @property {function({}, {}): boolean} match a predicate function that tests a bid against this rule + * @property {ReplacerFn} replacer generator function for mock bid responses + * @property {RuleOptions} options + */ + + /** + * @param {{}} ruleDef + * @param {Number} ruleNo + * @returns {Rule} + */ + rule(ruleDef, ruleNo) { + return { + no: ruleNo, + match: this.matcher(ruleDef.when, ruleNo), + replace: this.replacer(ruleDef.then || {}, ruleNo), + options: Object.assign({}, this.DEFAULT_RULE_OPTIONS, ruleDef.options), + } + }, + /** + * @typedef {Function} MatchPredicate + * @param {*} candidate a bid to match, or a portion of it if used inside an ObjectMather. + * e.g. matcher((bid, bidRequest) => ....) or matcher({property: (property, bidRequest) => ...}) + * @param {BidRequest} bidRequest the request `candidate` belongs to + * @returns {boolean} + * + * @typedef {{[key]: Scalar|RegExp|MatchPredicate|ObjectMatcher}} ObjectMatcher + */ + + /** + * @param {MatchPredicate|ObjectMatcher} matchDef matcher definition + * @param {Number} ruleNo + * @returns {MatchPredicate} a predicate function that matches a bid against the given `matchDef` + */ + matcher(matchDef, ruleNo) { + if (typeof matchDef === 'function') { + return matchDef; + } + if (typeof matchDef !== 'object') { + logError(`Invalid 'when' definition for debug bid interceptor (in rule #${ruleNo})`); + return () => false; + } + function matches(candidate, {ref = matchDef, args = []}) { + return Object.entries(ref).map(([key, val]) => { + const cVal = candidate[key]; + if (val instanceof RegExp) { + return val.exec(cVal) != null; + } + if (typeof val === 'function') { + return !!val(cVal, ...args); + } + if (typeof val === 'object') { + return matches(cVal, {ref: val, args}); + } + return cVal === val; + }).every((i) => i); + } + return (candidate, ...args) => matches(candidate, {args}); + }, + /** + * @typedef {Function} ReplacerFn + * @param {*} bid a bid that was intercepted + * @param {BidRequest} bidRequest the request `bid` belongs to + * @returns {*} the response to mock for `bid`, or a portion of it if used inside an ObjectReplacer. + * e.g. replacer((bid, bidRequest) => mockResponse) or replacer({property: (bid, bidRequest) => mockProperty}) + * + * @typedef {{[key]: ReplacerFn|ObjectReplacer|*}} ObjectReplacer + */ + + /** + * @param {ReplacerFn|ObjectReplacer} replDef replacer definition + * @param ruleNo + * @return {ReplacerFn} + */ + replacer(replDef, ruleNo) { + let replFn; + if (typeof replDef === 'function') { + replFn = ({args}) => replDef(...args); + } else if (typeof replDef !== 'object') { + logError(`Invalid 'then' definition for debug bid interceptor (in rule #${ruleNo})`); + replFn = () => ({}); + } else { + replFn = ({args, ref = replDef}) => { + const result = {}; + Object.entries(ref).forEach(([key, val]) => { + if (typeof val === 'function') { + result[key] = val(...args); + } else if (typeof val === 'object') { + result[key] = replFn({args, ref: val}) + } else { + result[key] = val; + } + }); + return result; + } + } + return (bid, ...args) => { + const response = this.responseDefaults(bid); + mergeDeep(response, replFn({args: [bid, ...args]})); + if (!response.ad) { + response.ad = this.defaultAd(bid, response); + } + response.isDebug = true; + return response; + } + }, + responseDefaults(bid) { + return { + requestId: bid.bidId, + cpm: 3.5764, + currency: 'EUR', + width: 300, + height: 250, + ttl: 360, + creativeId: 'mock-creative-id', + netRevenue: false, + meta: {} + }; + }, + defaultAd(bid, bidResponse) { + return ``; + }, + /** + * Match a candidate bid against all registered rules. + * + * @param {{}} candidate + * @param args + * @returns {Rule|undefined} the first matching rule, or undefined if no match was found. + */ + match(candidate, ...args) { + return this.rules.find((rule) => rule.match(candidate, ...args)); + }, + /** + * Match a set of bids against all registered rules. + * + * @param bids + * @param bidRequest + * @returns {[{bid: *, rule: Rule}[], *[]]} a 2-tuple for matching bids (decorated with the matching rule) and + * non-matching bids. + */ + matchAll(bids, bidRequest) { + const [matches, remainder] = [[], []]; + bids.forEach((bid) => { + const rule = this.match(bid, bidRequest); + if (rule != null) { + matches.push({rule: rule, bid: bid}); + } else { + remainder.push(bid); + } + }) + return [matches, remainder]; + }, + /** + * Run a set of bids against all registered rules, filter out those that match, + * and generate mock responses for them. + * + * @param {{}[]} bids? + * @param {BidRequest} bidRequest + * @param {function(*)} addBid called once for each mock response + * @param {function()} done called once after all mock responses have been run through `addBid` + * @returns {{bids: {}[], bidRequest: {}} remaining bids that did not match any rule (this applies also to + * bidRequest.bids) + */ + intercept({bids, bidRequest, addBid, done}) { + if (bids == null) { + bids = bidRequest.bids; + } + const [matches, remainder] = this.matchAll(bids, bidRequest); + if (matches.length > 0) { + const callDone = delayExecution(done, matches.length); + matches.forEach((match) => { + const mockResponse = match.rule.replace(match.bid, bidRequest); + const delay = match.rule.options.delay; + logMessage(`Intercepted bid request (matching rule #${match.rule.no}), mocking response in ${delay}ms. Request, response:`, match.bid, mockResponse) + this.setTimeout(() => { + addBid(mockResponse, match.bid); + callDone(); + }, delay) + }); + bidRequest = deepClone(bidRequest); + bids = bidRequest.bids = remainder; + } else { + this.setTimeout(done, 0); + } + return {bids, bidRequest}; + } +}); diff --git a/modules/debugging/index.js b/modules/debugging/index.js new file mode 100644 index 00000000000..72692c3fc98 --- /dev/null +++ b/modules/debugging/index.js @@ -0,0 +1,62 @@ +import {deepClone, delayExecution} from '../../src/utils.js'; +import {processBidderRequests} from '../../src/adapters/bidderFactory.js'; +import {BidInterceptor} from './bidInterceptor.js'; +import {hook} from '../../src/hook.js'; +import {pbsBidInterceptor} from './pbsInterceptor.js'; +import { + onDisableOverrides, + onEnableOverrides, + saveDebuggingConfig +} from '../../src/debugging.js'; + +const interceptorHooks = []; +const bidInterceptor = new BidInterceptor(); + +saveDebuggingConfig.before(function (next, debugConfig, ...args) { + if (debugConfig.intercept) { + debugConfig = deepClone(debugConfig); + debugConfig.intercept = bidInterceptor.serializeConfig(debugConfig.intercept); + } + next(debugConfig, ...args); +}); + +function resetHooks(enable) { + interceptorHooks.forEach(([getHookFn, interceptor]) => { + getHookFn().getHooks({hook: interceptor}).remove(); + }); + if (enable) { + interceptorHooks.forEach(([getHookFn, interceptor]) => { + getHookFn().before(interceptor); + }) + } +} + +onEnableOverrides.push((overrides) => { + bidInterceptor.updateConfig(overrides); + resetHooks(true); +}); + +onDisableOverrides.push(() => { + bidInterceptor.updateConfig({}); + resetHooks(false); +}) + +function registerBidInterceptor(getHookFn, interceptor) { + const interceptBids = (...args) => bidInterceptor.intercept(...args); + interceptorHooks.push([getHookFn, function (next, ...args) { + interceptor(next, interceptBids, ...args) + }]); +} + +export function bidderBidInterceptor(next, interceptBids, spec, bids, bidRequest, ajax, wrapCallback, cbs) { + const done = delayExecution(cbs.onCompletion, 2); + ({bids, bidRequest} = interceptBids({bids, bidRequest, addBid: cbs.onBid, done})); + if (bids.length === 0) { + done(); + } else { + next(spec, bids, bidRequest, ajax, wrapCallback, {...cbs, onCompletion: done}); + } +} + +registerBidInterceptor(() => processBidderRequests, bidderBidInterceptor); +registerBidInterceptor(() => hook.get('processPBSRequest'), pbsBidInterceptor); diff --git a/modules/debugging/pbsInterceptor.js b/modules/debugging/pbsInterceptor.js new file mode 100644 index 00000000000..5af2384cad9 --- /dev/null +++ b/modules/debugging/pbsInterceptor.js @@ -0,0 +1,38 @@ +import {deepClone, delayExecution} from '../../src/utils.js'; +import {createBid} from '../../src/bidfactory.js'; +import * as CONSTANTS from '../../src/constants.json'; + +export function pbsBidInterceptor (next, interceptBids, s2sBidRequest, bidRequests, ajax, { + onResponse, + onError, + onBid +}) { + let responseArgs; + const done = delayExecution(() => onResponse(...responseArgs), bidRequests.length + 1) + function signalResponse(...args) { + responseArgs = args; + done(); + } + function addBid(bid, bidRequest) { + onBid({ + adUnit: bidRequest.adUnitCode, + bid: Object.assign(createBid(CONSTANTS.STATUS.GOOD, bidRequest), bid) + }) + } + bidRequests = bidRequests + .map((req) => interceptBids({bidRequest: req, addBid, done}).bidRequest) + .filter((req) => req.bids.length > 0) + + if (bidRequests.length > 0) { + const bidIds = new Set(); + bidRequests.forEach((req) => req.bids.forEach((bid) => bidIds.add(bid.bidId))); + s2sBidRequest = deepClone(s2sBidRequest); + s2sBidRequest.ad_units.forEach((unit) => { + unit.bids = unit.bids.filter((bid) => bidIds.has(bid.bid_id)); + }) + s2sBidRequest.ad_units = s2sBidRequest.ad_units.filter((unit) => unit.bids.length > 0); + next(s2sBidRequest, bidRequests, ajax, {onResponse: signalResponse, onError, onBid}); + } else { + signalResponse(true, []); + } +} diff --git a/modules/deepintentDpesIdSystem.js b/modules/deepintentDpesIdSystem.js index 375c8c07ed1..43c7af1b3cc 100644 --- a/modules/deepintentDpesIdSystem.js +++ b/modules/deepintentDpesIdSystem.js @@ -9,7 +9,7 @@ import { submodule } from '../src/hook.js'; import { getStorageManager } from '../src/storageManager.js'; const MODULE_NAME = 'deepintentId'; -export const storage = getStorageManager(null, MODULE_NAME); +export const storage = getStorageManager({gvlid: null, moduleName: MODULE_NAME}); /** @type {Submodule} */ export const deepintentDpesSubmodule = { diff --git a/modules/dfpAdServerVideo.js b/modules/dfpAdServerVideo.js index 79cb03ec001..7f8ad3351fa 100644 --- a/modules/dfpAdServerVideo.js +++ b/modules/dfpAdServerVideo.js @@ -9,7 +9,7 @@ import { config } from '../src/config.js'; import { getHook, submodule } from '../src/hook.js'; import { auctionManager } from '../src/auctionManager.js'; import { gdprDataHandler, uspDataHandler } from '../src/adapterManager.js'; -import events from '../src/events.js'; +import * as events from '../src/events.js'; import CONSTANTS from '../src/constants.json'; /** @@ -88,7 +88,14 @@ export function buildDfpVideoUrl(options) { sz: parseSizesInput(deepAccess(adUnit, 'mediaTypes.video.playerSize')).join('|'), url: encodeURIComponent(location.href), }; - const encodedCustomParams = getCustParams(bid, options); + + const urlSearchComponent = urlComponents.search; + const urlSzParam = urlSearchComponent && urlSearchComponent.sz + if (urlSzParam) { + derivedParams.sz = urlSzParam + '|' + derivedParams.sz; + } + + let encodedCustomParams = getCustParams(bid, options, urlSearchComponent && urlSearchComponent.cust_params); const queryParams = Object.assign({}, defaultParamConstants, @@ -111,12 +118,11 @@ export function buildDfpVideoUrl(options) { const uspConsent = uspDataHandler.getConsentData(); if (uspConsent) { queryParams.us_privacy = uspConsent; } - return buildUrl({ + return buildUrl(Object.assign({ protocol: 'https', host: 'securepubads.g.doubleclick.net', - pathname: '/gampad/ads', - search: queryParams - }); + pathname: '/gampad/ads' + }, urlComponents, { search: queryParams })); } export function notifyTranslationModule(fn) { @@ -227,9 +233,7 @@ function buildUrlFromAdserverUrlComponents(components, bid, options) { const descriptionUrl = getDescriptionUrl(bid, components, 'search'); if (descriptionUrl) { components.search.description_url = descriptionUrl; } - const encodedCustomParams = getCustParams(bid, options); - components.search.cust_params = (components.search.cust_params) ? components.search.cust_params + '%26' + encodedCustomParams : encodedCustomParams; - + components.search.cust_params = getCustParams(bid, options, components.search.cust_params); return buildUrl(components); } @@ -258,7 +262,7 @@ function getDescriptionUrl(bid, components, prop) { * @param {Object} options this is the options passed in from the `buildDfpVideoUrl` function * @return {Object} Encoded key value pairs for cust_params */ -function getCustParams(bid, options) { +function getCustParams(bid, options, urlCustParams) { const adserverTargeting = (bid && bid.adserverTargeting) || {}; let allTargetingData = {}; @@ -281,7 +285,12 @@ function getCustParams(bid, options) { // merge the prebid + publisher targeting sets const publisherTargetingSet = deepAccess(options, 'params.cust_params'); const targetingSet = Object.assign({}, prebidTargetingSet, publisherTargetingSet); - return encodeURIComponent(formatQS(targetingSet)); + let encodedParams = encodeURIComponent(formatQS(targetingSet)); + if (urlCustParams) { + encodedParams = urlCustParams + '%26' + encodedParams; + } + + return encodedParams; } registerVideoSupport('dfp', { diff --git a/modules/displayioBidAdapter.js b/modules/displayioBidAdapter.js new file mode 100644 index 00000000000..55a2f4a8604 --- /dev/null +++ b/modules/displayioBidAdapter.js @@ -0,0 +1,157 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; + +const BIDDER_VERSION = '1.0.0'; +const BIDDER_CODE = 'displayio'; +const GVLID = 999; +const BID_TTL = 300; +const SUPPORTED_AD_TYPES = [BANNER, VIDEO]; +const DEFAULT_CURRENCY = 'USD'; + +export const spec = { + code: BIDDER_CODE, + gvlid: GVLID, + supportedMediaTypes: SUPPORTED_AD_TYPES, + isBidRequestValid: function(bid) { + return !!(bid.params && bid.params.placementId && bid.params.siteId && + bid.params.adsSrvDomain && bid.params.cdnDomain); + }, + buildRequests: function (bidRequests, bidderRequest) { + return bidRequests.map(bid => { + let url = '//' + bid.params.adsSrvDomain + '/srv?method=getPlacement&app=' + + bid.params.siteId + '&placement=' + bid.params.placementId; + const data = this._getPayload(bid, bidderRequest); + return { + method: 'POST', + headers: {'Content-Type': 'application/json;charset=utf-8'}, + url, + data + }; + }); + }, + interpretResponse: function (serverResponse, serverRequest) { + const ads = serverResponse.body.data.ads; + const bidResponses = []; + const { data } = serverRequest.data; + if (ads.length) { + const adData = ads[0].ad.data; + const bidResponse = { + requestId: data.id, + cpm: adData.ecpm, + width: adData.w, + height: adData.h, + netRevenue: true, + ttl: BID_TTL, + creativeId: adData.adId || 0, + currency: DEFAULT_CURRENCY, + referrer: data.data.ref, + mediaType: ads[0].ad.subtype, + ad: adData.markup, + placement: data.placement, + adData: adData + }; + if (bidResponse.vastUrl === 'videoVast') { + bidResponse.vastUrl = adData.videos[0].url + } + bidResponses.push(bidResponse); + } + return bidResponses; + }, + _getPayload: function (bid, bidderRequest) { + const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection; + const userSession = 'us_web_xxxxxxxxxxxx'.replace(/[x]/g, c => { + let r = Math.random() * 16 | 0; + let v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + const { params } = bid; + const { siteId, placementId } = params; + const { refererInfo, uspConsent, gdprConsent } = bidderRequest; + const mediation = {consent: '-1', gdpr: '-1'}; + if (gdprConsent) { + if (gdprConsent.consentString !== undefined) { + mediation.consent = gdprConsent.consentString; + } + if (gdprConsent.gdprApplies !== undefined) { + mediation.gdpr = gdprConsent.gdprApplies ? '1' : '0'; + } + } + const payload = { + userSession, + data: { + id: bid.bidId, + action: 'getPlacement', + app: siteId, + placement: placementId, + data: { + pagecat: params.pageCategory ? params.pageCategory.split(',').map(k => k.trim()) : [], + keywords: params.keywords ? params.keywords.split(',').map(k => k.trim()) : [], + lang_content: document.documentElement.lang, + lang: window.navigator.language, + domain: window.location.hostname, + page: window.location.href, + ref: refererInfo.referer, + userids: _getUserIDs(), + geo: '', + }, + complianceData: { + child: '-1', + us_privacy: uspConsent, + dnt: window.navigator.doNotTrack, + iabConsent: {}, + mediation: { + consent: mediation.consent, + gdpr: mediation.gdpr, + } + }, + integration: 'JS', + omidpn: 'Displayio', + mediationPlatform: 0, + prebidVersion: BIDDER_VERSION, + device: { + w: window.screen.width, + h: window.screen.height, + connection_type: connection ? connection.effectiveType : '', + } + } + } + if (navigator.permissions) { + navigator.permissions.query({ name: 'geolocation' }) + .then((result) => { + if (result.state === 'granted') { + payload.data.data.geo = _getGeoData(); + } + }); + } + return payload + } +}; + +function _getUserIDs () { + let ids = {}; + try { + ids = window.owpbjs.getUserIdsAsEids(); + } catch (e) {} + return ids; +} + +async function _getGeoData () { + let geoData = null; + const getCurrentPosition = () => { + return new Promise((resolve, reject) => + navigator.geolocation.getCurrentPosition(resolve, reject) + ); + } + try { + const position = await getCurrentPosition(); + let {latitude, longitude, accuracy} = position.coords; + geoData = { + 'lat': latitude, + 'lng': longitude, + 'precision': accuracy + }; + } catch (e) {} + return geoData +} + +registerBidder(spec); diff --git a/modules/displayioBidAdapter.md b/modules/displayioBidAdapter.md new file mode 100644 index 00000000000..41505ee966e --- /dev/null +++ b/modules/displayioBidAdapter.md @@ -0,0 +1,148 @@ +# Overview + +``` +Module Name: DisplayIO Bidder Adapter +Module Type: Bidder Adapter +``` + +# Description + +Module that connects to display.io's demand sources. +Web mobile (not relevant for web desktop). + + +#Features +| Feature | | Feature | | +|---------------|---------------------------------------------------------|-----------------------|-----| +| Bidder Code | displayio | Prebid member | no | +| Media Types | Banner, video.
Sizes (display 320x480 / vertical video) | GVL ID | no | +| GDPR Support | yes | Prebid.js Adapter | yes | +| USP Support | yes | Prebid Server Adapter | no | + + +#Global configuration +```javascript + + + + @@ -469,6 +478,7 @@ const renderCreative = (site, auctionId, bid, seat, request) => { const spec = { code: BIDDER_CODE, + gvlid: GVLID, aliases: [], supportedMediaTypes: [BANNER, NATIVE, VIDEO], isBidRequestValid(bid) { @@ -513,6 +523,7 @@ const spec = { applyGdpr(bidderRequest, payload); applyClientHints(payload); + applyUserIds(validBidRequests[0], payload); return { method: 'POST', @@ -558,10 +569,14 @@ const spec = { /* bid response might include ext object containing siteId / slotId, as detected by OneCode update site / slot data in this case + + ext also might contain publisherId and custom ad label */ - const { siteid, slotid } = ext; + const { siteid, slotid, pubid, adlabel } = ext; site.id = siteid || site.id; site.slot = slotid || site.slot; + site.publisherId = pubid; + site.adLabel = adlabel; } if (bidRequest && site.id && !strIncludes(site.id, 'bidid')) { @@ -598,10 +613,28 @@ const spec = { bid.mediaType = 'native'; // check native object try { - const nativeData = JSON.parse(serverBid.adm).native; + const nativeData = serverBid.admNative || JSON.parse(serverBid.adm).native; bid.native = parseNative(nativeData); bid.width = 1; bid.height = 1; + + // append viewability tracker + const jsData = { + rid: bidRequest.auctionId, + crid: bid.creativeId, + adunit: bidRequest.adUnitCode, + url: bid.native.clickUrl, + vendor: seat, + site: site.id, + slot: site.slot, + cpm: bid.cpm.toPrecision(4), + } + const jsTracker = ' + + + `; +} + +registerBidder(spec); diff --git a/modules/targetVideoBidAdapter.md b/modules/targetVideoBidAdapter.md new file mode 100644 index 00000000000..557c9f94410 --- /dev/null +++ b/modules/targetVideoBidAdapter.md @@ -0,0 +1,34 @@ +# Overview + +``` +Module Name: Target Video Bid Adapter +Module Type: Bidder Adapter +Maintainer: grajzer@gmail.com +``` + +# Description + +Connects to Appnexus exchange for bids. + +TargetVideo bid adapter supports Banner. + +# Test Parameters +``` +var adUnits = [ + // Banner adUnit + { + code: 'banner-div', + mediaTypes: { + banner: { + sizes: [[640, 480], [300, 250]], + } + }, + bids: [{ + bidder: 'targetVideo', + params: { + placementId: 13232361 + } + }] + } +]; +``` diff --git a/modules/teadsBidAdapter.js b/modules/teadsBidAdapter.js index ea581905883..a8902c896f6 100644 --- a/modules/teadsBidAdapter.js +++ b/modules/teadsBidAdapter.js @@ -12,7 +12,7 @@ const gdprStatus = { CMP_NOT_FOUND_OR_ERROR: 22 }; const FP_TEADS_ID_COOKIE_NAME = '_tfpvi'; -export const storage = getStorageManager(GVL_ID, BIDDER_CODE); +export const storage = getStorageManager({gvlid: GVL_ID, bidderCode: BIDDER_CODE}); export const spec = { code: BIDDER_CODE, @@ -190,8 +190,7 @@ function buildRequestObject(bid) { const reqObj = {}; let placementId = getValue(bid.params, 'placementId'); let pageId = getValue(bid.params, 'pageId'); - const impressionData = deepAccess(bid, 'ortb2Imp.ext.data'); - const gpid = deepAccess(impressionData, 'pbadslot') || deepAccess(impressionData, 'adserver.adslot'); + const gpid = deepAccess(bid, 'ortb2Imp.ext.gpid'); reqObj.sizes = getSizes(bid); reqObj.bidId = getBidIdParameter('bidId', bid); diff --git a/modules/telariaBidAdapter.js b/modules/telariaBidAdapter.js index 560bf762394..42913414cbc 100644 --- a/modules/telariaBidAdapter.js +++ b/modules/telariaBidAdapter.js @@ -1,8 +1,6 @@ import { logError, isEmpty, deepAccess, triggerPixel, logWarn, isArray } from '../src/utils.js'; -import {createBid as createBidFactory} from '../src/bidfactory.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {VIDEO} from '../src/mediaTypes.js'; -import {STATUS} from '../src/constants.json'; const BIDDER_CODE = 'telaria'; const DOMAIN = 'tremorhub.com'; @@ -11,7 +9,11 @@ const EVENTS_ENDPOINT = `events.${DOMAIN}/diag`; export const spec = { code: BIDDER_CODE, - aliases: ['tremor', 'tremorvideo'], + gvlid: 52, + aliases: [ + { code: 'tremor', gvlid: 52 }, + { code: 'tremorvideo', gvlid: 52 } + ], supportedMediaTypes: [VIDEO], /** * Determines if the request is valid @@ -86,7 +88,9 @@ export const spec = { logError(errorMessage); } else if (!isEmpty(bidResult.seatbid)) { bidResult.seatbid[0].bid.forEach(tag => { - bids.push(createBid(STATUS.GOOD, bidderRequest, tag, width, height, BIDDER_CODE)); + if (tag) { + bids.push(createBid(bidderRequest, tag, width, height)); + } }); } @@ -253,38 +257,31 @@ function generateUrl(bid, bidderRequest) { } /** - * Create and return a bid object based on status and tag - * @param status + * Create and return a bid response * @param reqBid * @param response * @param width * @param height - * @param bidderCode */ -function createBid(status, reqBid, response, width, height, bidderCode) { - let bid = createBidFactory(status, reqBid); - +function createBid(reqBid, response, width, height) { // TTL 5 mins by default, future support for extended imp wait time - if (response) { - Object.assign(bid, { - requestId: reqBid.bidId, - cpm: response.price, - creativeId: response.crid || '-1', - vastXml: response.adm, - vastUrl: reqBid.vastUrl, - mediaType: 'video', - width: width, - height: height, - bidderCode: bidderCode, - currency: 'USD', - netRevenue: true, - ttl: 300, - ad: response.adm - }); - } - - bid.meta = bid.meta || {}; - if (response && response.adomain && response.adomain.length > 0) { + const bid = { + requestId: reqBid.bidId, + cpm: response.price, + creativeId: response.crid || '-1', + vastXml: response.adm, + vastUrl: reqBid.vastUrl, + mediaType: 'video', + width: width, + height: height, + currency: 'USD', + netRevenue: true, + ttl: 300, + ad: response.adm, + meta: {} + }; + + if (response.adomain && response.adomain.length > 0) { bid.meta.advertiserDomains = response.adomain; } diff --git a/modules/trionBidAdapter.js b/modules/trionBidAdapter.js index dd1624f90d7..5750406116b 100644 --- a/modules/trionBidAdapter.js +++ b/modules/trionBidAdapter.js @@ -2,12 +2,11 @@ import { getBidIdParameter, parseSizesInput, tryAppendQueryString } from '../src import {registerBidder} from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; -const storage = getStorageManager(); - const BID_REQUEST_BASE_URL = 'https://in-appadvertising.com/api/bidRequest'; const USER_SYNC_URL = 'https://in-appadvertising.com/api/userSync.html'; const BIDDER_CODE = 'trion'; const BASE_KEY = '_trion_'; +const storage = getStorageManager({bidderCode: BIDDER_CODE}); export const spec = { code: BIDDER_CODE, diff --git a/modules/trustpidSystem.js b/modules/trustpidSystem.js new file mode 100644 index 00000000000..051775fa777 --- /dev/null +++ b/modules/trustpidSystem.js @@ -0,0 +1,197 @@ +/** + * This module adds TrustPid provided by Vodafone Sales and Services Limited to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/trustpidSystem + * @requires module:modules/userId + */ +import { logInfo, logError } from '../src/utils.js'; +import { submodule } from '../src/hook.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const MODULE_NAME = 'trustpid'; +const LOG_PREFIX = 'Trustpid module' +let mnoAcronym = ''; +let mnoDomain = ''; + +export const storage = getStorageManager({gvlid: null, moduleName: MODULE_NAME}); + +/** + * Handle an event for an iframe. + * Takes the body.url parameter from event and returns the string domain. + * i.e.: "fc.vodafone.de" + * @param event + */ +function messageHandler(event) { + let msg; + try { + if (event && event.data && typeof event.data === 'string' && event.data) { + msg = JSON.parse(event.data); + if (msg.msgType === 'MNOSELECTOR' && msg.body && msg.body.url) { + let URL = msg.body.url.split('//'); + let domainURL = URL[1].split('/'); + mnoDomain = domainURL[0]; + logInfo(`${LOG_PREFIX}: Message handler set domain to ${mnoDomain}`); + getDomainAcronym(mnoDomain); + } + } + } catch (e) { + logError(e); + } +} + +/** + * Properly sets the trustpid acronym depending on the domain value. + * @param domain + */ +function getDomainAcronym(domain) { + let acronym = ''; + const prefix = '-'; + switch (domain) { + case 'tmi.mno.link': + acronym = 'ndye'; + break; + case 'tmi.vodafone.de': + acronym = 'pqnx'; + break; + case 'tmi.telekom.de': + acronym = 'avgw'; + break; + case 'tmi.tmid.es': + acronym = 'kjws'; + break; + case 'uat.mno.link': + acronym = 'xxxx'; + break; + case 'es.tmiservice.orange.com': + acronym = 'aplw'; + break; + default: + return 'none'; + } + return mnoAcronym = prefix + acronym; +} + +// Set a listener to handle the iframe response message. +window.addEventListener('message', messageHandler, false); + +/** + * Get the "umid" from html5 local storage to make it available to the UserId module. + * @param config + * @returns {{trustpid: (*|string), acr: (string)}} + */ +function getTrustpidFromStorage() { + // Get the domain either from localStorage or global + let domain = JSON.parse(storage.getDataFromLocalStorage('fcIdConnectDomain')) || mnoDomain; + logInfo(`${LOG_PREFIX}: Local storage domain: ${domain}`); + + if (!domain) { + logInfo(`${LOG_PREFIX}: Local storage domain not found, returning null`); + return { + trustpid: null, + acr: null, + }; + } + + // Get the acronym from global + let acronym = mnoAcronym; + // if acronym is empty, but "domain" is available, get the acronym from domain + if (!acronym) { + getDomainAcronym(domain); + acronym = mnoAcronym; + } + + logInfo(`${LOG_PREFIX}: Domain acronym found: ${acronym}`); + + // Domain is correct in both local storage and idGraph, but no acronym is existing for the domain + if (domain && !acronym) { + return { + trustpid: null, + acr: null + } + } + + let fcIdConnectObject; + let fcIdConnectData = JSON.parse(storage.getDataFromLocalStorage('fcIdConnectData')); + logInfo(`${LOG_PREFIX}: Local storage fcIdConnectData: ${JSON.stringify(fcIdConnectData)}`); + + if (fcIdConnectData && + fcIdConnectData.connectId && + Array.isArray(fcIdConnectData.connectId.idGraph) && + fcIdConnectData.connectId.idGraph.length > 0) { + fcIdConnectObject = fcIdConnectData.connectId.idGraph.find(item => { + return item.domain === domain; + }); + } + logInfo(`${LOG_PREFIX}: Local storage fcIdConnectObject for domain: ${JSON.stringify(fcIdConnectObject)}`); + + return { + trustpid: (fcIdConnectObject && fcIdConnectObject.umid) + ? fcIdConnectObject.umid + : null, + acr: acronym, + }; +} + +/** @type {Submodule} */ +export const trustpidSubmodule = { + /** + * Used to link submodule with config + * @type {string} + */ + name: MODULE_NAME, + /** + * Decodes the stored id value for passing to bid requests. + * @function + * @returns {{trustpid: string} | null} + */ + decode(bidId) { + logInfo(`${LOG_PREFIX}: Decoded ID value ${JSON.stringify(bidId)}`); + return bidId.trustpid ? bidId : null; + }, + /** + * Get the id from helper function and initiate a new user sync. + * @param config + * @returns {{callback: result}|{id: {trustpid: string}}} + */ + getId: function(config) { + const data = getTrustpidFromStorage(); + if (data.trustpid) { + logInfo(`${LOG_PREFIX}: Local storage ID value ${JSON.stringify(data)}`); + return {id: {trustpid: data.trustpid + data.acr}}; + } else { + if (!config) { + config = {}; + } + if (!config.params) { + config.params = {}; + } + if (typeof config.params.maxDelayTime === 'undefined' || config.params.maxDelayTime === null) { + config.params.maxDelayTime = 1000; + } + // Current delay and delay step in milliseconds + let currentDelay = 0; + const delayStep = 50; + const result = (callback) => { + const data = getTrustpidFromStorage(); + if (!data.trustpid) { + if (currentDelay > config.params.maxDelayTime) { + logInfo(`${LOG_PREFIX}: No trustpid value set after ${config.params.maxDelayTime} max allowed delay time`); + callback(null); + } else { + currentDelay += delayStep; + setTimeout(() => { + result(callback); + }, delayStep); + } + } else { + const dataToReturn = { trustpid: data.trustpid + data.acr }; + logInfo(`${LOG_PREFIX}: Returning ID value data of ${JSON.stringify(dataToReturn)}`); + callback(dataToReturn); + } + }; + return { callback: result }; + } + }, +}; + +submodule('userId', trustpidSubmodule); diff --git a/modules/trustpidSystem.md b/modules/trustpidSystem.md new file mode 100644 index 00000000000..c4309c9d807 --- /dev/null +++ b/modules/trustpidSystem.md @@ -0,0 +1,45 @@ +## trustpid User Id Submodule + +trustpid User Id Module. + +First, make sure to add the trustpid submodule to your Prebid.js package with: + +``` +gulp build --modules=userId,adfBidAdapter,trustpidSystem +``` + +The following configuration parameters are available: + +``` +pbjs.setConfig({ + userSync: { + userIds: [ + { + name: 'trustpid', + params: { + maxDelayTime: 1000, + }, + bidders: ["adf"], + storage: { + type: "html5", + name: "trustpid", + expires: 1, //days + }, + } + ], + } +}); +``` + +## Parameter Descriptions + +| Param under userSync.userIds[] | Scope | Type | Description | Example | +| --- | --- | --- | --- | --- | +| name | Required | String | The name of the module | `"trustpid"` +| params | Required | Object | Object with configuration parameters for trustpid User Id submodule | - | +| params.maxDelayTime | Required | Integer | Max amount of time (in seconds) before looking into storage for data | 2500 | +| bidders | Required | Array of Strings | An array of bidder codes to which this user ID may be sent. Currently required and supporting AdformOpenRTB | `["adf"]` | +| storage | Required | Object | Local storage configuration object | - | +| storage.type | Required | String | Type of the storage that would be used to store user ID. Must be `"html5"` to utilise HTML5 local storage. | `"html5"` | +| storage.name | Required | String | The name of the key in local storage where the user ID will be stored. | `"trustpid"` | +| storage.expires | Required | Integer | How long (in days) the user ID information will be stored. For safety reasons, this information is required.| `1` | \ No newline at end of file diff --git a/modules/trustxBidAdapter.js b/modules/trustxBidAdapter.js index b2f0936e3c7..7d40a0b0452 100644 --- a/modules/trustxBidAdapter.js +++ b/modules/trustxBidAdapter.js @@ -1,4 +1,4 @@ -import { isEmpty, deepAccess, logError, logWarn, parseGPTSingleSizeArrayToRtbSize } from '../src/utils.js'; +import {isEmpty, deepAccess, logError, logWarn, parseGPTSingleSizeArrayToRtbSize, mergeDeep} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import { Renderer } from '../src/Renderer.js'; import { VIDEO, BANNER } from '../src/mediaTypes.js'; @@ -6,9 +6,8 @@ import { config } from '../src/config.js'; const BIDDER_CODE = 'trustx'; const ENDPOINT_URL = 'https://grid.bidswitch.net/hbjson?sp=trustx'; -const ADDITIONAL_SYNC_URL = 'https://x.bidswitch.net/sync?ssp=themediagrid'; const TIME_TO_LIVE = 360; -const ADAPTER_SYNC_URL = 'https://sofia.trustx.org/push_sync'; +const ADAPTER_SYNC_URL = 'https://x.bidswitch.net/sync?ssp=themediagrid'; const RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; const LOG_ERROR_MESS = { @@ -22,6 +21,7 @@ const LOG_ERROR_MESS = { hasEmptySeatbidArray: 'Response has empty seatbid array', hasNoArrayOfBids: 'Seatbid from response has no array of bid objects - ' }; + export const spec = { code: BIDDER_CODE, supportedMediaTypes: [ BANNER, VIDEO ], @@ -102,12 +102,17 @@ export const spec = { } }; - if (ortb2Imp && ortb2Imp.ext && ortb2Imp.ext.data) { - impObj.ext.data = ortb2Imp.ext.data; - if (impObj.ext.data.adserver && impObj.ext.data.adserver.adslot) { - impObj.ext.gpid = impObj.ext.data.adserver.adslot.toString(); - } else { - impObj.ext.gpid = ortb2Imp.ext.data.pbadslot && ortb2Imp.ext.data.pbadslot.toString(); + if (ortb2Imp) { + if (ortb2Imp.instl) { + impObj.instl = ortb2Imp.instl; + } + if (ortb2Imp.ext && ortb2Imp.ext.data) { + impObj.ext.data = ortb2Imp.ext.data; + if (impObj.ext.data.adserver && impObj.ext.data.adserver.adslot) { + impObj.ext.gpid = impObj.ext.data.adserver.adslot.toString(); + } else { + impObj.ext.gpid = ortb2Imp.ext.data.pbadslot && ortb2Imp.ext.data.pbadslot.toString(); + } } } @@ -169,16 +174,25 @@ export const spec = { request.site.content = content; } - const userData = []; - addSegments('iow_labs_pub_data', 'jwpseg', jwpseg, userData); - addSegments('permutive', 'p_standard', permutiveseg, userData, 'permutive.com'); - - if (userData.length) { + if (jwpseg && jwpseg.length) { user = { - data: userData + data: [{ + name: 'iow_labs_pub_data', + segment: segmentProcessing(jwpseg, 'jwpseg'), + }] }; } + const ortb2UserData = config.getConfig('ortb2.user.data'); + if (ortb2UserData && ortb2UserData.length) { + if (!user) { + user = { data: [] }; + } + user = mergeDeep(user, { + data: [...ortb2UserData] + }); + } + if (gdprConsent && gdprConsent.consentString) { userExt = {consent: gdprConsent.consentString}; } @@ -281,12 +295,12 @@ export const spec = { }, getUserSyncs: function(syncOptions, responses, gdprConsent, uspConsent) { if (syncOptions.pixelEnabled) { - const syncsPerBidder = config.getConfig('userSync.syncsPerBidder'); let params = []; - if (gdprConsent && typeof gdprConsent.consentString === 'string') { + if (gdprConsent) { if (typeof gdprConsent.gdprApplies === 'boolean') { - params.push(`gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`); - } else { + params.push(`gdpr=${Number(gdprConsent.gdprApplies)}`); + } + if (typeof gdprConsent.consentString === 'string') { params.push(`gdpr_consent=${gdprConsent.consentString}`); } } @@ -294,17 +308,10 @@ export const spec = { params.push(`us_privacy=${uspConsent}`); } const stringParams = params.join('&'); - const syncs = [{ + return { type: 'image', - url: ADAPTER_SYNC_URL + (stringParams ? `?${stringParams}` : '') - }]; - if (syncsPerBidder > 1) { - syncs.push({ - type: 'image', - url: ADDITIONAL_SYNC_URL + (stringParams ? `&${stringParams}` : '') - }); - } - return syncs; + url: ADAPTER_SYNC_URL + stringParams + }; } } } @@ -434,34 +441,20 @@ function createBannerRequest(bid, mediaType) { return result; } -function addSegments(name, segName, segments, data, bidConfigName) { - if (segments && segments.length) { - data.push({ - name: name, - segment: segments - .map((seg) => seg && (seg.id || seg)) - .filter((seg) => seg && (typeof seg === 'string' || typeof seg === 'number')) - .map((seg) => ({ name: segName, value: seg.toString() })) - }); - } else if (bidConfigName) { - const configData = config.getConfig('ortb2.user.data'); - let segData = null; - configData && configData.some(({name, segment}) => { - if (name === bidConfigName) { - segData = segment; - return true; +function segmentProcessing(segment, forceSegName) { + return segment + .map((seg) => { + const value = seg && (seg.value || seg.id || seg); + if (typeof value === 'string' || typeof value === 'number') { + return { + value: value.toString(), + ...(forceSegName && { name: forceSegName }), + ...(seg.name && { name: seg.name }), + }; } - }); - if (segData && segData.length) { - data.push({ - name: name, - segment: segData - .map((seg) => seg && (seg.id || seg)) - .filter((seg) => seg && (typeof seg === 'string' || typeof seg === 'number')) - .map((seg) => ({ name: segName, value: seg.toString() })) - }); - } - } + return null; + }) + .filter((seg) => !!seg); } function reformatKeywords(pageKeywords) { diff --git a/modules/ttdBidAdapter.js b/modules/ttdBidAdapter.js new file mode 100644 index 00000000000..4919442336f --- /dev/null +++ b/modules/ttdBidAdapter.js @@ -0,0 +1,519 @@ +import * as utils from '../src/utils.js'; +import { config } from '../src/config.js'; +import { createEidsArray } from './userId/eids.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; + +const BIDADAPTERVERSION = 'TTD-PREBID-2022.02.18'; +const BIDDER_CODE = 'ttd'; +const BIDDER_CODE_LONG = 'thetradedesk'; +const BIDDER_ENDPOINT = 'https://direct.adsrvr.org/bid/bidder/'; +const USER_SYNC_ENDPOINT = 'https://match.adsrvr.org'; + +const MEDIA_TYPE = { + BANNER: 1, + VIDEO: 2 +}; + +function getExt(firstPartyData) { + const ext = { + ver: BIDADAPTERVERSION, + pbjs: '$prebid.version$', + keywords: firstPartyData.site?.keywords ? firstPartyData.site.keywords.split(',').map(k => k.trim()) : [] + } + return { + ttdprebid: ext + }; +} + +function getRegs(bidderRequest) { + let regs = {}; + + if (bidderRequest.gdprConsent && typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') { + utils.deepSetValue(regs, 'ext.gdpr', bidderRequest.gdprConsent.gdprApplies ? 1 : 0); + } + if (bidderRequest.uspConsent) { + utils.deepSetValue(regs, 'ext.us_privacy', bidderRequest.uspConsent); + } + if (config.getConfig('coppa') === true) { + regs.coppa = 1; + } + return regs; +} + +function getBidFloor(bid) { + if (!utils.isFn(bid.getFloor)) { + return null; + } + + let floor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*' + }); + if (utils.isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === 'USD') { + return floor.floor; + } + return null; +} + +function getSource(validBidRequests) { + let source = {}; + if (validBidRequests[0].schain) { + utils.deepSetValue(source, 'ext.schain', validBidRequests[0].schain); + } + return source; +} + +function getDevice() { + const language = navigator.language || navigator.browserLanguage || navigator.userLanguage || navigator.systemLanguage; + let device = { + ua: navigator.userAgent, + dnt: utils.getDNT() ? 1 : 0, + language: language, + connectiontype: getConnectionType() + }; + + return device; +}; + +function getConnectionType() { + const connection = navigator.connection || navigator.webkitConnection; + if (!connection) { + return 0; + } + switch (connection.type) { + case 'ethernet': + return 1; + case 'wifi': + return 2; + case 'cellular': + switch (connection.effectiveType) { + case 'slow-2g': + case '2g': + return 4; + case '3g': + return 5; + case '4g': + return 6; + default: + return 3; + } + default: + return 0; + } +} + +function getUser(bidderRequest) { + let user = {}; + if (bidderRequest.gdprConsent) { + utils.deepSetValue(user, 'ext.consent', bidderRequest.gdprConsent.consentString); + } + + if (utils.isStr(utils.deepAccess(bidderRequest, 'bids.0.userId.tdid'))) { + user.buyeruid = bidderRequest.bids[0].userId.tdid; + } + + var eids = createEidsArray(utils.deepAccess(bidderRequest, 'bids.0.userId')) + if (eids.length) { + utils.deepSetValue(user, 'ext.eids', eids); + } + + return user; +} + +function getSite(bidderRequest, firstPartyData) { + var site = { + id: utils.deepAccess(bidderRequest, 'bids.0.params.siteId'), + page: utils.deepAccess(bidderRequest, 'refererInfo.referer'), + publisher: { + id: utils.deepAccess(bidderRequest, 'bids.0.params.publisherId'), + }, + ...firstPartyData.site + }; + + var publisherDomain = config.getConfig('publisherDomain'); + if (publisherDomain) { + utils.deepSetValue(site, 'publisher.domain', publisherDomain); + } + return site; +} + +function getImpression(bidRequest) { + let impression = { + id: bidRequest.bidId, + tagid: bidRequest.params.placementId + }; + + let gpid = utils.deepAccess(bidRequest, 'ortb2Imp.ext.gpid'); + if (gpid) { + impression.ext = { + gpid: gpid + } + } + + const mediaTypesVideo = utils.deepAccess(bidRequest, 'mediaTypes.video'); + const mediaTypesBanner = utils.deepAccess(bidRequest, 'mediaTypes.banner'); + + let mediaTypes = {}; + if (mediaTypesBanner) { + mediaTypes[BANNER] = banner(bidRequest); + } + if (mediaTypesVideo) { + mediaTypes[VIDEO] = video(bidRequest); + } + + Object.assign(impression, mediaTypes); + + let bidfloor = getBidFloor(bidRequest); + if (bidfloor) { + impression.bidfloor = parseFloat(bidfloor); + impression.bidfloorcur = 'USD'; + } + + return impression; +} + +function getSizes(sizes) { + const sizeStructs = utils.parseSizesInput(sizes) + .filter(x => x) // sizes that don't conform are returned as null, which we want to ignore + .map(x => x.split('x')) + .map(size => { + return { + width: parseInt(size[0]), + height: parseInt(size[1]), + } + }); + + return sizeStructs; +} + +function banner(bid) { + const sizes = getSizes(bid.mediaTypes.banner.sizes).map(x => { + return { + w: x.width, + h: x.height, + } + }); + const pos = parseInt(utils.deepAccess(bid, 'mediaTypes.banner.pos')); + const expdir = utils.deepAccess(bid, 'params.banner.expdir'); + let optionalParams = {}; + if (pos) { + optionalParams.pos = pos; + } + if (expdir && Array.isArray(expdir)) { + optionalParams.expdir = expdir; + } + + const banner = Object.assign( + { + w: sizes[0].w, + h: sizes[0].h, + format: sizes, + }, + optionalParams); + return banner; +} + +function video(bid) { + let minduration = utils.deepAccess(bid, 'mediaTypes.video.minduration'); + const maxduration = utils.deepAccess(bid, 'mediaTypes.video.maxduration'); + const playerSize = utils.deepAccess(bid, 'mediaTypes.video.playerSize'); + const api = utils.deepAccess(bid, 'mediaTypes.video.api'); + const mimes = utils.deepAccess(bid, 'mediaTypes.video.mimes'); + const placement = utils.deepAccess(bid, 'mediaTypes.video.placement'); + const protocols = utils.deepAccess(bid, 'mediaTypes.video.protocols'); + const playbackmethod = utils.deepAccess(bid, 'mediaTypes.video.playbackmethod'); + const pos = utils.deepAccess(bid, 'mediaTypes.video.pos'); + const startdelay = utils.deepAccess(bid, 'mediaTypes.video.startdelay'); + const skip = utils.deepAccess(bid, 'mediaTypes.video.skip'); + const skipmin = utils.deepAccess(bid, 'mediaTypes.video.skipmin'); + const skipafter = utils.deepAccess(bid, 'mediaTypes.video.skipafter'); + const minbitrate = utils.deepAccess(bid, 'mediaTypes.video.minbitrate'); + const maxbitrate = utils.deepAccess(bid, 'mediaTypes.video.maxbitrate'); + + if (!minduration || !utils.isInteger(minduration)) { + minduration = 0 + } + let video = { + minduration: minduration, + maxduration: maxduration, + api: api, + mimes: mimes, + placement: placement, + protocols: protocols + }; + + if (typeof playerSize !== 'undefined') { + if (utils.isArray(playerSize[0])) { + video.w = parseInt(playerSize[0][0]); + video.h = parseInt(playerSize[0][1]); + } else if (utils.isNumber(playerSize[0])) { + video.w = parseInt(playerSize[0]); + video.h = parseInt(playerSize[1]); + } + } + + if (playbackmethod) { + video.playbackmethod = playbackmethod; + } + if (pos) { + video.pos = pos; + } + if (startdelay && utils.isInteger(startdelay)) { + video.startdelay = startdelay; + } + if (skip && (skip === 0 || skip === 1)) { + video.skip = skip; + } + if (skipmin && utils.isInteger(skipmin)) { + video.skipmin = skipmin; + } + if (skipafter && utils.isInteger(skipafter)) { + video.skipafter = skipafter; + } + if (minbitrate && utils.isInteger(minbitrate)) { + video.minbitrate = minbitrate; + } + if (maxbitrate && utils.isInteger(maxbitrate)) { + video.maxbitrate = maxbitrate; + } + + return video; +} + +export const spec = { + code: BIDDER_CODE, + gvlid: 21, + aliases: [BIDDER_CODE_LONG], + supportedMediaTypes: [BANNER, VIDEO], + + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (bid) { + const alphaRegex = /^[\w+]+$/; + + // required parameters + if (!bid || !bid.params) { + utils.logWarn(BIDDER_CODE + ': Missing bid parameters'); + return false; + } + if (!bid.params.supplySourceId) { + utils.logWarn(BIDDER_CODE + ': Missing required parameter params.supplySourceId'); + return false; + } + if (!alphaRegex.test(bid.params.supplySourceId)) { + utils.logWarn(BIDDER_CODE + ': supplySourceId must only contain alphabetic characters'); + return false; + } + if (!bid.params.publisherId) { + utils.logWarn(BIDDER_CODE + ': Missing required parameter params.publisherId'); + return false; + } + if (bid.params.publisherId.length > 32) { + utils.logWarn(BIDDER_CODE + ': params.publisherId must be 32 characters or less'); + return false; + } + if (!bid.params.siteId) { + utils.logWarn(BIDDER_CODE + ': Missing required parameter params.siteId'); + return false; + } + if (bid.params.siteId.length > 50) { + utils.logWarn(BIDDER_CODE + ': params.siteId must be 50 characters or less'); + return false; + } + if (!bid.params.placementId) { + utils.logWarn(BIDDER_CODE + ': Missing required parameter params.placementId'); + return false; + } + if (bid.params.placementId.length > 128) { + utils.logWarn(BIDDER_CODE + ': params.placementId must be 128 characters or less'); + return false; + } + + const mediaTypesBanner = utils.deepAccess(bid, 'mediaTypes.banner'); + const mediaTypesVideo = utils.deepAccess(bid, 'mediaTypes.video'); + + if (!mediaTypesBanner && !mediaTypesVideo) { + utils.logWarn(BIDDER_CODE + ': one of mediaTypes.banner or mediaTypes.video must be passed'); + return false; + } + + if (mediaTypesVideo) { + if (!mediaTypesVideo.maxduration || !utils.isInteger(mediaTypesVideo.maxduration)) { + utils.logWarn(BIDDER_CODE + ': mediaTypes.video.maxduration must be set to the maximum video ad duration in seconds'); + return false; + } + if (!mediaTypesVideo.api || mediaTypesVideo.api.length === 0) { + utils.logWarn(BIDDER_CODE + ': mediaTypes.video.api should be an array of supported api frameworks. See the Open RTB v2.5 spec for valid values'); + return false; + } + if (!mediaTypesVideo.mimes || mediaTypesVideo.mimes.length === 0) { + utils.logWarn(BIDDER_CODE + ': mediaTypes.video.mimes should be an array of supported mime types'); + return false; + } + if (!mediaTypesVideo.protocols) { + utils.logWarn(BIDDER_CODE + ': mediaTypes.video.protocols should be an array of supported protocols. See the Open RTB v2.5 spec for valid values') + return false; + } + } + + return true; + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} an array of validBidRequests + * @param {*} bidderRequest + * @return {ServerRequest} Info describing the request to the server. + */ + buildRequests: function (validBidRequests, bidderRequest) { + const firstPartyData = config.getConfig('ortb2') || {}; + let topLevel = { + id: bidderRequest.auctionId, + imp: validBidRequests.map(bidRequest => getImpression(bidRequest)), + site: getSite(bidderRequest, firstPartyData), + device: getDevice(), + user: getUser(bidderRequest), + at: 1, + cur: ['USD'], + regs: getRegs(bidderRequest), + source: getSource(validBidRequests), + ext: getExt(firstPartyData) + } + + let url = BIDDER_ENDPOINT + bidderRequest.bids[0].params.supplySourceId; + + let serverRequest = { + method: 'POST', + url: url, + data: topLevel, + options: { + withCredentials: true + } + }; + + return serverRequest; + }, + + /** + * Format responses as Prebid bid responses + * + * Each bid can have the following elements: + * - requestId (required) + * - cpm (required) + * - width (required) + * - height (required) + * - ad (required) + * - ttl (required) + * - creativeId (required) + * - netRevenue (required) + * - currency (required) + * - vastUrl + * - vastImpUrl + * - vastXml + * - dealId + * + * @param {ttdResponseObj} bidResponse A successful response from ttd. + * @param {ServerRequest} serverRequest The result of buildRequests() that lead to this response. + * @return {Bid[]} An array of formatted bids. + */ + interpretResponse: function (response, serverRequest) { + let seatBidsInResponse = utils.deepAccess(response, 'body.seatbid'); + const currency = utils.deepAccess(response, 'body.cur'); + if (!seatBidsInResponse || seatBidsInResponse.length === 0) { + return []; + } + let bidResponses = []; + let requestedImpressions = utils.deepAccess(serverRequest, 'data.imp'); + + seatBidsInResponse.forEach(seatBid => { + seatBid.bid.forEach(bid => { + let matchingRequestedImpression = requestedImpressions.find(imp => imp.id === bid.impid); + + const cpm = bid.price || 0; + let bidResponse = { + requestId: bid.impid, + cpm: cpm, + creativeId: bid.crid, + dealId: bid.dealid || null, + currency: currency || 'USD', + netRevenue: true, + ttl: bid.ttl || 360, + meta: {}, + }; + + if (bid.adomain && bid.adomain.length > 0) { + bidResponse.meta.advertiserDomains = bid.adomain; + } + + if (bid.ext.mediatype === MEDIA_TYPE.BANNER) { + Object.assign( + bidResponse, + { + width: bid.w, + height: bid.h, + ad: utils.replaceAuctionPrice(bid.adm, cpm), + mediaType: BANNER + } + ); + } else if (bid.ext.mediatype === MEDIA_TYPE.VIDEO) { + Object.assign( + bidResponse, + { + width: matchingRequestedImpression.video.w, + height: matchingRequestedImpression.video.h, + mediaType: VIDEO + } + ); + if (bid.nurl) { + bidResponse.vastUrl = utils.replaceAuctionPrice(bid.nurl, cpm); + } else { + bidResponse.vastXml = utils.replaceAuctionPrice(bid.adm, cpm); + } + } + + bidResponses.push(bidResponse); + }); + }); + + return bidResponses; + }, + + /** + * Register the user sync pixels which should be dropped after the auction. + * + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} serverResponses List of server's responses. + * @param {gdprConsent} gdprConsent GDPR consent object + * @param {uspConsent} uspConsent USP consent object + * @return {UserSync[]} The user syncs which should be dropped. + */ + getUserSyncs: function(syncOptions, serverResponses, gdprConsent = {}, uspConsent = '') { + const syncs = []; + + let gdprParams = `&gdpr=${gdprConsent.gdprApplies ? 1 : 0}&gdpr_consent=${encodeURIComponent(gdprConsent.consentString)}`; + + let url = `${USER_SYNC_ENDPOINT}/track/usersync?us_privacy=${encodeURIComponent(uspConsent)}${gdprParams}`; + + if (syncOptions.pixelEnabled) { + syncs.push({ + type: 'image', + url: url + '&ust=image' + }); + } else if (syncOptions.iframeEnabled) { + syncs.push({ + type: 'iframe', + url: url + '&ust=iframe' + }); + } + return syncs; + }, +}; + +registerBidder(spec) diff --git a/modules/ttdBidAdapter.md b/modules/ttdBidAdapter.md new file mode 100644 index 00000000000..9c67f3267cb --- /dev/null +++ b/modules/ttdBidAdapter.md @@ -0,0 +1,122 @@ +# Overview + +``` +Module Name: The Trade Desk Bidder Adapter +Module Type: Bidder Adapter +Maintainer: prebid-maintainers@thetradedesk.com +``` + +# Description + +Module that connects to The Trade Desk's demand sources to fetch bids. + +The Trade Desk bid adapter supports Banner and Video. + +# Test Parameters + +```js + var adUnits = [ + // Banner adUnit with only required parameters + { + code: 'test-div-minimal', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [ + { + bidder: 'ttd', + params: { + supplySourceId: 'supplier', + publisherId: '1427ab10f2e448057ed3b422', + siteId: 'site-123', + placementId: 'footer1' + } + } + ] + }, + // Banner adUnit with all optional parameters provided + { + code: 'test-div-banner-optional-params', + mediaTypes: { + banner: { + sizes: [[728, 90]], + pos: 1 + } + }, + bids: [ + { + bidder: 'ttd', + params: { + supplySourceId: 'supplier', + publisherId: '1427ab10f2e448057ed3b422', + siteId: 'site-123', + placementId: 'footer1', + banner: { + expdir: [1, 3] + }, + } + } + ] + }, + // Video adUnit with only required parameters + { + code: 'test-div-video-minimal', + mediaTypes: { + video: { + maxduration: 30, + api: [1, 3], + mimes: ['video/mp4'], + placement: 3, + protocols: [2,3,5,6] + } + }, + bids: [ + { + bidder: 'ttd', + params: { + supplySourceId: 'supplier', + publisherId: '1427ab10f2e448057ed3b422', + siteId: 'site-123', + placementId: 'footer1' + } + } + ] + }, + // Video adUnit with all optional parameters provided + { + code: 'test-div-video-full', + mediaTypes: { + video: { + minduration: 1, + maxduration: 10, + playerSize: [640, 480], + api: [1, 3], + mimes: ['video/mp4'], + placement: 3, + protocols: [2, 3, 5, 6], + startdelay: 1, + playbackmethod: [1], + pos: 1, + minbitrate: 100, + maxbitrate: 500, + skip: 1, + skipmin: 5, + skipafter: 10 + } + }, + bids: [ + { + bidder: 'ttd', + params: { + supplySourceId: 'supplier', + publisherId: '1427ab10f2e448057ed3b422', + siteId: 'site-123', + placementId: 'footer1' + } + } + ] + } + ]; +``` diff --git a/modules/ucfunnelBidAdapter.js b/modules/ucfunnelBidAdapter.js index 8b85f1ebad3..ec087d005d6 100644 --- a/modules/ucfunnelBidAdapter.js +++ b/modules/ucfunnelBidAdapter.js @@ -3,7 +3,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO, NATIVE} from '../src/mediaTypes.js'; import { getStorageManager } from '../src/storageManager.js'; import { config } from '../src/config.js'; -const storage = getStorageManager(); + const COOKIE_NAME = 'ucf_uid'; const VER = 'ADGENT_PREBID-2018011501'; const BIDDER_CODE = 'ucfunnel'; @@ -13,6 +13,7 @@ const VIDEO_CONTEXT = { INSTREAM: 0, OUSTREAM: 2 } +const storage = getStorageManager({bidderCode: BIDDER_CODE}); export const spec = { code: BIDDER_CODE, diff --git a/modules/uid2IdSystem.js b/modules/uid2IdSystem.js index c0cd9166784..23656639532 100644 --- a/modules/uid2IdSystem.js +++ b/modules/uid2IdSystem.js @@ -23,7 +23,7 @@ function readFromLocalStorage() { } function getStorage() { - return getStorageManager(GVLID, MODULE_NAME); + return getStorageManager({gvlid: GVLID, moduleName: MODULE_NAME}); } const storage = getStorage(); diff --git a/modules/undertoneBidAdapter.js b/modules/undertoneBidAdapter.js index fda6f47b2af..f86faf3fe4d 100644 --- a/modules/undertoneBidAdapter.js +++ b/modules/undertoneBidAdapter.js @@ -12,6 +12,20 @@ const FRAME_USER_SYNC = 'https://cdn.undertone.com/js/usersync.html'; const PIXEL_USER_SYNC_1 = 'https://usr.undertone.com/userPixel/syncOne?id=1&of=2'; const PIXEL_USER_SYNC_2 = 'https://usr.undertone.com/userPixel/syncOne?id=2&of=2'; +function getBidFloor(bidRequest, mediaType) { + if (typeof bidRequest.getFloor !== 'function') { + return 0; + } + + const floor = bidRequest.getFloor({ + currency: 'USD', + mediaType: mediaType, + size: '*' + }); + + return (floor && floor.currency === 'USD' && floor.floor) || 0; +} + function getCanonicalUrl() { try { let doc = window.top.document; @@ -134,6 +148,9 @@ export const spec = { params: bidReq.params }; const videoMediaType = deepAccess(bidReq, 'mediaTypes.video'); + const mediaType = videoMediaType ? VIDEO : BANNER; + bid.mediaType = mediaType; + bid.bidfloor = getBidFloor(bidReq, mediaType); if (videoMediaType) { bid.video = { playerSize: deepAccess(bidReq, 'mediaTypes.video.playerSize') || null, @@ -142,7 +159,6 @@ export const spec = { maxDuration: deepAccess(bidReq, 'params.video.maxDuration') || null, skippable: deepAccess(bidReq, 'params.video.skippable') || null }; - bid.mediaType = 'video'; } payload['x-ut-hb-params'].push(bid); }); diff --git a/modules/unicornBidAdapter.js b/modules/unicornBidAdapter.js index 0209c808979..977e694acf7 100644 --- a/modules/unicornBidAdapter.js +++ b/modules/unicornBidAdapter.js @@ -3,12 +3,12 @@ import {BANNER} from '../src/mediaTypes.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {getStorageManager} from '../src/storageManager.js'; -const storage = getStorageManager(); const BIDDER_CODE = 'unicorn'; const UNICORN_ENDPOINT = 'https://ds.uncn.jp/pb/0/bid.json'; const UNICORN_DEFAULT_CURRENCY = 'JPY'; const UNICORN_PB_COOKIE_KEY = '__pb_unicorn_aud'; const UNICORN_PB_VERSION = '1.1'; +const storage = getStorageManager({bidderCode: BIDDER_CODE}); /** * Placement ID and Account ID are required. diff --git a/modules/unrulyBidAdapter.js b/modules/unrulyBidAdapter.js index 99fbe63aeb4..77160bb01e6 100644 --- a/modules/unrulyBidAdapter.js +++ b/modules/unrulyBidAdapter.js @@ -193,6 +193,7 @@ const isBannerMediaTypeValid = (mediaTypeBannerData) => { export const adapter = { code: 'unruly', supportedMediaTypes: [VIDEO, BANNER], + gvlid: 36, isBidRequestValid: function (bid) { let siteId = deepAccess(bid, 'params.siteId'); let isBidValid = siteId && isMediaTypesValid(bid); diff --git a/modules/userId/eids.js b/modules/userId/eids.js index 87b6ecc1f1c..5d138795bd8 100644 --- a/modules/userId/eids.js +++ b/modules/userId/eids.js @@ -1,10 +1,19 @@ import { pick, isFn, isStr, isPlainObject, deepAccess } from '../../src/utils.js'; // Each user-id sub-module is expected to mention respective config here -const USER_IDS_CONFIG = { +export const USER_IDS_CONFIG = { // key-name : {config} + // trustpid + 'trustpid': { + source: 'trustpid.com', + atype: 1, + getValue: function (data) { + return data; + }, + }, + // intentIqId 'intentIqId': { source: 'intentiq.com', @@ -17,6 +26,12 @@ const USER_IDS_CONFIG = { atype: 1 }, + // justId + 'justId': { + source: 'justtag.com', + atype: 1 + }, + // pubCommonId 'pubcid': { source: 'pubcid.org', @@ -48,6 +63,20 @@ const USER_IDS_CONFIG = { } }, + // ftrack + 'ftrackId': { + source: 'flashtalking.com', + atype: 1, + getValue: function(data) { + return data.uid + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + }, + // parrableId 'parrableId': { source: 'parrable.com', @@ -146,7 +175,13 @@ const USER_IDS_CONFIG = { atype: 1 }, - // haloId + // hadronId + 'hadronId': { + source: 'audigent.com', + atype: 1 + }, + + // haloId (deprecated in 7.0, use hadronId) 'haloId': { source: 'audigent.com', atype: 1 @@ -257,7 +292,19 @@ const USER_IDS_CONFIG = { 'connectId': { source: 'yahoo.com', atype: 3 - } + }, + + // Adquery ID + 'qid': { + source: 'adquery.io', + atype: 1 + }, + + // DAC ID + 'dacId': { + source: 'impact-ad.jp', + atype: 1 + }, }; // this function will create an eid object for the given UserId sub-module diff --git a/modules/userId/eids.md b/modules/userId/eids.md index 679bf5ffe27..a61ef66c56c 100644 --- a/modules/userId/eids.md +++ b/modules/userId/eids.md @@ -2,6 +2,14 @@ ``` userIdAsEids = [ + { + source: 'trustpid.com', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }] + }, + { source: 'pubcid.org', uids: [{ @@ -28,7 +36,15 @@ userIdAsEids = [ atype: 1 }] }, - + + { + source: 'justtag.com', + uids: [{ + id: 'justId', + atype: 1 + }] + }, + { source: 'neustar.biz', uids: [{ @@ -49,6 +65,13 @@ userIdAsEids = [ }] }, + { + source: 'flashtalking.com', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }, + { source: 'parrable.com', uids: [{ @@ -203,7 +226,7 @@ userIdAsEids = [ id: 'some-random-id-value', atype: 3 }] - }, + }, { source: 'kpuid.com', uids: [{ diff --git a/modules/userId/index.js b/modules/userId/index.js index b0293a9c26a..e656673befb 100644 --- a/modules/userId/index.js +++ b/modules/userId/index.js @@ -130,20 +130,32 @@ * @property {(string[]|undefined)} submoduleNames - submodules to refresh */ -import find from 'core-js-pure/features/array/find.js'; -import { config } from '../../src/config.js'; -import events from '../../src/events.js'; -import { getGlobal } from '../../src/prebidGlobal.js'; -import { gdprDataHandler } from '../../src/adapterManager.js'; +import {find, includes} from '../../src/polyfill.js'; +import {config} from '../../src/config.js'; +import * as events from '../../src/events.js'; +import {getGlobal} from '../../src/prebidGlobal.js'; +import {gdprDataHandler} from '../../src/adapterManager.js'; import CONSTANTS from '../../src/constants.json'; -import { module, hook } from '../../src/hook.js'; -import { createEidsArray, buildEidPermissions } from './eids.js'; -import { getCoreStorageManager } from '../../src/storageManager.js'; +import {hook, module} from '../../src/hook.js'; +import {buildEidPermissions, createEidsArray, USER_IDS_CONFIG} from './eids.js'; +import {getCoreStorageManager} from '../../src/storageManager.js'; import { - getPrebidInternal, isPlainObject, logError, isArray, cyrb53Hash, deepAccess, timestamp, delayExecution, logInfo, isFn, - logWarn, isEmptyStr, isNumber + cyrb53Hash, + deepAccess, + delayExecution, + getPrebidInternal, + isArray, + isEmptyStr, + isFn, + isGptPubadsDefined, + isNumber, + isPlainObject, + logError, + logInfo, + logWarn, + timestamp, + isEmpty } from '../../src/utils.js'; -import includes from 'core-js-pure/features/array/includes.js'; const MODULE_NAME = 'User ID'; const COOKIE = 'cookie'; @@ -184,6 +196,9 @@ export let syncDelay; /** @type {(number|undefined)} */ export let auctionDelay; +/** @type {(string|undefined)} */ +let ppidSource; + /** @param {Submodule[]} submodules */ export function setSubmoduleRegistry(submodules) { submoduleRegistry = submodules; @@ -448,6 +463,20 @@ function getCombinedSubmoduleIds(submodules) { return combinedSubmoduleIds; } +/** + * This function will return a submodule ID object for particular source name + * @param {SubmoduleContainer[]} submodules + * @param {string} sourceName + */ +function getSubmoduleId(submodules, sourceName) { + if (!Array.isArray(submodules) || !submodules.length) { + return {}; + } + const submodule = submodules.filter(sub => isPlainObject(sub.idObj) && + Object.keys(sub.idObj).length && USER_IDS_CONFIG[Object.keys(sub.idObj)[0]].source === sourceName) + return !isEmpty(submodule) ? submodule[0].idObj : [] +} + /** * This function will create a combined object for bidder with allowed subModule Ids * @param {SubmoduleContainer[]} submodules @@ -557,6 +586,26 @@ export function requestBidsHook(fn, reqBidsConfigObj) { initializeSubmodulesAndExecuteCallbacks(function () { // pass available user id data to bid adapters addIdDataToAdUnitBids(reqBidsConfigObj.adUnits || getGlobal().adUnits, initializedSubmodules); + + // userSync.ppid should be one of the 'source' values in getUserIdsAsEids() eg pubcid.org or id5-sync.com + const matchingUserId = ppidSource && (getUserIdsAsEids() || []).find(userID => userID.source === ppidSource); + if (matchingUserId && typeof deepAccess(matchingUserId, 'uids.0.id') === 'string') { + const ppidValue = matchingUserId.uids[0].id.replace(/[\W_]/g, ''); + if (ppidValue.length >= 32 && ppidValue.length <= 150) { + if (isGptPubadsDefined()) { + window.googletag.pubads().setPublisherProvidedId(ppidValue); + } else { + window.googletag = window.googletag || {}; + window.googletag.cmd = window.googletag.cmd || []; + window.googletag.cmd.push(function() { + window.googletag.pubads().setPublisherProvidedId(ppidValue); + }); + } + } else { + logWarn(`User ID - Googletag Publisher Provided ID for ${ppidSource} is not between 32 and 150 characters - ${ppidValue}`); + } + } + // calling fn allows prebid to continue processing fn.call(this, reqBidsConfigObj); }); @@ -582,6 +631,79 @@ function getUserIdsAsEids() { return createEidsArray(getCombinedSubmoduleIds(initializedSubmodules)); } +/** + * This function will be exposed in global-name-space so that userIds stored by Prebid UserId module can be used by external codes as well. + * Simple use case will be passing these UserIds to A9 wrapper solution + */ + +function getUserIdsAsEidBySource(sourceName) { + initializeSubmodulesAndExecuteCallbacks(); + return createEidsArray(getSubmoduleId(initializedSubmodules, sourceName))[0]; +}; + +/** + * This function will be exposed in global-name-space so that userIds for a source can be exposed + * Sample use case is exposing this function to ESP + */ +function getEncryptedEidsForSource(source, encrypt, customFunction) { + let eidsSignals = {}; + + if (isFn(customFunction)) { + logInfo(`${MODULE_NAME} - Getting encrypted signal from custom function : ${customFunction.name} & source : ${source} `); + // Publishers are expected to define a common function which will be proxy for signal function. + const customSignals = customFunction(source); + eidsSignals[source] = customSignals ? encryptSignals(customSignals) : null; // by default encrypt using base64 to avoid JSON errors + } else { + // initialize signal with eids by default + const eid = getUserIdsAsEidBySource(source); + logInfo(`${MODULE_NAME} - Getting encrypted signal for eids :${JSON.stringify(eid)}`); + if (!isEmpty(eid)) { + eidsSignals[eid.source] = encrypt === true ? encryptSignals(eid) : eid.uids[0].id; // If encryption is enabled append version (1||) and encrypt entire object + } + } + const promise = Promise.resolve(eidsSignals[source]); + logInfo(`${MODULE_NAME} - Fetching encrypted eids: ${eidsSignals[source]}`); + return promise; +} + +function encryptSignals(signals, version = 1) { + let encryptedSig = ''; + switch (version) { + case 1: // Base64 Encryption + encryptedSig = typeof signals === 'object' ? window.btoa(JSON.stringify(signals)) : window.btoa(signals); // Test encryption. To be replaced with better algo + break; + default: + break; + } + return `${version}||${encryptedSig}`; +} + +/** +* This function will be exposed in the global-name-space so that publisher can register the signals-ESP. +*/ +function registerSignalSources() { + if (!isGptPubadsDefined()) { + return; + } + window.googletag.encryptedSignalProviders = window.googletag.encryptedSignalProviders || []; + const encryptedSignalSources = config.getConfig('userSync.encryptedSignalSources'); + if (encryptedSignalSources) { + const registerDelay = encryptedSignalSources.registerDelay || 0; + setTimeout(() => { + encryptedSignalSources['sources'] && encryptedSignalSources['sources'].forEach(({ source, encrypt, customFunc }) => { + source.forEach((src) => { + window.googletag.encryptedSignalProviders.push({ + id: src, + collectorFunction: () => getEncryptedEidsForSource(src, encrypt, customFunc) + }); + }); + }) + }, registerDelay) + } else { + logWarn(`${MODULE_NAME} - ESP : encryptedSignalSources config not defined under userSync Object`); + } +} + /** * This function will be exposed in the global-name-space so that userIds can be refreshed after initialization. * @param {RefreshUserIdsOptions} options @@ -817,6 +939,7 @@ export function attachIdSystem(submodule) { * @param {{getConfig:function}} config */ export function init(config) { + ppidSource = undefined; submodules = []; configRegistry = []; addedUserIdHook = false; @@ -839,9 +962,10 @@ export function init(config) { } // listen for config userSyncs to be set - config.getConfig(conf => { + config.getConfig('userSync', conf => { // Note: support for 'usersync' was dropped as part of Prebid.js 4.0 const userSync = conf.userSync; + ppidSource = userSync.ppid; if (userSync && userSync.userIds) { configRegistry = userSync.userIds; syncDelay = isNumber(userSync.syncDelay) ? userSync.syncDelay : DEFAULT_SYNC_DELAY; @@ -853,7 +977,10 @@ export function init(config) { // exposing getUserIds function in global-name-space so that userIds stored in Prebid can be used by external codes. (getGlobal()).getUserIds = getUserIds; (getGlobal()).getUserIdsAsEids = getUserIdsAsEids; + (getGlobal()).getEncryptedEidsForSource = getEncryptedEidsForSource; + (getGlobal()).registerSignalSources = registerSignalSources; (getGlobal()).refreshUserIds = refreshUserIds; + (getGlobal()).getUserIdsAsEidBySource = getUserIdsAsEidBySource; } // init config update listener to start the application diff --git a/modules/userId/userId.md b/modules/userId/userId.md index 095685aba3d..ee49211e4cb 100644 --- a/modules/userId/userId.md +++ b/modules/userId/userId.md @@ -1,6 +1,7 @@ ## User ID Example Configuration Example showing `cookie` storage for user id data for each of the submodules + ``` pbjs.setConfig({ userSync: { @@ -44,6 +45,17 @@ pbjs.setConfig({ expires: 90, // Expiration in days refreshInSeconds: 8*3600 // User Id cache lifetime in seconds, defaulting to 'expires' }, + }, { + name: "ftrackId", + storage: { + type: "html5", + name: "ftrackId", + expires: 90, + refreshInSeconds: 8*3600 + }, + params: { + url: 'https://d9.flashtalking.com/d9core', // required, if not populated ftrack will not run + } }, { name: 'parrableId', params: { @@ -139,6 +151,9 @@ pbjs.setConfig({ name: "knssoId", expires: 30 }, + { + name: "dacId" + } ], syncDelay: 5000, auctionDelay: 1000 @@ -147,10 +162,23 @@ pbjs.setConfig({ ``` Example showing `localStorage` for user id data for some submodules + ``` pbjs.setConfig({ userSync: { - userIds: [{ + userIds: [ + { + name: 'trustpid', + params: { + maxDelayTime: 2500 + }, + bidders: ['adform'], + storage: { + type: 'html5', + name: 'trustpid', + expires: 60 + } + }, { name: "unifiedId", params: { partner: "prebid", @@ -278,7 +306,7 @@ pbjs.setConfig({ name: "knssoId", expires: 30 }, - } + } }, { name: 'imuid', @@ -297,6 +325,14 @@ pbjs.setConfig({ type: 'html5', expires: 15 } + } + { + name: "qid", + storage: { + type: "html5", + name: "qid", + expires: 365 + } }], syncDelay: 5000 } @@ -304,6 +340,7 @@ pbjs.setConfig({ ``` Example showing how to configure a `value` object to pass directly to bid adapters + ``` pbjs.setConfig({ userSync: { diff --git a/modules/userIdTargeting.js b/modules/userIdTargeting.js index e15c9ddaca2..b7fd137779b 100644 --- a/modules/userIdTargeting.js +++ b/modules/userIdTargeting.js @@ -1,7 +1,7 @@ import {config} from '../src/config.js'; import {getGlobal} from '../src/prebidGlobal.js'; import CONSTANTS from '../src/constants.json'; -import events from '../src/events.js'; +import * as events from '../src/events.js'; import { isStr, isPlainObject, isBoolean, isFn, hasOwn, logInfo } from '../src/utils.js'; const MODULE_NAME = 'userIdTargeting'; diff --git a/modules/ventes.md b/modules/ventes.md new file mode 100644 index 00000000000..5f0f571ecd8 --- /dev/null +++ b/modules/ventes.md @@ -0,0 +1,71 @@ +--- +layout: bidder +title: ventes +description: Prebid ventes Bidder Adapter +pbjs: false +biddercode: ventes +gdpr_supported: false +usp_supported: false +media_types: banner +coppa_supported: false +schain_supported: false +dchain_supported: false +prebid_member: false +--- + +### BidParams +{: .table .table-bordered .table-striped } +| Name | Scope | Description | Example | Type | +|-----------------|----------|-----------------------------------------------------------|----------------------------------------------|---------------| +| `placementId` | required | Placement ID from Ventes Avenues | `'VA-062-0013-0183'` | `string` | +| `publisherId` | required | Publisher ID from Ventes Avenues | `'VA-062'` | `string` | +| `user` | optional | Object that specifies information about an external user. | `user: { age: 25, gender: 0, dnt: true}` | `object` | +| `app` | optional | Object containing mobile app parameters. | `app : { id: 'app-id'}` | `object` | + +#### User Object + +{: .table .table-bordered .table-striped } +| Name | Description | Example | Type | +|-------------------|-------------------------------------------------------------------------------------------|-----------------------|-----------------------| +| `age` | The age of the user. | `35` | `integer` | +| `externalUid` | Specifies a string that corresponds to an external user ID for this user. | `'1234567890abcdefg'` | `string` | +| `segments` | Specifies the segments to which the user belongs. | `[1, 2]` | `Array` | +| `gender` | Specifies the gender of the user. Allowed values: Unknown: `0`; Male: `1`; Female: `2` | `1` | `integer` | +| `dnt` | Do not track flag. Indicates if tracking cookies should be disabled for this auction | `true` | `boolean` | +| `language` | Two-letter ANSI code for this user's language. | `EN` | `string` | + + +### Ad Unit Setup for Banner +```javascript +var adUnits = [ +{ + code: 'test-hb-ad-11111-1', + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ] + } + }, + bids: [{ + bidder: 'ventes', + params: { + placementId: 'VA-062-0013-0183', + publisherId: '5cebea3c9eea646c7b623d5e', + IABCategories: "['IAB1', 'IAB5']", + device:{ + ip: '123.145.167.189', + ifa:"AEBE52E7-03EE-455A-B3C4-E57283966239", + }, + app: { + id: "agltb3B1Yi1pbmNyDAsSA0FwcBiJkfIUDA", + name: "Yahoo Weather", + bundle: 'com.kiloo.subwaysurf', + storeurl: 'https://play.google.com/store/apps/details?id=com.kiloo.subwaysurf&hl=en', + domain: 'somoaudience.com', + } + } + }] + } +] +``` diff --git a/modules/ventesBidAdapter.js b/modules/ventesBidAdapter.js index 7a2b60d2ee2..42292ddaed3 100644 --- a/modules/ventesBidAdapter.js +++ b/modules/ventesBidAdapter.js @@ -1,17 +1,11 @@ -import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; -import {convertCamelToUnderscore, isStr, isArray, isNumber, isPlainObject, replaceAuctionPrice} from '../src/utils.js'; -import find from 'core-js-pure/features/array/find.js'; -import includes from 'core-js-pure/features/array/includes.js'; +import {convertCamelToUnderscore, isArray, isNumber, isPlainObject, isStr, replaceAuctionPrice} from '../src/utils.js'; +import {find} from '../src/polyfill.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; const BID_METHOD = 'POST'; const BIDDER_URL = 'http://13.234.201.146:8088/va/ad'; -const FIRST_PRICE = 1; -const NET_REVENUE = true; -const TTL = 10; -const USER_PARAMS = ['age', 'externalUid', 'segments', 'gender', 'dnt', 'language']; -const DEVICE_PARAMS = ['ua', 'geo', 'dnt', 'lmt', 'ip', 'ipv6', 'devicetype']; -const APP_DEVICE_PARAMS = ['geo', 'device_id']; // appid is collected separately + const DOMAIN_REGEX = new RegExp('//([^/]*)'); function groupBy(values, key) { @@ -26,7 +20,11 @@ function groupBy(values, key) { return Object .keys(groups) - .map(id => ({id, key, values: groups[id]})); + .map(id => ({ + id, + key, + values: groups[id] + })); } function validateMediaTypes(mediaTypes, allowedMediaTypes) { @@ -45,22 +43,22 @@ function isBanner(mediaTypes) { function validateBanner(banner) { return isPlainObject(banner) && - isArray(banner.sizes) && - (banner.sizes.length > 0) && - banner.sizes.every(validateMediaSizes); + isArray(banner.sizes) && + (banner.sizes.length > 0) && + banner.sizes.every(validateMediaSizes); } function validateMediaSizes(mediaSize) { return isArray(mediaSize) && - (mediaSize.length === 2) && - mediaSize.every(size => (isNumber(size) && size >= 0)); + (mediaSize.length === 2) && + mediaSize.every(size => (isNumber(size) && size >= 0)); } function hasUserInfo(bid) { return !!bid.params.user; } -function validateParameters(parameters, adUnit) { +function validateParameters(parameters) { if (!(parameters.placementId)) { return false; } @@ -101,8 +99,8 @@ function generateSiteFromAdUnitContext(bidRequests, adUnitContext) { function validateServerRequest(serverRequest) { return isPlainObject(serverRequest) && - isPlainObject(serverRequest.data) && - isArray(serverRequest.data.imp) + isPlainObject(serverRequest.data) && + isArray(serverRequest.data.imp) } function createServerRequestFromAdUnits(adUnits, bidRequestId, adUnitContext) { @@ -122,14 +120,15 @@ function generateBidRequestsFromAdUnits(bidRequests, bidRequestId, adUnitContext let userObj = {}; if (userObjBid) { Object.keys(userObjBid.params.user) - .filter(param => includes(USER_PARAMS, param)) .forEach((param) => { let uparam = convertCamelToUnderscore(param); if (param === 'segments' && isArray(userObjBid.params.user[param])) { let segs = []; userObjBid.params.user[param].forEach(val => { if (isNumber(val)) { - segs.push({'id': val}); + segs.push({ + 'id': val + }); } else if (isPlainObject(val)) { segs.push(val); } @@ -146,7 +145,6 @@ function generateBidRequestsFromAdUnits(bidRequests, bidRequestId, adUnitContext if (deviceObjBid && deviceObjBid.params && deviceObjBid.params.device) { deviceObj = {}; Object.keys(deviceObjBid.params.device) - .filter(param => includes(DEVICE_PARAMS, param)) .forEach(param => deviceObj[param] = deviceObjBid.params.device[param]); if (!deviceObjBid.hasOwnProperty('ua')) { deviceObj.ua = navigator.userAgent; @@ -159,37 +157,41 @@ function generateBidRequestsFromAdUnits(bidRequests, bidRequestId, adUnitContext deviceObj.ua = navigator.userAgent; deviceObj.language = navigator.language; } - const appDeviceObjBid = find(bidRequests, hasAppInfo); - let appIdObj; - if (appDeviceObjBid && appDeviceObjBid.params && appDeviceObjBid.params.app && appDeviceObjBid.params.app.id) { - Object.keys(appDeviceObjBid.params.app) - .filter(param => includes(APP_DEVICE_PARAMS, param)) - .forEach(param => appDeviceObjBid[param] = appDeviceObjBid.params.app[param]); - } const payload = {} payload.id = bidRequestId - payload.at = FIRST_PRICE + payload.at = 1 payload.cur = ['USD'] payload.imp = bidRequests.reduce(generateImpressionsFromAdUnit, []) - payload.site = generateSiteFromAdUnitContext(bidRequests, adUnitContext) - payload.device = deviceObj - if (appDeviceObjBid && payload.site != null) { + const appDeviceObjBid = find(bidRequests, hasAppInfo); + if (!appDeviceObjBid) { + payload.site = generateSiteFromAdUnitContext(bidRequests, adUnitContext) + } else { + let appIdObj; + if (appDeviceObjBid && appDeviceObjBid.params && appDeviceObjBid.params.app && appDeviceObjBid.params.app.id) { + appIdObj = {}; + Object.keys(appDeviceObjBid.params.app) + .forEach(param => appIdObj[param] = appDeviceObjBid.params.app[param]); + } payload.app = appIdObj; } + payload.device = deviceObj; payload.user = userObj - // payload.regs = getRegulationFromAdUnitContext(adUnitContext) - // payload.ext = generateBidRequestExtension() - return payload } function generateImpressionsFromAdUnit(acc, adUnit) { - const {bidId, mediaTypes, params} = adUnit; - const {placementId} = params; + const { + bidId, + mediaTypes, + params + } = adUnit; + const { + placementId + } = params; const pmp = {}; - if (placementId) pmp.deals = [{id: placementId}] + if (placementId) pmp.deals = [{ id: placementId }] const imps = Object .keys(mediaTypes) @@ -204,21 +206,40 @@ function generateImpressionsFromAdUnit(acc, adUnit) { } function generateBannerFromAdUnit(impId, data, params) { - const {position, placementId} = params; + const { + position, + placementId + } = params; const pos = position || 0; const pmp = {}; - const ext = {placementId}; - - if (placementId) pmp.deals = [{id: placementId}] + const ext = { + placementId + }; - return data.sizes.map(([w, h]) => ({id: `${impId}`, banner: {format: [{w, h}], w, h, pos}, pmp, ext, tagid: placementId})); + if (placementId) pmp.deals = [{ id: placementId }] + + return data.sizes.map(([w, h]) => ({ + id: `${impId}`, + banner: { + format: [{ + w, + h + }], + w, + h, + pos + }, + pmp, + ext, + tagid: placementId + })); } function validateServerResponse(serverResponse) { return isPlainObject(serverResponse) && - isPlainObject(serverResponse.body) && - isStr(serverResponse.body.cur) && - isArray(serverResponse.body.seatbid); + isPlainObject(serverResponse.body) && + isStr(serverResponse.body.cur) && + isArray(serverResponse.body.seatbid); } function seatBidsToAds(seatBid, bidResponse, serverRequest) { @@ -241,10 +262,8 @@ function validateBids(bid) { return true; } -const VAST_REGEXP = /VAST\s+version/; - function getMediaType(adm) { - const videoRegex = new RegExp(VAST_REGEXP); + const videoRegex = new RegExp(/VAST\s+version/); if (videoRegex.test(adm)) { return VIDEO; @@ -273,14 +292,16 @@ function generateAdFromBid(bid, bidResponse) { requestId: bid.impid, cpm: bid.price, currency: bidResponse.cur, - ttl: TTL, + ttl: 10, creativeId: bid.crid, mediaType: mediaType, - netRevenue: NET_REVENUE + netRevenue: true }; if (bid.adomain) { - base.meta = { advertiserDomains: bid.adomain }; + base.meta = { + advertiserDomains: bid.adomain + }; } const size = getSizeFromBid(bid); @@ -292,17 +313,21 @@ function generateAdFromBid(bid, bidResponse) { width: size.width, ad: creative.markup, adUrl: creative.markupUrl, - // vastXml: isVideo && !isStr(creative.markupUrl) ? creative.markup : null, - // vastUrl: isVideo && isStr(creative.markupUrl) ? creative.markupUrl : null, renderer: creative.renderer }; } function getSizeFromBid(bid) { if (isNumber(bid.w) && isNumber(bid.h)) { - return { width: bid.w, height: bid.h }; + return { + width: bid.w, + height: bid.h + }; } - return { width: null, height: null }; + return { + width: null, + height: null + }; } function getCreativeFromBid(bid) { @@ -333,12 +358,12 @@ const venavenBidderSpec = { const allowedBidderCodes = [this.code]; return isPlainObject(adUnit) && - allowedBidderCodes.indexOf(adUnit.bidder) !== -1 && - isStr(adUnit.adUnitCode) && - isStr(adUnit.bidderRequestId) && - isStr(adUnit.bidId) && - validateMediaTypes(adUnit.mediaTypes, this.supportedMediaTypes) && - validateParameters(adUnit.params, adUnit); + allowedBidderCodes.indexOf(adUnit.bidder) !== -1 && + isStr(adUnit.adUnitCode) && + isStr(adUnit.bidderRequestId) && + isStr(adUnit.bidId) && + validateMediaTypes(adUnit.mediaTypes, this.supportedMediaTypes) && + validateParameters(adUnit.params); }, buildRequests(bidRequests, bidderRequest) { if (!bidRequests) return null; @@ -367,4 +392,6 @@ const venavenBidderSpec = { registerBidder(venavenBidderSpec); -export {venavenBidderSpec as spec}; +export { + venavenBidderSpec as spec +}; diff --git a/modules/ventesBidAdapter.md b/modules/ventesBidAdapter.md index 479f6dd2898..c79ef080cd1 100644 --- a/modules/ventesBidAdapter.md +++ b/modules/ventesBidAdapter.md @@ -55,7 +55,6 @@ var adUnits = [ publisherId: '5cebea3c9eea646c7b623d5e', IABCategories: "['IAB1', 'IAB5']", device:{ - ip: '123.145.167.189', ifa:"AEBE52E7-03EE-455A-B3C4-E57283966239", }, app: { diff --git a/modules/verizonMediaIdSystem.js b/modules/verizonMediaIdSystem.js index 280a6c47894..27577ad0de4 100644 --- a/modules/verizonMediaIdSystem.js +++ b/modules/verizonMediaIdSystem.js @@ -7,8 +7,8 @@ import {ajax} from '../src/ajax.js'; import {submodule} from '../src/hook.js'; -import { logError, formatQS } from '../src/utils.js'; -import includes from 'core-js-pure/features/array/includes.js'; +import {formatQS, logError} from '../src/utils.js'; +import {includes} from '../src/polyfill.js'; const MODULE_NAME = 'verizonMediaId'; const VENDOR_ID = 25; diff --git a/modules/vibrantmediaBidAdapter.js b/modules/vibrantmediaBidAdapter.js new file mode 100644 index 00000000000..b6fe51c43bc --- /dev/null +++ b/modules/vibrantmediaBidAdapter.js @@ -0,0 +1,220 @@ +/* + * Vibrant Media Ltd. + * + * Prebid Adapter for sending bid requests to the prebid server and bid responses back to the client + * + * Note: Only BANNER and VIDEO are currently supported by the prebid server. + */ + +import {logError, logInfo} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; +import {OUTSTREAM} from '../src/video.js'; + +const BIDDER_CODE = 'vibrantmedia'; +const VIBRANT_MEDIA_PREBID_URL = 'https://prebid.intellitxt.com/prebid'; +const SUPPORTED_MEDIA_TYPES = [BANNER, NATIVE, VIDEO]; + +/** + * Returns whether the given bid request contains at least one supported media request, which has valid data. (We can + * ignore invalid/unsupported ones, as they will be filtered out by the prebid server.) + * + * @param {*} bidRequest the bid requests sent by the Prebid API. + * + * @return {boolean} true if the given bid request contains at least one supported media request with valid details, + * otherwise false. + */ +const areValidSupportedMediaTypesPresent = function(bidRequest) { + const mediaTypes = Object.keys(bidRequest.mediaTypes); + + return mediaTypes.some(function(mediaType) { + if (mediaType === BANNER) { + return true; + } else if (mediaType === VIDEO) { + return (bidRequest.mediaTypes[VIDEO].context === OUTSTREAM); + } else if (mediaType === NATIVE) { + return !!bidRequest.mediaTypes[NATIVE].image; + } + + return false; + }); +}; + +/** + * Returns whether the given URL contains just a domain, and not (for example) a subdirectory or query parameters. + * @param {string} url the URL to check. + * @returns {boolean} whether the URL contains just a domain. + */ +const isBaseUrl = function(url) { + const urlMinusScheme = url.substring(url.indexOf('://') + 3); + const endOfDomain = urlMinusScheme.indexOf('/'); + return (endOfDomain === -1) || (endOfDomain === (urlMinusScheme.length - 1)); +}; + +/** + * Returns transformed bid requests that are in a format native to the prebid server. + * + * @param {*[]} bidRequests the bid requests sent by the Prebid API. + * + * @returns {*[]} the transformed bid requests. + */ +const transformBidRequests = function(bidRequests) { + const transformedBidRequests = []; + + bidRequests.forEach(function(bidRequest) { + const params = bidRequest.params || {}; + const transformedBidRequest = { + code: bidRequest.adUnitCode || bidRequest.code, + id: bidRequest.placementId || params.placementId || params.invCode, + requestId: bidRequest.bidId || bidRequest.transactionId, + bidder: bidRequest.bidder, + mediaTypes: bidRequest.mediaTypes, + bids: bidRequest.bids, + sizes: bidRequest.sizes + }; + + transformedBidRequests.push(transformedBidRequest); + }); + + return transformedBidRequests; +}; + +/** @type {BidderSpec} */ +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: SUPPORTED_MEDIA_TYPES, + + /** + * Transforms the 'raw' bid params into ones that this adapter can use, prior to creating the bid request. + * + * @param {object} bidParams the params to transform. + * + * @returns {object} the bid params. + */ + transformBidParams: function(bidParams) { + return bidParams; + }, + + /** + * Determines whether or not the given bid request is valid. For all bid requests passed to the buildRequests + * function, each will have been passed to this function and this function will have returned true. + * + * @param {object} bid the bid params to validate. + * + * @return {boolean} true if this is a valid bid, otherwise false. + * @see SUPPORTED_MEDIA_TYPES + */ + isBidRequestValid: function(bid) { + const areBidRequestParamsValid = !!(bid.params.placementId || (bid.params.member && bid.params.invCode)); + return areBidRequestParamsValid && areValidSupportedMediaTypesPresent(bid); + }, + + /** + * Return a prebid server request from the list of bid requests. + * + * @param {BidRequest[]} validBidRequests an array of bids validated via the isBidRequestValid function. + * @param {BidderRequest} bidderRequest an object with data common to all bid requests. + * + * @return ServerRequest Info describing the request to the prebid server. + */ + buildRequests: function(validBidRequests, bidderRequest) { + const transformedBidRequests = transformBidRequests(validBidRequests); + + var url = window.parent.location.href; + + if ((window.self === window.top) && (!url || (url.substr(0, 4) !== 'http') || isBaseUrl(url))) { + url = document.URL; + } + + url = encodeURIComponent(url); + + const prebidData = { + url, + gdpr: bidderRequest.gdprConsent, + usp: bidderRequest.uspConsent, + window: { + width: window.innerWidth, + height: window.innerHeight, + }, + biddata: transformedBidRequests, + }; + + return { + method: 'POST', + url: VIBRANT_MEDIA_PREBID_URL, + data: JSON.stringify(prebidData) + }; + }, + + /** + * Translate the Kormorant prebid server response into a list of bids. + * + * @param {ServerResponse} serverResponse a successful response from the prebid server. + * @param {BidRequest} bidRequest the original bid request associated with this response. + * + * @return {Bid[]} an array of bids returned by the prebid server, translated into the expected Prebid.js format. + */ + interpretResponse: function(serverResponse, bidRequest) { + const bids = serverResponse.body; + + bids.forEach(function(bid) { + bid.adResponse = serverResponse; + }); + + return bids; + }, + + /** + * Called if the Prebid API gives up waiting for a prebid server response. + * + * Example timeout data: + * + * [{ + * "bidder": "example", + * "bidId": "51ef8751f9aead", + * "params": { + * ... + * }, + * "adUnitCode": "div-gpt-ad-1460505748561-0", + * "timeout": 3000, + * "auctionId": "18fd8b8b0bd757" + * }] + * + * @param {{}} timeoutData data relating to the timeout. + */ + onTimeout: function(timeoutData) { + logError('Timed out waiting for bids: ' + JSON.stringify(timeoutData)); + }, + + /** + * Called when a bid returned by the prebid server is successful. + * + * Example bid won data: + * + * { + * "bidder": "example", + * "width": 300, + * "height": 250, + * "adId": "330a22bdea4cac", + * "mediaType": "banner", + * "cpm": 0.28 + * "ad": "...", + * "requestId": "418b37f85e772c", + * "adUnitCode": "div-gpt-ad-1460505748561-0", + * "size": "350x250", + * "adserverTargeting": { + * "hb_bidder": "example", + * "hb_adid": "330a22bdea4cac", + * "hb_pb": "0.20", + * "hb_size": "350x250" + * } + * } + * + * @param {*} bidData the data associated with the won bid. See example above for data format. + */ + onBidWon: function(bidData) { + logInfo('Bid won: ' + JSON.stringify(bidData)); + } +}; + +registerBidder(spec); diff --git a/modules/vibrantmediaBidAdapter.md b/modules/vibrantmediaBidAdapter.md new file mode 100644 index 00000000000..ce5db42fdb5 --- /dev/null +++ b/modules/vibrantmediaBidAdapter.md @@ -0,0 +1,92 @@ +## Overview + +**Module Name:** Vibrant Media Bidder Adapter + +**Module Type:** Bidder Adapter + +**Maintainer:** kormorant@vibrantmedia.com + +## Description + +Module that allows Vibrant Media to provide ad bids for banner, native and video (outstream only). + +## Test Parameters + +```javascript +var adUnits = [ + // Banner ad unit + { + code: 'test-banner', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bids: [{ + bidder: 'vibrantmedia', + params: { + placementId: 12345 + } + }] + }, + + // Video (outstream) ad unit + { + code: 'test-video-outstream', + sizes: [[300, 250]], + mediaTypes: { + video: { + playerSize: [[300, 250]], + context: 'outstream', + minduration: 1, // Minimum ad duration, in seconds + maxduration: 60, // Maximum ad duration, in seconds + skip: 0, // 1 - true, 0 - false + skipafter: 5, // Number of seconds before the video can be skipped + playbackmethod: [2], // Auto-play without sound + protocols: [1, 2, 3] // VAST 1.0, 2.0 and 3.0 + } + }, + bids: [ + { + bidder: 'vibrantmedia', + params: { + placementId: 67890, + video: { + skippable: true, + playback_method: 'auto_play_sound_off' + } + } + } + ] + }, + + // Native ad unit + { + code: 'test-native', + mediaTypes: { + native: { + image: { + required: true, + sizes: [300, 250] + }, + title: { + required: true + }, + sponsoredBy: { + required: true + }, + clickUrl: { + required: true + }, + } + }, + bids: [{ + bidder: 'vibrantmedia', + params: { + placementId: 13579, + allowSmallerSizes: true + } + }] + } +]; +``` diff --git a/modules/vidazooBidAdapter.js b/modules/vidazooBidAdapter.js index 0e7b4ee63b2..cf252bda2dc 100644 --- a/modules/vidazooBidAdapter.js +++ b/modules/vidazooBidAdapter.js @@ -24,7 +24,7 @@ export const SUPPORTED_ID_SYSTEMS = { 'pubcid': 1, 'tdid': 1, }; -const storage = getStorageManager(GVLID); +const storage = getStorageManager({gvlid: GVLID, bidderCode: BIDDER_CODE}); export function createDomain(subDomain = DEFAULT_SUB_DOMAIN) { return `https://${subDomain}.cootlogix.com`; @@ -48,7 +48,7 @@ function isBidRequestValid(bid) { } function buildRequest(bid, topWindowUrl, sizes, bidderRequest) { - const { params, bidId, userId, adUnitCode } = bid; + const { params, bidId, userId, adUnitCode, schain } = bid; const { bidFloor, ext } = params; const hashUrl = hashCode(topWindowUrl); const dealId = getNextDealId(hashUrl); @@ -71,7 +71,8 @@ function buildRequest(bid, topWindowUrl, sizes, bidderRequest) { uniqueDealId: uniqueDealId, bidderVersion: BIDDER_VERSION, prebidVersion: '$prebid.version$', - res: `${screen.width}x${screen.height}` + res: `${screen.width}x${screen.height}`, + schain: schain }; appendUserIdsToRequestPayload(data, userId); diff --git a/modules/vidoomyBidAdapter.js b/modules/vidoomyBidAdapter.js index 182284410e6..d268f7a9d64 100644 --- a/modules/vidoomyBidAdapter.js +++ b/modules/vidoomyBidAdapter.js @@ -1,4 +1,4 @@ -import { logError, deepAccess } from '../src/utils.js'; +import { logError, deepAccess, parseSizesInput } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; @@ -42,6 +42,11 @@ const isBidRequestValid = bid => { return false; } + if (bid.params.bidfloor && (isNaN(bid.params.bidfloor) || bid.params.bidfloor < 0)) { + logError(BIDDER_CODE + ': bid.params.bidfloor should be a number equal or greater than zero'); + return false; + } + return true; }; @@ -53,7 +58,7 @@ const isBidResponseValid = bid => { case BANNER: return Boolean(bid.width && bid.height && bid.ad); case VIDEO: - return Boolean(bid.vastUrl); + return Boolean(bid.vastUrl || bid.vastXml); default: return false; } @@ -62,24 +67,27 @@ const isBidResponseValid = bid => { const buildRequests = (validBidRequests, bidderRequest) => { const serverRequests = validBidRequests.map(bid => { let adType = BANNER; - let w, h; + let sizes; if (bid.mediaTypes && bid.mediaTypes[BANNER] && bid.mediaTypes[BANNER].sizes) { - [w, h] = bid.mediaTypes[BANNER].sizes[0]; + sizes = bid.mediaTypes[BANNER].sizes; adType = BANNER; } else if (bid.mediaTypes && bid.mediaTypes[VIDEO] && bid.mediaTypes[VIDEO].playerSize) { - [w, h] = bid.mediaTypes[VIDEO].playerSize; + sizes = bid.mediaTypes[VIDEO].playerSize; adType = VIDEO; } + const [w, h] = (parseSizesInput(sizes)[0] || '0x0').split('x'); const aElement = document.createElement('a'); aElement.href = (bidderRequest.refererInfo && bidderRequest.refererInfo.referer) || top.location.href; const hostname = aElement.hostname const videoContext = deepAccess(bid, 'mediaTypes.video.context'); + const bidfloor = deepAccess(bid, `params.bidfloor`, 0); const queryParams = { id: bid.params.id, adtype: adType, + auc: bid.adUnitCode, w, h, pos: parseInt(bid.params.position) || 1, @@ -88,7 +96,9 @@ const buildRequests = (validBidRequests, bidderRequest) => { dt: /Mobi/.test(navigator.userAgent) ? 2 : 1, pid: bid.params.pid, requestId: bid.bidId, - d: getDomainWithoutSubdomain(hostname), + schain: bid.schain || '', + bidfloor, + d: getDomainWithoutSubdomain(hostname), // 'vidoomy.com', sp: encodeURIComponent(aElement.href), usp: bidderRequest.uspConsent || '', coppa: !!config.getConfig('coppa'), @@ -127,7 +137,7 @@ const interpretResponse = (serverResponse, bidRequest) => { let responseBody = serverResponse.body; if (!responseBody) return; if (responseBody.mediaType === 'video') { - responseBody.ad = responseBody.vastUrl; + responseBody.ad = responseBody.vastUrl || responseBody.vastXml; const videoContext = bidRequest.data.videoContext; if (videoContext === OUTSTREAM) { @@ -143,13 +153,12 @@ const interpretResponse = (serverResponse, bidRequest) => { responseBody.renderer = renderer; } catch (e) { - responseBody.ad = responseBody.vastUrl; + responseBody.ad = responseBody.vastUrl || responseBody.vastXml; logError(BIDDER_CODE + ': error while installing renderer to show outstream ad'); } } } const bid = { - vastUrl: responseBody.vastUrl, ad: responseBody.ad, renderer: responseBody.renderer, mediaType: responseBody.mediaType, @@ -178,6 +187,11 @@ const interpretResponse = (serverResponse, bidRequest) => { secondaryCatIds: responseBody.meta.secondaryCatIds } }; + if (responseBody.vastUrl) { + bid.vastUrl = responseBody.vastUrl; + } else if (responseBody.vastXml) { + bid.vastXml = responseBody.vastXml; + } const bids = []; @@ -202,7 +216,7 @@ function getUserSyncs (syncOptions, responses, gdprConsent, uspConsent) { return [].concat(urls).map(url => ({ type: pixelType, url: url - .replace('{{GDPR}}', gdprConsent ? gdprConsent.gdprApplies : '0') + .replace('{{GDPR}}', gdprConsent ? (gdprConsent.gdprApplies ? '1' : '0') : '0') .replace('{{GDPR_CONSENT}}', gdprConsent ? encodeURIComponent(gdprConsent.consentString) : '') .replace('{{USP_CONSENT}}', uspConsent ? encodeURIComponent(uspConsent) : '') })); diff --git a/modules/vidoomyBidAdapter.md b/modules/vidoomyBidAdapter.md index 11e0ad40dbb..d91ace5a7b9 100644 --- a/modules/vidoomyBidAdapter.md +++ b/modules/vidoomyBidAdapter.md @@ -26,7 +26,8 @@ var adUnits = [ bidder: 'vidoomy', params: { id: '123123', - pid: '123123' + pid: '123123', + bidfloor: 0.5 // This is optional } } ] @@ -50,7 +51,8 @@ var adUnits = [ bidder: 'vidoomy', params: { id: '123123', - pid: '123123' + pid: '123123', + bidfloor: 0.5 // This is optional } } ] diff --git a/modules/viewability.js b/modules/viewability.js new file mode 100644 index 00000000000..39b2ee3da16 --- /dev/null +++ b/modules/viewability.js @@ -0,0 +1,177 @@ +import {insertHtmlIntoIframe, isFn, isStr, logInfo, logWarn, triggerPixel} from '../src/utils.js'; +import {getGlobal} from '../src/prebidGlobal.js'; +import {find} from '../src/polyfill.js'; + +export const MODULE_NAME = 'viewability'; + +export function init() { + (getGlobal()).viewability = { + startMeasurement: startMeasurement, + stopMeasurement: stopMeasurement, + }; + + listenMessagesFromCreative(); +} + +const observers = {}; + +function isValid(vid, element, tracker, criteria) { + if (!element) { + logWarn(`${MODULE_NAME}: no html element provided`); + return false; + } + + let validTracker = tracker && + ((tracker.method === 'img' && isStr(tracker.value)) || + (tracker.method === 'js' && isStr(tracker.value)) || + (tracker.method === 'callback' && isFn(tracker.value))); + + if (!validTracker) { + logWarn(`${MODULE_NAME}: invalid tracker`, tracker); + return false; + } + + if (!criteria || !criteria.inViewThreshold || !criteria.timeInView) { + logWarn(`${MODULE_NAME}: missing criteria`, criteria); + return false; + } + + if (!vid || observers[vid]) { + logWarn(`${MODULE_NAME}: must provide an unregistered vid`, vid); + return false; + } + + return true; +} + +function stopObserving(observer, vid, element) { + observer.unobserve(element); + observers[vid].done = true; +} + +function fireViewabilityTracker(element, tracker) { + switch (tracker.method) { + case 'img': + triggerPixel(tracker.value, () => { + logInfo(`${MODULE_NAME}: viewability pixel fired`, tracker.value); + }); + break; + case 'js': + insertHtmlIntoIframe(``); + break; + case 'callback': + tracker.value(element); + break; + } +} + +function viewabilityCriteriaMet(observer, vid, element, tracker) { + stopObserving(observer, vid, element); + fireViewabilityTracker(element, tracker); +} + +/** + * Start measuring viewability of an element + * @typedef {{ method: string='img','js','callback', value: string|function }} ViewabilityTracker { method: 'img', value: 'http://my.tracker/123' } + * @typedef {{ inViewThreshold: number, timeInView: number }} ViewabilityCriteria { inViewThreshold: 0.5, timeInView: 1000 } + * @param {string} vid unique viewability identifier + * @param {HTMLElement} element + * @param {ViewabilityTracker} tracker + * @param {ViewabilityCriteria} criteria + */ +export function startMeasurement(vid, element, tracker, criteria) { + if (!isValid(vid, element, tracker, criteria)) { + return; + } + + const options = { + root: null, + rootMargin: '0px', + threshold: criteria.inViewThreshold, + }; + + let observer; + let viewable = false; + let stateChange = (entries) => { + viewable = entries[0].isIntersecting; + + if (viewable) { + observers[vid].timeoutId = window.setTimeout(() => { + viewabilityCriteriaMet(observer, vid, element, tracker); + }, criteria.timeInView); + } else if (observers[vid].timeoutId) { + window.clearTimeout(observers[vid].timeoutId); + } + }; + + observer = new IntersectionObserver(stateChange, options); + observers[vid] = { + observer: observer, + element: element, + timeoutId: null, + done: false, + }; + + observer.observe(element); + + logInfo(`${MODULE_NAME}: startMeasurement called with:`, arguments); +} + +/** + * Stop measuring viewability of an element + * @param {string} vid unique viewability identifier + */ +export function stopMeasurement(vid) { + if (!vid || !observers[vid]) { + logWarn(`${MODULE_NAME}: must provide a registered vid`, vid); + return; + } + + observers[vid].observer.unobserve(observers[vid].element); + if (observers[vid].timeoutId) { + window.clearTimeout(observers[vid].timeoutId); + } + + // allow the observer under this vid to be created again + if (!observers[vid].done) { + delete observers[vid]; + } +} + +function listenMessagesFromCreative() { + window.addEventListener('message', receiveMessage, false); +} + +/** + * Recieve messages from creatives + * @param {MessageEvent} evt + */ +export function receiveMessage(evt) { + var key = evt.message ? 'message' : 'data'; + var data = {}; + try { + data = JSON.parse(evt[key]); + } catch (e) { + return; + } + + if (!data || data.message !== 'Prebid Viewability') { + return; + } + + switch (data.action) { + case 'startMeasurement': + let element = data.elementId && document.getElementById(data.elementId); + if (!element) { + element = find(document.getElementsByTagName('IFRAME'), iframe => (iframe.contentWindow || iframe.contentDocument.defaultView) == evt.source); + } + + startMeasurement(data.vid, element, data.tracker, data.criteria); + break; + case 'stopMeasurement': + stopMeasurement(data.vid); + break; + } +} + +init(); diff --git a/modules/viewability.md b/modules/viewability.md new file mode 100644 index 00000000000..df93b5c40db --- /dev/null +++ b/modules/viewability.md @@ -0,0 +1,87 @@ +# Overview + +Module Name: Viewability + +Purpose: Track when a given HTML element becomes viewable + +Maintainer: atrajkovic@magnite.com + +# Configuration + +Module does not need any configuration, as long as you include it in your PBJS bundle. +Viewability module has only two functions `startMeasurement` and `stopMeasurement` which can be used to enable more complex viewability measurements. Since it allows tracking from within creative (possibly inside a safe frame) this module registers a message listener, for messages with a format that is described bellow. + +## `startMeasurement` + +| startMeasurement Arg Object | Scope | Type | Description | Example | +| --------------------- | -------- | ------------ | -------------------------------------------------------------------------------- | --------- | +| vid | Required | String | Unique viewability identifier, used to reference particular observer | `"ae0f9"` | +| element | Required | HTMLElement | Reference to an HTML element that needs to be tracked | `document.getElementById('test_div')` | +| tracker | Required | ViewabilityTracker | How viewaility event is communicated back to the parties of interest | `{ method: 'img', value: 'http://my.tracker/123' }` | +| criteria | Required | ViewabilityCriteria| Defines custom viewability criteria using the threshold and duration provided | `{ inViewThreshold: 0.5, timeInView: 1000 }` | + +| ViewabilityTracker | Scope | Type | Description | Example | +| --------------------- | -------- | ------------ | -------------------------------------------------------------------------------- | --------- | +| method | Required | String | Type of method for Tracker | `'img' OR 'js' OR 'callback'` | +| value | Required | String | URL string for 'img' and 'js' Trackers, or a function for 'callback' Tracker | `'http://my.tracker/123'` | + +| ViewabilityCriteria | Scope | Type | Description | Example | +| --------------------- | -------- | ------------ | -------------------------------------------------------------------------------- | --------- | +| inViewThreshold | Required | Number | Represents a percentage threshold for the Element to be registered as in view | `0.5` | +| timeInView | Required | Number | Number of milliseconds that a given element needs to be in view continuously, above the threshold | `1000` | + +## Please Note: +- `vid` allows for multiple trackers, with different criteria to be registered for a given HTML element, independently. It's not autogenerated by `startMeasurement()`, it needs to be provided by the caller so that it doesn't have to be posted back to the source iframe (in case viewability is started from within the creative). +- In case of 'callback' method, HTML element is being passed back to the callback function. +- When a tracker needs to be started, without direct access to pbjs, postMessage mechanism can be used to invoke `startMeasurement`, with a following payload: `vid`, `tracker` and `criteria` as described above, but also with `message: 'Prebid Viewability'` and `action: 'startMeasurement'`. Optionally payload can provide `elementId`, if available at that time (for ad servers where name of the iframe is known, or adservers that render outside an iframe). If `elementId` is not provided, viewability module will try to find the iframe that corresponds to the message source. + + +## `stopMeasurement` + +| stopMeasurement Arg Object | Scope | Type | Description | Example | +| --------------------- | -------- | ------------ | -------------------------------------------------------------------------------- | --------- | +| vid | Required | String | Unique viewability identifier, referencing an already started viewability tracker. | `"ae0f9"` | + +## Please Note: +- When a tracker needs to be stopped, without direct access to pbjs, postMessage mechanism can be used here as well. To invoke `stopMeasurement`, you provide the payload with `vid`, `message: 'Prebid Viewability'` and `action: 'stopMeasurement`. Check the example bellow. + +# Examples + +## Example of starting a viewability measurement, when you have direct access to pbjs +``` +pbjs.viewability.startMeasurement( + 'ae0f9', + document.getElementById('test_div'), + { method: 'img', value: 'http://my.tracker/123' }, + { inViewThreshold: 0.5, timeInView: 1000 } +); +``` + +## Example of starting a viewability measurement from within a rendered creative +``` +let viewabilityRecord = { + vid: 'ae0f9', + tracker: { method: 'img', value: 'http://my.tracker/123'}, + criteria: { inViewThreshold: 0.5, timeInView: 1000 }, + message: 'Prebid Viewability', + action: 'startMeasurement' +} + +window.parent.postMessage(JSON.stringify(viewabilityRecord), '*'); +``` + +## Example of stopping the viewability measurement, when you have direct access to pbjs +``` +pbjs.viewability.stopMeasurement('ae0f9'); +``` + +## Example of stopping the viewability measurement from within a rendered creative +``` +let viewabilityRecord = { + vid: 'ae0f9', + message: 'Prebid Viewability', + action: 'stopMeasurement' +} + +window.parent.postMessage(JSON.stringify(viewabilityRecord), '*'); +``` diff --git a/modules/viewdeosDXBidAdapter.js b/modules/viewdeosDXBidAdapter.js index e3d02938c5b..9e0cb91af9b 100644 --- a/modules/viewdeosDXBidAdapter.js +++ b/modules/viewdeosDXBidAdapter.js @@ -1,8 +1,8 @@ -import { deepAccess, isArray, flatten, logError, parseSizesInput } from '../src/utils.js'; +import {deepAccess, flatten, isArray, logError, parseSizesInput} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {VIDEO, BANNER} from '../src/mediaTypes.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {Renderer} from '../src/Renderer.js'; -import findIndex from 'core-js-pure/features/array/find-index.js'; +import {findIndex} from '../src/polyfill.js'; const URL = 'https://ghb.sync.viewdeos.com/auction/'; const OUTSTREAM_SRC = 'https://player.sync.viewdeos.com/outstream-unit/2.01/outstream.min.js'; diff --git a/modules/visxBidAdapter.js b/modules/visxBidAdapter.js index 3442cbc8dd8..696d54e4b52 100644 --- a/modules/visxBidAdapter.js +++ b/modules/visxBidAdapter.js @@ -1,4 +1,4 @@ -import { triggerPixel, parseSizesInput, deepAccess, logError } from '../src/utils.js'; +import { triggerPixel, parseSizesInput, deepAccess, logError, getGptSlotInfoForAdUnitCode } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; @@ -42,7 +42,7 @@ export const spec = { } } } - return !!bid.params.uid; + return !!bid.params.uid && !isNaN(parseInt(bid.params.uid)); }, buildRequests: function(validBidRequests, bidderRequest) { const auids = []; @@ -203,6 +203,15 @@ export const spec = { }, onTimeout: function(timeoutData) { // Call '/track/bid_timeout' with timeout data + timeoutData.forEach(({ params }) => { + if (params) { + params.forEach((item) => { + if (item && item.uid) { + item.uid = parseInt(item.uid); + } + }); + } + }); triggerPixel(buildUrl(TRACK_TIMEOUT_PATH) + '//' + JSON.stringify(timeoutData)); } }; @@ -241,7 +250,7 @@ function makeVideo(videoParams = {}) { } function buildImpObject(bid) { - const { params: { uid }, bidId, mediaTypes, sizes } = bid; + const { params: { uid }, bidId, mediaTypes, sizes, adUnitCode } = bid; const video = mediaTypes && _isVideoBid(bid) && _isValidVideoBid(bid) && makeVideo(mediaTypes.video); const banner = makeBanner((mediaTypes && mediaTypes.banner) || (!video && { sizes })); const impObject = { @@ -249,10 +258,14 @@ function buildImpObject(bid) { ...(banner && { banner }), ...(video && { video }), ext: { - bidder: { uid: Number(uid) }, + bidder: { uid: parseInt(uid) }, } }; + if (impObject.banner) { + impObject.ext.bidder.adslotExists = _isAdSlotExists(adUnitCode); + } + if (impObject.ext.bidder.uid && (impObject.banner || impObject.video)) { return impObject; } @@ -355,4 +368,17 @@ function _isValidVideoBid(bid, logErrors = false) { return result; } +function _isAdSlotExists(adUnitCode) { + if (document.getElementById(adUnitCode)) { + return true; + } + + const gptAdSlot = getGptSlotInfoForAdUnitCode(adUnitCode); + if (gptAdSlot && gptAdSlot.divId && document.getElementById(gptAdSlot.divId)) { + return true; + } + + return false; +} + registerBidder(spec); diff --git a/modules/visxBidAdapter.md b/modules/visxBidAdapter.md index 9578f7cc4a7..34ebe9bb937 100644 --- a/modules/visxBidAdapter.md +++ b/modules/visxBidAdapter.md @@ -3,7 +3,7 @@ ``` Module Name: YOC VIS.X Bidder Adapter Module Type: Bidder Adapter -Maintainer: service@yoc.com +Maintainer: supply.partners@yoc.com ``` # Description @@ -47,16 +47,14 @@ var adUnits = [ } ] }, - // YOC In-stream adUnit + // In-stream video adUnit { code: 'instream-test-div', mediaTypes: { video: { context: 'instream', - playerSize: [400, 300], - mimes: ['video/mp4'], - protocols: [3, 6] - }, + playerSize: [400, 300] + } }, bids: [ { diff --git a/modules/voxBidAdapter.js b/modules/voxBidAdapter.js index 8db97800630..25dbbda90cf 100644 --- a/modules/voxBidAdapter.js +++ b/modules/voxBidAdapter.js @@ -1,7 +1,7 @@ -import { _map, logWarn, deepAccess, isArray } from '../src/utils.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js' -import {BANNER, VIDEO} from '../src/mediaTypes.js' -import find from 'core-js-pure/features/array/find.js'; +import {_map, deepAccess, isArray, logWarn} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {find} from '../src/polyfill.js'; import {auctionManager} from '../src/auctionManager.js'; import {Renderer} from '../src/Renderer.js'; diff --git a/modules/waardexBidAdapter.js b/modules/waardexBidAdapter.js index ee17a71dd35..1a97e3bd351 100644 --- a/modules/waardexBidAdapter.js +++ b/modules/waardexBidAdapter.js @@ -1,8 +1,8 @@ -import { logError, isArray, deepAccess, getBidIdParameter } from '../src/utils.js'; +import {deepAccess, getBidIdParameter, isArray, logError} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; -import find from 'core-js-pure/features/array/find.js'; +import {find} from '../src/polyfill.js'; const ENDPOINT = `https://hb.justbidit.xyz:8843/prebid`; const BIDDER_CODE = 'waardex'; diff --git a/modules/weboramaRtdProvider.js b/modules/weboramaRtdProvider.js index 01086ad129f..0885df02f05 100644 --- a/modules/weboramaRtdProvider.js +++ b/modules/weboramaRtdProvider.js @@ -2,90 +2,556 @@ * This module adds Weborama provider to the real time data module * The {@link module:modules/realTimeData} module is required * The module will fetch contextual data (page-centric) from Weborama server + * and may access user-centric data from local storage * @module modules/weboramaRtdProvider * @requires module:modules/realTimeData */ +/** onData callback type + * @callback dataCallback + * @param {Object} data profile data + * @param {Boolean} site true if site, else it is user + * @returns {void} + */ + /** -* @typedef {Object} ModuleParams -* @property {WeboCtxConf} weboCtxConf -*/ + * @typedef {Object} ModuleParams + * @property {?Boolean} setPrebidTargeting if true, will set the GAM targeting (default undefined) + * @property {?Boolean} sendToBidders if true, will send the contextual profile to all bidders (default undefined) + * @property {?dataCallback} onData callback + * @property {?WeboCtxConf} weboCtxConf + * @property {?WeboUserDataConf} weboUserDataConf + */ + +/** + * @typedef {Object} WeboCtxConf + * @property {string} token required token to be used on bigsea contextual API requests + * @property {?string} targetURL specify the target url instead use the referer + * @property {?Boolean} setPrebidTargeting if true, will set the GAM targeting (default params.setPrebidTargeting or true) + * @property {?Boolean} sendToBidders if true, will send the contextual profile to all bidders (default params.sendToBidders or true) + * @property {?dataCallback} onData callback + * @property {?object} defaultProfile to be used if the profile is not found + * @property {?Boolean} enabled if false, will ignore this configuration + */ /** -* @typedef {Object} WeboCtxConf -* @property {string} token required token to be used on bigsea contextual API requests -* @property {?string} targetURL specify the target url instead use the referer -* @property {?boolean} setTargeting if true will set the GAM targeting -* @property {?object} defaultProfile to be used if the profile is not found -*/ - -import { deepSetValue, logError, tryAppendQueryString, logMessage } from '../src/utils.js'; -import {submodule} from '../src/hook.js'; -import {ajax} from '../src/ajax.js'; -import {config} from '../src/config.js'; + * @typedef {Object} WeboUserDataConf + * @property {?number} accountId wam account id + * @property {?Boolean} setPrebidTargeting if true, will set the GAM targeting (default params.setPrebidTargeting or true) + * @property {?Boolean} sendToBidders if true, will send the user-centric profile to all bidders (default params.sendToBidders or true) + * @property {?object} defaultProfile to be used if the profile is not found + * @property {?dataCallback} onData callback + * @property {?string} localStorageProfileKey can be used to customize the local storage key (default is 'webo_wam2gam_entry') + * @property {?Boolean} enabled if false, will ignore this configuration + */ + +import { + getGlobal +} from '../src/prebidGlobal.js'; +import { + deepSetValue, + deepAccess, + isEmpty, + mergeDeep, + logError, + logWarn, + tryAppendQueryString, + logMessage, + isFn +} from '../src/utils.js'; +import { + submodule +} from '../src/hook.js'; +import { + ajax +} from '../src/ajax.js'; +import { + getStorageManager +} from '../src/storageManager.js'; + +const adapterManager = require('../src/adapterManager.js').default; /** @type {string} */ const MODULE_NAME = 'realTimeData'; /** @type {string} */ const SUBMODULE_NAME = 'weborama'; /** @type {string} */ -const WEBO_CTX = 'webo_ctx'; +export const DEFAULT_LOCAL_STORAGE_USER_PROFILE_KEY = 'webo_wam2gam_entry'; /** @type {string} */ -const WEBO_DS = 'webo_ds'; +const LOCAL_STORAGE_USER_TARGETING_SECTION = 'targeting'; +/** @type {number} */ +const GVLID = 284; +/** @type {object} */ +export const storage = getStorageManager({gvlid: GVLID, moduleName: SUBMODULE_NAME}); /** @type {null|Object} */ -let _bigseaContextualProfile = null; +let _weboContextualProfile = null; -/** function that provides ad server targeting data to RTD-core -* @param {Array} adUnitsCodes -* @param {Object} moduleConfig -* @returns {Object} target data +/** @type {Boolean} */ +let _weboCtxInitialized = false; + +/** @type {null|Object} */ +let _weboUserDataUserProfile = null; + +/** @type {Boolean} */ +let _weboUserDataInitialized = false; + +/** Initialize module + * @param {object} moduleConfig + * @return {Boolean} true if module was initialized with success */ -function getTargetingData(adUnitsCodes, moduleConfig) { +function init(moduleConfig) { moduleConfig = moduleConfig || {}; const moduleParams = moduleConfig.params || {}; - const weboCtxConf = moduleParams.weboCtxConf || {}; - const defaultContextualProfiles = weboCtxConf.defaultProfile || {} - const profile = _bigseaContextualProfile || defaultContextualProfiles; + const weboCtxConf = moduleParams.weboCtxConf; + const weboUserDataConf = moduleParams.weboUserDataConf; - if (weboCtxConf.setOrtb2) { - const ortb2 = config.getConfig('ortb2') || {}; - if (profile[WEBO_CTX]) { - deepSetValue(ortb2, 'site.ext.data.webo_ctx', profile[WEBO_CTX]); - } - if (profile[WEBO_DS]) { - deepSetValue(ortb2, 'site.ext.data.webo_ds', profile[WEBO_DS]); - } - config.setConfig({ortb2: ortb2}); + _weboCtxInitialized = initWeboCtx(moduleParams, weboCtxConf); + _weboUserDataInitialized = initWeboUserData(moduleParams, weboUserDataConf); + + return _weboCtxInitialized || _weboUserDataInitialized; +} + +/** Initialize contextual sub module + * @param {ModuleParams} moduleParams + * @param {WeboCtxConf} weboCtxConf + * @return {Boolean} true if sub module was initialized with success + */ +function initWeboCtx(moduleParams, weboCtxConf) { + if (!weboCtxConf || weboCtxConf.enabled === false) { + moduleParams.weboCtxConf = null; + + return false } - if (weboCtxConf.setTargeting === false) { - return {}; + normalizeConf(moduleParams, weboCtxConf); + + _weboCtxInitialized = false; + _weboContextualProfile = null; + + if (!weboCtxConf.token) { + logWarn('missing param "token" for weborama contextual sub module initialization'); + return false; + } + + logMessage('weborama contextual intialized with success'); + + return true; +} + +/** Initialize weboUserData sub module + * @param {ModuleParams} moduleParams + * @param {WeboUserDataConf} weboUserDataConf + * @return {Boolean} true if sub module was initialized with success + */ +function initWeboUserData(moduleParams, weboUserDataConf) { + if (!weboUserDataConf || weboUserDataConf.enabled === false) { + moduleParams.weboUserDataConf = null; + + return false; } + normalizeConf(moduleParams, weboUserDataConf); + + _weboUserDataInitialized = false; + _weboUserDataUserProfile = null; + + let message = 'weborama user-centric intialized with success'; + if (weboUserDataConf.hasOwnProperty('accountId')) { + message = `weborama user-centric intialized with success for account: ${weboUserDataConf.accountId}`; + } + + logMessage(message); + + return true; +} + +/** @type {Object} */ +const globalDefaults = { + setPrebidTargeting: true, + sendToBidders: true, + onData: (data, kind, def) => logMessage('onData(data,kind,default)', data, kind, def), +} + +/** normalize submodule configuration + * @param {ModuleParams} moduleParams + * @param {WeboCtxConf|WeboUserDataConf} submoduleParams + * @return {void} + */ +function normalizeConf(moduleParams, submoduleParams) { + Object.entries(globalDefaults).forEach(([propertyName, globalDefaultValue]) => { + if (!submoduleParams.hasOwnProperty(propertyName)) { + const hasModuleParam = moduleParams.hasOwnProperty(propertyName); + submoduleParams[propertyName] = (hasModuleParam) ? moduleParams[propertyName] : globalDefaultValue; + } + }) +} + +/** function that provides ad server targeting data to RTD-core + * @param {Array} adUnitsCodes + * @param {Object} moduleConfig + * @returns {Object} target data + */ +function getTargetingData(adUnitsCodes, moduleConfig) { + moduleConfig = moduleConfig || {}; + const moduleParams = moduleConfig.params || {}; + const weboCtxConf = moduleParams.weboCtxConf || {}; + const weboUserDataConf = moduleParams.weboUserDataConf || {}; + const weboCtxConfTargeting = weboCtxConf.setPrebidTargeting; + const weboUserDataConfTargeting = weboUserDataConf.setPrebidTargeting; + try { - const formattedProfile = profile; - const r = adUnitsCodes.reduce((rp, adUnitCode) => { + const profile = getCompleteProfile(moduleParams, weboCtxConfTargeting, weboUserDataConfTargeting); + + if (isEmpty(profile)) { + return {}; + } + + const td = adUnitsCodes.reduce((data, adUnitCode) => { if (adUnitCode) { - rp[adUnitCode] = formattedProfile; + data[adUnitCode] = profile; } - return rp; + return data; }, {}); - return r; + + return td; } catch (e) { logError('unable to format weborama rtd targeting data', e); return {}; } } +/** function that provides complete profile formatted to be used + * @param {ModuleParams} moduleParams + * @param {Boolean} weboCtxConfTargeting + * @param {Boolean} weboUserDataConfTargeting + * @returns {Object} complete profile + */ +function getCompleteProfile(moduleParams, weboCtxConfTargeting, weboUserDataConfTargeting) { + const profile = {}; + + if (weboCtxConfTargeting) { + const contextualProfile = getContextualProfile(moduleParams.weboCtxConf || {}); + mergeDeep(profile, contextualProfile); + } + + if (weboUserDataConfTargeting) { + const weboUserDataProfile = getWeboUserDataProfile(moduleParams.weboUserDataConf || {}); + mergeDeep(profile, weboUserDataProfile); + } + + return profile; +} + +/** return contextual profile + * @param {WeboCtxConf} weboCtxConf + * @returns {Object} contextual profile + */ +function getContextualProfile(weboCtxConf) { + const defaultContextualProfile = weboCtxConf.defaultProfile || {}; + return _weboContextualProfile || defaultContextualProfile; +} + +/** return weboUserData profile + * @param {WeboUserDataConf} weboUserDataConf + * @returns {Object} weboUserData profile + */ +function getWeboUserDataProfile(weboUserDataConf) { + const weboUserDataDefaultUserProfile = weboUserDataConf.defaultProfile || {}; + + if (storage.localStorageIsEnabled() && !_weboUserDataUserProfile) { + const localStorageProfileKey = weboUserDataConf.localStorageProfileKey || DEFAULT_LOCAL_STORAGE_USER_PROFILE_KEY; + + const entry = storage.getDataFromLocalStorage(localStorageProfileKey); + if (entry) { + const data = JSON.parse(entry); + if (data && Object.keys(data).length > 0) { + _weboUserDataUserProfile = data[LOCAL_STORAGE_USER_TARGETING_SECTION]; + } + } + } + + return _weboUserDataUserProfile || weboUserDataDefaultUserProfile; +} + +/** function that will allow RTD sub-modules to modify the AdUnit object for each auction + * @param {Object} reqBidsConfigObj + * @param {doneCallback} onDone + * @param {Object} moduleConfig + * @returns {void} + */ +export function getBidRequestData(reqBidsConfigObj, onDone, moduleConfig) { + moduleConfig = moduleConfig || {}; + const moduleParams = moduleConfig.params || {}; + const weboCtxConf = moduleParams.weboCtxConf || {}; + + const adUnits = reqBidsConfigObj.adUnits || getGlobal().adUnits; + + if (!_weboCtxInitialized) { + handleBidRequestData(adUnits, moduleParams); + + onDone(); + + return; + } + + fetchContextualProfile(weboCtxConf, (data) => { + logMessage('fetchContextualProfile on getBidRequestData is done'); + + setWeboContextualProfile(data); + }, () => { + handleBidRequestData(adUnits, moduleParams); + + onDone(); + }); +} + +/** function that handles bid request data + * @param {Object[]} adUnits + * @param {ModuleParams} moduleParams + * @returns {void} + */ + +function handleBidRequestData(adUnits, moduleParams) { + const weboCtxConf = moduleParams.weboCtxConf || {}; + const weboUserDataConf = moduleParams.weboUserDataConf || {}; + const weboCtxConfTargeting = weboCtxConf.sendToBidders; + const weboUserDataConfTargeting = weboUserDataConf.sendToBidders; + + if (weboCtxConfTargeting) { + const contextualProfile = getContextualProfile(weboCtxConf); + if (!isEmpty(contextualProfile)) { + setBidRequestProfile(adUnits, contextualProfile, true); + } + } + + if (weboUserDataConfTargeting) { + const weboUserDataProfile = getWeboUserDataProfile(weboUserDataConf); + if (!isEmpty(weboUserDataProfile)) { + setBidRequestProfile(adUnits, weboUserDataProfile, false); + } + } + + handleOnData(weboCtxConf, weboUserDataConf); +} + +/** function that handle with onData callbacks + * @param {WeboCtxConf} weboCtxConf + * @param {WeboUserDataConf} weboUserDataConf + */ + +function handleOnData(weboCtxConf, weboUserDataConf) { + const callbacks = [{ + onData: weboCtxConf.onData, + fetchData: () => getContextualProfile(weboCtxConf), + site: true, + }, { + onData: weboUserDataConf.onData, + fetchData: () => getWeboUserDataProfile(weboUserDataConf), + site: false, + }]; + + callbacks.filter(obj => isFn(obj.onData)).forEach(obj => { + try { + const data = obj.fetchData(); + obj.onData(data, obj.site); + } catch (e) { + const kind = (obj.site) ? 'site' : 'user'; + logError(`error while executure onData callback with ${kind}-based data:`, e); + } + }); +} + +/** function that set bid request data on each segment (site or user centric) + * @param {Object[]} adUnits + * @param {Object} profile + * @param {Boolean} site true if site centric, else it is user centric + * @returns {void} + */ +function setBidRequestProfile(adUnits, profile, site) { + setGlobalOrtb2(profile, site); + + adUnits.forEach(adUnit => { + if (adUnit.hasOwnProperty('bids')) { + const adUnitCode = adUnit.code || 'no code'; + adUnit.bids.forEach(bid => handleBid(adUnitCode, profile, site, bid)); + } + }); +} + +/** @type {string} */ +const APPNEXUS = 'appnexus'; + +/** @type {string} */ +const PUBMATIC = 'pubmatic'; + +/** @type {string} */ +const RUBICON = 'rubicon'; + +/** @type {string} */ +const SMARTADSERVER = 'smartadserver'; + +/** @type {Object} */ +const bidderAliasRegistry = adapterManager.aliasRegistry || {}; + +/** handle individual bid + * @param {string} adUnitCode + * @param {Object} profile + * @param {Boolean} site true if site centric, else it is user centric + * @param {Object} bid + * @returns {void} + */ +function handleBid(adUnitCode, profile, site, bid) { + const bidder = bidderAliasRegistry[bid.bidder] || bid.bidder; + + logMessage(`handling on adunit '${adUnitCode}', bidder '${bidder}' and bid`, bid); + + switch (bidder) { + case APPNEXUS: + handleAppnexusBid(profile, bid); + + break; + + case PUBMATIC: + handlePubmaticBid(profile, bid); + + break; + + case SMARTADSERVER: + handleSmartadserverBid(profile, bid); + + break; + case RUBICON: + handleRubiconBid(profile, site, bid); + + break; + default: + logMessage(`unsupported bidder '${bidder}', trying via bidder ortb2 fpd`); + const section = ((site) ? 'site' : 'user'); + const base = `ortb2.${section}.ext.data`; + + assignProfileToObject(bid, base, profile); + } +} + +/** + * set ortb2 global data + * @param {Object} profile + * @param {Boolean} site + * @returns {void} + */ +function setGlobalOrtb2(profile, site) { + const section = ((site) ? 'site' : 'user'); + const base = `${section}.ext.data`; + const addOrtb2 = {}; + + assignProfileToObject(addOrtb2, base, profile); + + if (!isEmpty(addOrtb2)) { + const testGlobal = getGlobal().getConfig('ortb2') || {}; + const ortb2 = { + ortb2: mergeDeep({}, testGlobal, addOrtb2) + }; + getGlobal().setConfig(ortb2); + } +} + +/** + * assign profile to object + * @param {Object} destination + * @param {string} base + * @param {Object} profile + * @returns {void} + */ +function assignProfileToObject(destination, base, profile) { + Object.keys(profile).forEach(key => { + const path = `${base}.${key}`; + deepSetValue(destination, path, profile[key]) + }) +} + +/** handle rubicon bid + * @param {Object} profile + * @param {Boolean} site + * @param {Object} bid + * @returns {void} + */ +function handleRubiconBid(profile, site, bid) { + const section = (site) ? 'inventory' : 'visitor'; + const base = `params.${section}`; + assignProfileToObject(bid, base, profile); +} + +/** handle appnexus/xandr bid + * @param {Object} profile + * @param {Object} bid + * @returns {void} + */ +function handleAppnexusBid(profile, bid) { + const base = 'params.keywords'; + assignProfileToObject(bid, base, profile); +} + +/** handle pubmatic bid + * @param {Object} profile + * @param {Object} bid + * @returns {void} + */ +function handlePubmaticBid(profile, bid) { + const sep = '|'; + const subsep = ','; + const bidKey = 'params.dctr'; + const target = []; + + const data = deepAccess(bid, bidKey); + if (data) { + data.split(sep).forEach(t => target.push(t)); + } + + Object.keys(profile).forEach(key => { + const value = profile[key].join(subsep); + const keyword = `${key}=${value}`; + if (target.indexOf(keyword) === -1) { + target.push(keyword); + } + }); + + deepSetValue(bid, bidKey, target.join(sep)); +} + +/** handle smartadserver bid + * @param {Object} profile + * @param {Object} bid + * @returns {void} + */ +function handleSmartadserverBid(profile, bid) { + const sep = ';'; + const bidKey = 'params.target'; + const target = []; + + const data = deepAccess(bid, bidKey); + if (data) { + data.split(sep).forEach(t => target.push(t)); + } + + Object.keys(profile).forEach(key => { + profile[key].forEach(value => { + const keyword = `${key}=${value}`; + if (target.indexOf(keyword) === -1) { + target.push(keyword); + } + }); + }); + deepSetValue(bid, bidKey, target.join(sep)); +} + /** set bigsea contextual profile on module state - * if the profile is empty, will store the default profile * @param {null|Object} data * @returns {void} */ -export function setBigseaContextualProfile(data) { +export function setWeboContextualProfile(data) { if (data && Object.keys(data).length > 0) { - _bigseaContextualProfile = data; + _weboContextualProfile = data; } } @@ -96,9 +562,9 @@ export function setBigseaContextualProfile(data) { */ /** onDone callback type - * @callback doneCallback - * @returns {void} - */ + * @callback doneCallback + * @returns {void} + */ /** Fetch Bigsea Contextual Profile * @param {WeboCtxConf} weboCtxConf @@ -114,10 +580,10 @@ function fetchContextualProfile(weboCtxConf, onSuccess, onDone) { queryString = tryAppendQueryString(queryString, 'token', token); queryString = tryAppendQueryString(queryString, 'url', targetURL); - const url = 'https://ctx.weborama.com/api/profile?' + queryString; + const url = `https://ctx.weborama.com/api/profile?${queryString}`; ajax(url, { - success: function (response, req) { + success: function(response, req) { if (req.status === 200) { try { const data = JSON.parse(response); @@ -126,49 +592,28 @@ function fetchContextualProfile(weboCtxConf, onSuccess, onDone) { } catch (e) { onDone(); logError('unable to parse weborama data', e); + throw e; } } else if (req.status === 204) { onDone(); } }, - error: function () { + error: function() { onDone(); logError('unable to get weborama data'); } }, - null, - { + null, { method: 'GET', withCredentials: false, }); } -/** Initialize module - * @param {object} moduleConfig - * @return {boolean} true if module was initialized with success - */ -function init(moduleConfig) { - _bigseaContextualProfile = null; - - moduleConfig = moduleConfig || {}; - const moduleParams = moduleConfig.params || {}; - const weboCtxConf = moduleParams.weboCtxConf || {}; - - if (weboCtxConf.token) { - fetchContextualProfile(weboCtxConf, setBigseaContextualProfile, - () => logMessage('fetchContextualProfile on init is done')); - } else { - logError('missing param "token" for weborama rtd module initialization'); - return false; - } - - return true; -} - export const weboramaSubmodule = { name: SUBMODULE_NAME, init: init, getTargetingData: getTargetingData, + getBidRequestData: getBidRequestData, }; submodule(MODULE_NAME, weboramaSubmodule); diff --git a/modules/weboramaRtdProvider.md b/modules/weboramaRtdProvider.md index e7b9b96d668..732944c6e1c 100644 --- a/modules/weboramaRtdProvider.md +++ b/modules/weboramaRtdProvider.md @@ -10,8 +10,6 @@ Maintainer: prebid-support@weborama.com Weborama provides a Semantic AI Contextual API that classifies in Real-time a web page seen by a web user within generic and custom topics. It enables publishers to better monetize their inventory and unlock it to programmatic. -ORTB2 compliant and FPD support for Prebid versions < 4.29 - Contact prebid-support@weborama.com for information. ### Publisher Usage @@ -23,26 +21,51 @@ Compile the Weborama RTD module into your Prebid build: Add the Weborama RTD provider to your Prebid config. ```javascript -pbjs.setConfig( - ... - realTimeData: { - auctionDelay: 1000, - dataProviders: [ - { +var pbjs = pbjs || {}; +pbjs.que = pbjs.que || []; + +pbjs.que.push(function () { + pbjs.setConfig({ + debug: true, + realTimeData: { + auctionDelay: 1000, + dataProviders: [{ name: "weborama", waitForIt: true, params: { - weboCtxConf: { - setTargeting: true, - token: "<>", - targetURL: "..." // default is document.URL + setPrebidTargeting: true, // optional + sendToBidders: true, // optional + onData: function(data, site){ // optional + var kind = (site)? 'site' : 'user'; + console.log('onData', kind, data); + }, + weboCtxConf: { + token: "to-be-defined", // mandatory + targetURL: "https://prebid.org", // default is document.URL + setPrebidTargeting: true, // override param.setPrebidTargeting or default true + sendToBidders: true, // override param.sendToBidders or default true + defaultProfile: { // optional + webo_ctx: ['moon'], + webo_ds: ['bar'] + } + //, onData: function (data, ...) { ...} + }, + weboUserDataConf: { + accountId: 12345, // optional, used for logging + setPrebidTargeting: true, // override param.setPrebidTargeting or default true + sendToBidders: true, // override param.sendToBidders or default true + defaultProfile: { // optional + webo_cs: ['Red'], + webo_audiences: ['bam'] + }, + localStorageProfileKey: 'webo_wam2gam_entry' // default + //, onData: function (data, ...) { ...} } } - } - ] - } - ... -} + }] + } + }); +}); ``` ### Parameter Descriptions for the Weborama Configuration Section @@ -52,18 +75,82 @@ pbjs.setConfig( | name | String | Real time data module name | Mandatory. Always 'Weborama' | | waitForIt | Boolean | Mandatory. Required to ensure that the auction is delayed until prefetch is complete | Optional. Defaults to false but recommended to true | | params | Object | | Optional | -| params.weboCtxConf | Object | Weborama Contextual Configuration | Optional | -| params.weboCtxConf.token | String | Security Token provided by Weborama, unique per client | Mandatory | -| params.weboCtxConf.targetURL | String | Url to be profiled in the contextual api | Optional. Defaults to `document.URL` | -| params.weboCtxConf.defaultProfile | Object | default value of the profile to be used when there are no response from contextual api (such as timeout)| Optional. Default is `{}` | -| params.weboCtxConf.setTargeting|Boolean|If true, will use the contextual profile to set the gam targeting of all adunits managed by prebid.js| Optional. Default is *true*.| -| params.weboCtxConf.setOrtb2|Boolean|If true, will use the contextual profile to set the ortb2 configuration on `site.ext.data`| Optional. Default is *false*.| +| params.setPrebidTargeting | Boolean | If true, may use the profile to set the prebid (GPT/GAM or AST) targeting of all adunits managed by prebid.js | Optional. Affects the `weboCtxConf` and `weboUserDataConf` sections | +| params.sendToBidders | Boolean | If true, may send the profile to all bidders | Optional. Affects the `weboCtxConf` and `weboUserDataConf` sections | +| params.weboCtxConf | Object | Weborama Contextual Configuration | Optional +| params.weboUserDataConf | Object | Weborama User-Centric Configuration | Optional | +| params.onData | Callback | If set, will receive the profile and site flag | Optional. Affects the `weboCtxConf` and `weboUserDataConf` sections | + +#### Contextual Configuration + +| Name |Type | Description | Notes | +| :------------ | :------------ | :------------ |:------------ | +| token | String | Security Token provided by Weborama, unique per client | Mandatory | +| targetURL | String | Url to be profiled in the contextual api | Optional. Defaults to `document.URL` | +| setPrebidTargeting|Boolean|If true, will use the contextual profile to set the prebid (GPT/GAM or AST) targeting of all adunits managed by prebid.js| Optional. Default is `params.setPrebidTargeting` (if any) or **true**.| +| sendToBidders|Boolean|If true, will send the contextual profile to all bidders| Optional. Default is `params.sendToBidders` (if any) or **true**.| +| defaultProfile | Object | default value of the profile to be used when there are no response from contextual api (such as timeout)| Optional. Default is `{}` | +| onData | Callback | If set, will receive the profile and site flag | Optional. Default is `params.onData` (if any) or log via prebid debug | +| enabled | Boolean| if false, will ignore this configuration| default true| + +#### User-Centric Configuration + +| Name |Type | Description | Notes | +| :------------ | :------------ | :------------ |:------------ | +| accountId|Number|WAM account id. If present, will be used on logging and statistics| Optional.| +| setPrebidTargeting|Boolean|If true, will use the user profile to set the prebid (GPT/GAM or AST) targeting of all adunits managed by prebid.js| Optional. Default is `params.setPrebidTargeting` (if any) or **true**.| +| sendToBidders|Boolean|If true, will send the user profile to all bidders| Optional. Default is `params.sendToBidders` (if any) or **true**.| +| onData | Callback | If set, will receive the profile and site flag | Optional. Default is `params.onData` (if any) or log via prebid debug | +| defaultProfile | Object | default value of the profile to be used when there are no response from contextual api (such as timeout)| Optional. Default is `{}` | +| localStorageProfileKey| String | can be used to customize the local storage key | Optional | +| enabled | Boolean| if false, will ignore this configuration| default true| + +### Supported Bidders + +We currently support the following bidder adapters: +* SmartADServer SSP +* PubMatic SSP +* AppNexus SSP +* Rubicon SSP + +We also set the bidder and global ortb2 `site` and `user` sections. The following bidders may support it, to be sure, check the `First Party Data Support` on the feature list for the particular bidder from here: https://docs.prebid.org/dev-docs/bidders + +* Adagio +* AdformOpenRTB +* AdKernel +* AdMixer +* Adnuntius +* Adrelevantis +* adxcg +* AMX RTB +* Avocet +* BeOp +* Criteo +* Etarget +* Inmar +* Index Exchange +* Livewrapped +* Mediakeys +* NoBid +* OpenX +* Opt Out Advertising +* Ozone Project +* Proxistore +* Rise +* Smaato +* Sonobi +* TheMediaGrid +* TripleLift +* TrustX +* Yahoo SSP +* Yieldlab +* Zeta Global Ssp ### Testing To view an example of available segments returned by Weborama's backends: -`gulp serve --modules=rtdModule,weboramaRtdProvider,appnexusBidAdapter` +`gulp serve --notest --nolint --modules=rtdModule,weboramaRtdProvider,smartadserverBidAdapter,pubmaticBidAdapter,appnexusBidAdapter,rubiconBidAdapter,criteoBidAdapter` and then point your browser at: diff --git a/modules/welectBidAdapter.js b/modules/welectBidAdapter.js new file mode 100644 index 00000000000..d88a3f4c3e2 --- /dev/null +++ b/modules/welectBidAdapter.js @@ -0,0 +1,106 @@ +import { deepAccess } from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; + +const BIDDER_CODE = 'welect'; +const DEFAULT_DOMAIN = 'www.welect.de'; + +export const spec = { + code: BIDDER_CODE, + aliases: ['wlt'], + gvlid: 282, + supportedMediaTypes: ['video'], + + // short code + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (bid) { + return ( + deepAccess(bid, 'mediaTypes.video.context') === 'instream' && + !!bid.params.placementId + ); + }, + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function (validBidRequests) { + return validBidRequests.map((bidRequest) => { + let rawSizes = + deepAccess(bidRequest, 'mediaTypes.video.playerSize') || + bidRequest.sizes; + let size = rawSizes[0]; + + let domain = bidRequest.params.domain || DEFAULT_DOMAIN; + + let url = `https://${domain}/api/v2/preflight/${bidRequest.params.placementId}`; + + let gdprConsent = null; + + if (bidRequest && bidRequest.gdprConsent) { + gdprConsent = { + gdpr_consent: { + gdprApplies: bidRequest.gdprConsent.gdprApplies, + tcString: bidRequest.gdprConsent.gdprConsent, + }, + }; + } + + const data = { + width: size[0], + height: size[1], + bid_id: bidRequest.bidId, + ...gdprConsent, + }; + + return { + method: 'POST', + url: url, + data: data, + options: { + contentType: 'application/json', + withCredentials: false, + crossOrigin: true, + }, + }; + }); + }, + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function (serverResponse, bidRequest) { + const responseBody = serverResponse.body; + + if (typeof responseBody !== 'object' || responseBody.available !== true) { + return []; + } + + const bidResponses = []; + const bidResponse = { + requestId: responseBody.bidResponse.requestId, + cpm: responseBody.bidResponse.cpm, + width: responseBody.bidResponse.width, + height: responseBody.bidResponse.height, + creativeId: responseBody.bidResponse.creativeId, + currency: responseBody.bidResponse.currency, + netRevenue: responseBody.bidResponse.netRevenue, + ttl: responseBody.bidResponse.ttl, + ad: responseBody.bidResponse.ad, + vastUrl: responseBody.bidResponse.vastUrl, + meta: { + advertiserDomains: responseBody.bidResponse.meta.advertiserDomains + } + }; + bidResponses.push(bidResponse); + return bidResponses; + }, +}; +registerBidder(spec); diff --git a/modules/widespaceBidAdapter.js b/modules/widespaceBidAdapter.js index 7890628f94b..ba94f90f9c9 100644 --- a/modules/widespaceBidAdapter.js +++ b/modules/widespaceBidAdapter.js @@ -1,14 +1,8 @@ import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import { - parseQueryStringParameters, - parseSizesInput -} from '../src/utils.js'; -import includes from 'core-js-pure/features/array/includes.js'; -import find from 'core-js-pure/features/array/find.js'; -import { getStorageManager } from '../src/storageManager.js'; - -export const storage = getStorageManager(); +import {parseQueryStringParameters, parseSizesInput} from '../src/utils.js'; +import {find, includes} from '../src/polyfill.js'; +import {getStorageManager} from '../src/storageManager.js'; const BIDDER_CODE = 'widespace'; const WS_ADAPTER_VERSION = '2.0.1'; @@ -17,6 +11,7 @@ const LS_KEYS = { LC_UID: 'wsLcuid', CUST_DATA: 'wsCustomData' }; +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); let preReqTime = 0; diff --git a/modules/winrBidAdapter.js b/modules/winrBidAdapter.js index 9213c113460..124aba57866 100644 --- a/modules/winrBidAdapter.js +++ b/modules/winrBidAdapter.js @@ -1,12 +1,22 @@ -import { convertCamelToUnderscore, isArray, isNumber, isPlainObject, deepAccess, logError, convertTypes, getParameterByName, getBidRequest, isEmpty, transformBidderParamKeywords, isFn } from '../src/utils.js'; -import { config } from '../src/config.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER } from '../src/mediaTypes.js'; -import find from 'core-js-pure/features/array/find.js'; -import includes from 'core-js-pure/features/array/includes.js'; -import { getStorageManager } from '../src/storageManager.js'; - -export const storage = getStorageManager(); +import { + convertCamelToUnderscore, + convertTypes, + deepAccess, + getBidRequest, + getParameterByName, + isArray, + isEmpty, + isFn, + isNumber, + isPlainObject, + logError, + transformBidderParamKeywords +} from '../src/utils.js'; +import {config} from '../src/config.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER} from '../src/mediaTypes.js'; +import {find, includes} from '../src/polyfill.js'; +import {getStorageManager} from '../src/storageManager.js'; const BIDDER_CODE = 'winr'; const URL = 'https://ib.adnxs.com/ut/v3/prebid'; @@ -17,6 +27,8 @@ const SOURCE = 'pbjs'; const DEFAULT_CURRENCY = 'USD'; const GATE_COOKIE_NAME = 'wnr_gate'; +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); + function buildBid(bidData) { const bid = bidData; const position = { @@ -39,9 +51,9 @@ function wrapAd(bid, position) { - + ' + }, + 'rtb': { + 'banner': { + 'content': '', + 'width': 300, + 'height': 250 + }, + 'trackers': [ + { + 'impression_urls': [ + 'https://lax1-ib.adnxs.com/impression', + 'https://www.test.com/tracker' + ], + 'video_events': {} + } + ] + } + } + ] + } + ] + }; + + it('should get correct bid response', function () { + const expectedResponse = [ + { + 'requestId': '3db3773286ee59', + 'cpm': 0.5, + 'creativeId': 29681110, + 'dealId': undefined, + 'width': 300, + 'height': 250, + 'ad': '', + 'mediaType': 'banner', + 'currency': 'USD', + 'ttl': 300, + 'netRevenue': true, + 'adUnitCode': 'code', + 'appnexus': { + 'buyerMemberId': 958 + }, + 'meta': { + 'dchain': { + 'ver': '1.0', + 'complete': 0, + 'nodes': [{ + 'bsid': '958' + }] + } + } + } + ]; + const bidderRequest = { + bids: [{ + bidId: '3db3773286ee59', + adUnitCode: 'code' + }] + }; + const result = spec.interpretResponse({ body: response }, {bidderRequest}); + expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); + }); + + it('handles outstream video responses', function () { + const response = { + 'tags': [{ + 'uuid': '84ab500420319d', + 'ads': [{ + 'ad_type': 'video', + 'cpm': 0.500000, + 'notify_url': 'imptracker.com', + 'rtb': { + 'video': { + 'content': '' + } + }, + 'javascriptTrackers': '' + }] + }] + }; + const bidderRequest = { + bids: [{ + bidId: '84ab500420319d', + adUnitCode: 'code', + mediaTypes: { + video: { + context: 'outstream' + } + } + }] + } + + const result = spec.interpretResponse({ body: response }, {bidderRequest}); + expect(result[0]).not.to.have.property('vastXml'); + expect(result[0]).not.to.have.property('vastUrl'); + expect(result[0]).to.have.property('width', 1); + expect(result[0]).to.have.property('height', 1); + expect(result[0]).to.have.property('mediaType', 'banner'); + }); + }); + + describe('getUserSyncs', function() { + const syncOptions = { + syncEnabled: false + }; + + it('should not return sync', function() { + const serverResponse = [{ body: '' }]; + const result = spec.getUserSyncs(syncOptions, serverResponse); + expect(result).to.be.undefined; + }); + }); + + describe('transformBidParams', function() { + it('cast placementId to number', function() { + const adUnit = { + code: 'adunit-code', + params: { + placementId: '456' + } + }; + const bid = { + params: { + placementId: '456' + }, + sizes: [[300, 250]], + mediaTypes: { + banner: { sizes: [[300, 250]] } + } + }; + + const params = spec.transformBidParams({ placementId: '456' }, true, adUnit, [{ bidderCode: 'bigRichmedia', auctionId: bid.auctionId, bids: [bid] }]); + + expect(params.placement_id).to.exist; + expect(params.placement_id).to.be.a('number'); + }); + }); + + describe('onBidWon', function() { + it('Should not have any error', function() { + const result = spec.onBidWon({}); + expect(true).to.be.true; + }); + }); +}); diff --git a/test/spec/modules/bizzclickBidAdapter_spec.js b/test/spec/modules/bizzclickBidAdapter_spec.js index 500f45e0573..f80051b0a50 100644 --- a/test/spec/modules/bizzclickBidAdapter_spec.js +++ b/test/spec/modules/bizzclickBidAdapter_spec.js @@ -48,6 +48,17 @@ const BANNER_BID_REQUEST = { sizes: [[300, 250], [300, 600]] } }, + schain: { + ver: '1.0', + complete: 1, + nodes: [ + { + asi: 'example.com', + sid: '164', + hp: 1 + } + ] + }, bidder: 'bizzclick', params: { placementId: 'hash', @@ -252,6 +263,11 @@ describe('BizzclickAdapter', function() { expect(request.data[0].regs.ext.us_privacy).to.equal(BANNER_BID_REQUEST.uspConsent); }) + it('check schain is set properly', function() { + expect(request.data[0].source.ext.schain.complete).to.equal(1); + expect(request.data[0].source.ext.schain.ver).to.equal('1.0'); + }) + it('Returns valid URL', function () { expect(request.url).to.equal('https://us-e-node1.bizzclick.com/bid?rtb_seat_id=prebidjs&secret_key=accountId'); }); diff --git a/test/spec/modules/bliinkBidAdapter_spec.js b/test/spec/modules/bliinkBidAdapter_spec.js index 04a200d95a7..729605f7db8 100644 --- a/test/spec/modules/bliinkBidAdapter_spec.js +++ b/test/spec/modules/bliinkBidAdapter_spec.js @@ -477,6 +477,8 @@ const testsBuildRequests = [ refererInfo: getConfigBuildRequest('banner').refererInfo }, data: { + gdpr: false, + gdpr_consent: '', height: 250, width: 300, keywords: '', diff --git a/test/spec/modules/brandmetricsRtdProvider_spec.js b/test/spec/modules/brandmetricsRtdProvider_spec.js new file mode 100644 index 00000000000..3cac5a3d559 --- /dev/null +++ b/test/spec/modules/brandmetricsRtdProvider_spec.js @@ -0,0 +1,191 @@ +import * as brandmetricsRTD from '../../../modules/brandmetricsRtdProvider.js'; +import {config} from 'src/config.js'; + +const VALID_CONFIG = { + name: 'brandmetrics', + waitForIt: true, + params: { + scriptId: '00000000-0000-0000-0000-000000000000', + bidders: ['ozone'] + } +}; + +const NO_BIDDERS_CONFIG = { + name: 'brandmetrics', + waitForIt: true, + params: { + scriptId: '00000000-0000-0000-0000-000000000000' + } +}; + +const NO_SCRIPTID_CONFIG = { + name: 'brandmetrics', + waitForIt: true +}; + +const USER_CONSENT = { + gdpr: { + vendorData: { + vendor: { + consents: { + 422: true + } + }, + purpose: { + consents: { + 1: true, + 7: true + } + } + }, + gdprApplies: true + } +}; + +const NO_TCF_CONSENT = { + gdpr: { + vendorData: { + vendor: { + consents: { + 422: false + } + }, + purpose: { + consents: { + 1: false, + 7: false + } + } + }, + gdprApplies: true + } +}; + +const NO_USP_CONSENT = { + usp: '1NYY' +}; + +function mockSurveyLoaded(surveyConf) { + const commands = window._brandmetrics || []; + commands.forEach(command => { + if (command.cmd === '_addeventlistener') { + const conf = command.val; + if (conf.event === 'surveyloaded') { + conf.handler(surveyConf); + } + } + }); +} + +function scriptTagExists(url) { + const tags = document.getElementsByTagName('script'); + for (let i = 0; i < tags.length; i++) { + if (tags[i].src === url) { + return true; + } + } + return false; +} + +describe('BrandmetricsRTD module', () => { + beforeEach(function () { + const scriptTags = document.getElementsByTagName('script'); + for (let i = 0; i < scriptTags.length; i++) { + if (scriptTags[i].src.indexOf('brandmetrics') !== -1) { + scriptTags[i].remove(); + } + } + }); + + it('should init and return true', () => { + expect(brandmetricsRTD.brandmetricsSubmodule.init(VALID_CONFIG, USER_CONSENT)).to.equal(true); + }); + + it('should init and return true even if bidders is not included', () => { + expect(brandmetricsRTD.brandmetricsSubmodule.init(NO_BIDDERS_CONFIG, USER_CONSENT)).to.equal(true); + }); + + it('should init even if script- id is not configured', () => { + expect(brandmetricsRTD.brandmetricsSubmodule.init(NO_SCRIPTID_CONFIG, USER_CONSENT)).to.equal(true); + }); + + it('should not init when there is no TCF- consent', () => { + expect(brandmetricsRTD.brandmetricsSubmodule.init(VALID_CONFIG, NO_TCF_CONSENT)).to.equal(false); + }); + + it('should not init when there is no usp- consent', () => { + expect(brandmetricsRTD.brandmetricsSubmodule.init(VALID_CONFIG, NO_USP_CONSENT)).to.equal(false); + }); +}); + +describe('getBidRequestData', () => { + beforeEach(function () { + config.resetConfig() + }) + + it('should set targeting keys for specified bidders', () => { + brandmetricsRTD.brandmetricsSubmodule.getBidRequestData({}, () => { + const bidderConfig = config.getBidderConfig() + const expected = VALID_CONFIG.params.bidders + + expected.forEach(exp => { + expect(bidderConfig[exp].ortb2.user.ext.data.mockTargetKey).to.equal('mockMeasurementId') + }) + }, VALID_CONFIG); + + mockSurveyLoaded({ + available: true, + conf: { + displayOption: { + type: 'pbjs', + targetKey: 'mockTargetKey' + } + }, + survey: { + measurementId: 'mockMeasurementId' + } + }); + }); + + it('should only set targeting keys when the brandmetrics survey- type is "pbjs"', () => { + mockSurveyLoaded({ + available: true, + conf: { + displayOption: { + type: 'dfp', + targetKey: 'mockTargetKey' + } + }, + survey: { + measurementId: 'mockMeasurementId' + } + }); + + brandmetricsRTD.brandmetricsSubmodule.getBidRequestData({}, () => {}, VALID_CONFIG); + const bidderConfig = config.getBidderConfig() + expect(Object.keys(bidderConfig).length).to.equal(0) + }); + + it('should use a default targeting key name if the brandmetrics- configuration does not include one', () => { + mockSurveyLoaded({ + available: true, + conf: { + displayOption: { + type: 'pbjs', + } + }, + survey: { + measurementId: 'mockMeasurementId' + } + }); + + brandmetricsRTD.brandmetricsSubmodule.getBidRequestData({}, () => {}, VALID_CONFIG); + + const bidderConfig = config.getBidderConfig() + const expected = VALID_CONFIG.params.bidders + + expected.forEach(exp => { + expect(bidderConfig[exp].ortb2.user.ext.data.brandmetrics_survey).to.equal('mockMeasurementId') + }) + }); +}); diff --git a/test/spec/modules/browsiRtdProvider_spec.js b/test/spec/modules/browsiRtdProvider_spec.js index 32e1c7fe795..c36b48c5105 100644 --- a/test/spec/modules/browsiRtdProvider_spec.js +++ b/test/spec/modules/browsiRtdProvider_spec.js @@ -1,6 +1,8 @@ import * as browsiRTD from '../../../modules/browsiRtdProvider.js'; import {makeSlot} from '../integration/faker/googletag.js'; import * as utils from '../../../src/utils' +import * as events from '../../../src/events'; +import * as sinon from 'sinon'; describe('browsi Real time data sub module', function () { const conf = { @@ -15,6 +17,28 @@ describe('browsi Real time data sub module', function () { } }] }; + const auction = {adUnits: [ + { + code: 'adMock', + transactionId: 1 + }, + { + code: 'hasPrediction', + transactionId: 1 + } + ]}; + + let sandbox; + let eventsEmitSpy; + + before(() => { + sandbox = sinon.sandbox.create(); + eventsEmitSpy = sandbox.spy(events, ['emit']); + }); + + after(() => { + sandbox.restore(); + }); it('should init and return true', function () { browsiRTD.collectData(); @@ -61,13 +85,13 @@ describe('browsi Real time data sub module', function () { describe('should return data to RTD module', function () { it('should return empty if no ad units defined', function () { browsiRTD.setData({}); - expect(browsiRTD.browsiSubmodule.getTargetingData([])).to.eql({}); + expect(browsiRTD.browsiSubmodule.getTargetingData([], null, null, auction)).to.eql({}); }); it('should return NA if no prediction for ad unit', function () { makeSlot({code: 'adMock', divId: 'browsiAd_2'}); browsiRTD.setData({}); - expect(browsiRTD.browsiSubmodule.getTargetingData(['adMock'])).to.eql({adMock: {bv: 'NA'}}); + expect(browsiRTD.browsiSubmodule.getTargetingData(['adMock'], null, null, auction)).to.eql({adMock: {bv: 'NA'}}); }); it('should return prediction from server', function () { @@ -78,7 +102,7 @@ describe('browsi Real time data sub module', function () { pmd: undefined }; browsiRTD.setData(data); - expect(browsiRTD.browsiSubmodule.getTargetingData(['hasPrediction'])).to.eql({hasPrediction: {bv: '0.20'}}); + expect(browsiRTD.browsiSubmodule.getTargetingData(['hasPrediction'], null, null, auction)).to.eql({hasPrediction: {bv: '0.20'}}); }) }) @@ -135,4 +159,77 @@ describe('browsi Real time data sub module', function () { expect(utils.deepAccess(fakeAdUnits[1], 'ortb2Imp.ext.data.browsi')).to.eql({bv: '0.10'}); }) }) + + describe('should emit billable event', function () { + beforeEach(() => { + eventsEmitSpy.resetHistory(); + }) + it('should send one event per ad unit code', function () { + const auction = {adUnits: [ + { + code: 'a', + transactionId: 1 + }, + { + code: 'b', + transactionId: 2 + }, + { + code: 'a', + transactionId: 3 + }, + ]}; + + browsiRTD.browsiSubmodule.getTargetingData(['a', 'b'], null, null, auction); + expect(eventsEmitSpy.callCount).to.equal(2); + }) + it('should send events only for received ad unit codes', function () { + const auction = {adUnits: [ + { + code: 'a', + transactionId: 1 + }, + { + code: 'b', + transactionId: 2 + }, + { + code: 'c', + transactionId: 3 + }, + ]}; + + browsiRTD.browsiSubmodule.getTargetingData(['a'], null, null, auction); + expect(eventsEmitSpy.callCount).to.equal(1); + browsiRTD.browsiSubmodule.getTargetingData(['b'], null, null, auction); + expect(eventsEmitSpy.callCount).to.equal(2); + }) + it('should use 1st transaction ID in case of twin ad unit codes', function () { + const auction = { + auctionId: '123', + adUnits: [ + { + code: 'a', + transactionId: 1 + }, + { + code: 'a', + transactionId: 3 + }, + ]}; + + const expectedCall = { + vendor: 'browsi', + type: 'adRequest', + transactionId: 1, + auctionId: '123' + } + + browsiRTD.browsiSubmodule.getTargetingData(['a'], null, null, auction); + const callArguments = eventsEmitSpy.getCalls()[0].args[1]; + // billing id is random, we can't check its value + delete callArguments['billingId']; + expect(callArguments).to.eql(expectedCall); + }) + }) }); diff --git a/test/spec/modules/colossussspBidAdapter_spec.js b/test/spec/modules/colossussspBidAdapter_spec.js index 0fe4d5b358e..5a213589f4f 100644 --- a/test/spec/modules/colossussspBidAdapter_spec.js +++ b/test/spec/modules/colossussspBidAdapter_spec.js @@ -7,7 +7,8 @@ describe('ColossussspAdapter', function () { bidder: 'colossusssp', bidderRequestId: '145e1d6a7837c9', params: { - placement_id: 0 + placement_id: 0, + group_id: 0 }, placementCode: 'placementid_0', auctionId: '74f78609-a92d-4cf1-869f-1b244bbfb5d2', @@ -60,6 +61,7 @@ describe('ColossussspAdapter', function () { }); it('Should return false when placement_id is not a number', function () { bid.params.placement_id = 'aaa'; + delete bid.params.group_id; expect(spec.isBidRequestValid(bid)).to.be.false; }); }); @@ -95,9 +97,10 @@ describe('ColossussspAdapter', function () { let placements = data['placements']; for (let i = 0; i < placements.length; i++) { let placement = placements[i]; - expect(placement).to.have.all.keys('placementId', 'eids', 'bidId', 'traffic', 'sizes', 'schain', 'floor', 'gpid'); + expect(placement).to.have.all.keys('placementId', 'groupId', 'eids', 'bidId', 'traffic', 'sizes', 'schain', 'floor', 'gpid'); expect(placement.schain).to.be.an('object') expect(placement.placementId).to.be.a('number'); + expect(placement.groupId).to.be.a('number'); expect(placement.bidId).to.be.a('string'); expect(placement.traffic).to.be.a('string'); expect(placement.sizes).to.be.an('array'); @@ -186,14 +189,23 @@ describe('ColossussspAdapter', function () { }); }); + describe('onBidWon', function () { + it('should make an ajax call', function () { + const bid = { + nurl: 'http://example.com/win', + }; + expect(spec.onBidWon(bid)).to.equals(undefined); + }); + }) + describe('getUserSyncs', function () { - let userSync = spec.getUserSyncs(); + let userSync = spec.getUserSyncs({}, {}, {}, {}); it('Returns valid URL and type', function () { expect(userSync).to.be.an('array').with.lengthOf(1); expect(userSync[0].type).to.exist; expect(userSync[0].url).to.exist; - expect(userSync[0].type).to.be.equal('image'); - expect(userSync[0].url).to.be.equal('https://colossusssp.com/?c=o&m=cookie'); + expect(userSync[0].type).to.be.equal('hms.gif'); + expect(userSync[0].url).to.be.equal('https://sync.colossusssp.com/hms.gif?pbjs=1&coppa=0'); }); }); }); diff --git a/test/spec/modules/compassBidAdapter_spec.js b/test/spec/modules/compassBidAdapter_spec.js new file mode 100644 index 00000000000..28021c4f7c0 --- /dev/null +++ b/test/spec/modules/compassBidAdapter_spec.js @@ -0,0 +1,398 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/compassBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; + +const bidder = 'compass' + +describe('CompassBidAdapter', function () { + const bids = [ + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + placementId: 'testBanner', + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [VIDEO]: { + playerSize: [[300, 300]], + minduration: 5, + maxduration: 60 + } + }, + params: { + placementId: 'testVideo', + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [NATIVE]: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + } + }, + params: { + placementId: 'testNative', + } + } + ]; + + const invalidBid = { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + + } + } + + const bidderRequest = { + uspConsent: '1---', + gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + refererInfo: { + referer: 'https://test.com' + } + }; + + describe('isBidRequestValid', function () { + it('Should return true if there are bidId, params and key parameters present', function () { + expect(spec.isBidRequestValid(bids[0])).to.be.true; + }); + it('Should return false if at least one of parameters is not present', function () { + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests(bids, bidderRequest); + + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + + it('Returns valid URL', function () { + expect(serverRequest.url).to.equal('https://sa-lb.deliverimp.com/pbjs'); + }); + + it('Returns general data valid', function () { + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.all.keys('deviceWidth', + 'deviceHeight', + 'language', + 'secure', + 'host', + 'page', + 'placements', + 'coppa', + 'ccpa', + 'gdpr', + 'tmax' + ); + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.language).to.be.a('string'); + expect(data.secure).to.be.within(0, 1); + expect(data.host).to.be.a('string'); + expect(data.page).to.be.a('string'); + expect(data.coppa).to.be.a('number'); + expect(data.gdpr).to.be.a('string'); + expect(data.ccpa).to.be.a('string'); + expect(data.tmax).to.be.a('number'); + expect(data.placements).to.have.lengthOf(3); + }); + + it('Returns valid placements', function () { + const { placements } = serverRequest.data; + for (let i = 0, len = placements.length; i < len; i++) { + const placement = placements[i]; + expect(placement.placementId).to.be.oneOf(['testBanner', 'testVideo', 'testNative']); + expect(placement.adFormat).to.be.oneOf([BANNER, VIDEO, NATIVE]); + expect(placement.bidId).to.be.a('string'); + expect(placement.schain).to.be.an('object'); + expect(placement.bidfloor).to.exist.and.to.equal(0); + expect(placement.type).to.exist.and.to.equal('publisher'); + + if (placement.adFormat === BANNER) { + expect(placement.sizes).to.be.an('array'); + } + switch (placement.adFormat) { + case BANNER: + expect(placement.sizes).to.be.an('array'); + break; + case VIDEO: + expect(placement.playerSize).to.be.an('array'); + expect(placement.minduration).to.be.an('number'); + expect(placement.maxduration).to.be.an('number'); + break; + case NATIVE: + expect(placement.native).to.be.an('object'); + break; + } + } + }); + + it('Returns data with gdprConsent and without uspConsent', function () { + delete bidderRequest.uspConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data.gdpr).to.exist; + expect(data.gdpr).to.be.a('string'); + expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + expect(data.ccpa).to.not.exist; + delete bidderRequest.gdprConsent; + }); + + it('Returns data with uspConsent and without gdprConsent', function () { + bidderRequest.uspConsent = '1---'; + delete bidderRequest.gdprConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data.ccpa).to.exist; + expect(data.ccpa).to.be.a('string'); + expect(data.ccpa).to.equal(bidderRequest.uspConsent); + expect(data.gdpr).to.not.exist; + }); + + it('Returns empty data if no valid requests are passed', function () { + serverRequest = spec.buildRequests([], bidderRequest); + let data = serverRequest.data; + expect(data.placements).to.be.an('array').that.is.empty; + }); + }); + + describe('interpretResponse', function () { + it('Should interpret banner response', function () { + const banner = { + body: [{ + mediaType: 'banner', + width: 300, + height: 250, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let dataItem = bannerResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta'); + expect(dataItem.requestId).to.equal(banner.body[0].requestId); + expect(dataItem.cpm).to.equal(banner.body[0].cpm); + expect(dataItem.width).to.equal(banner.body[0].width); + expect(dataItem.height).to.equal(banner.body[0].height); + expect(dataItem.ad).to.equal(banner.body[0].ad); + expect(dataItem.ttl).to.equal(banner.body[0].ttl); + expect(dataItem.creativeId).to.equal(banner.body[0].creativeId); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal(banner.body[0].currency); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + it('Should interpret video response', function () { + const video = { + body: [{ + vastUrl: 'test.com', + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let videoResponses = spec.interpretResponse(video); + expect(videoResponses).to.be.an('array').that.is.not.empty; + + let dataItem = videoResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'vastUrl', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta'); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.5); + expect(dataItem.vastUrl).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + it('Should interpret native response', function () { + const native = { + body: [{ + mediaType: 'native', + native: { + clickUrl: 'test.com', + title: 'Test', + image: 'test.com', + impressionTrackers: ['test.com'], + }, + ttl: 120, + cpm: 0.4, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let nativeResponses = spec.interpretResponse(native); + expect(nativeResponses).to.be.an('array').that.is.not.empty; + + let dataItem = nativeResponses[0]; + expect(dataItem).to.have.keys('requestId', 'cpm', 'ttl', 'creativeId', 'netRevenue', 'currency', 'mediaType', 'native', 'meta'); + expect(dataItem.native).to.have.keys('clickUrl', 'impressionTrackers', 'title', 'image') + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.native.clickUrl).to.equal('test.com'); + expect(dataItem.native.title).to.equal('Test'); + expect(dataItem.native.image).to.equal('test.com'); + expect(dataItem.native.impressionTrackers).to.be.an('array').that.is.not.empty; + expect(dataItem.native.impressionTrackers[0]).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + it('Should return an empty array if invalid banner response is passed', function () { + const invBanner = { + body: [{ + width: 300, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + + let serverResponses = spec.interpretResponse(invBanner); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid video response is passed', function () { + const invVideo = { + body: [{ + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invVideo); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid native response is passed', function () { + const invNative = { + body: [{ + mediaType: 'native', + clickUrl: 'test.com', + title: 'Test', + impressionTrackers: ['test.com'], + ttl: 120, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }] + }; + let serverResponses = spec.interpretResponse(invNative); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid response is passed', function () { + const invalid = { + body: [{ + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invalid); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); + + describe('getUserSyncs', function() { + it('Should return array of objects with proper sync config , include GDPR', function() { + const syncData = spec.getUserSyncs({}, {}, { + consentString: 'ALL', + gdprApplies: true, + }, {}); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://sa-cs.deliverimp.com/image?pbjs=1&gdpr=1&gdpr_consent=ALL&coppa=0') + }); + it('Should return array of objects with proper sync config , include CCPA', function() { + const syncData = spec.getUserSyncs({}, {}, {}, { + consentString: '1---' + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://sa-cs.deliverimp.com/image?pbjs=1&ccpa_consent=1---&coppa=0') + }); + }); +}); diff --git a/test/spec/modules/consentManagementUsp_spec.js b/test/spec/modules/consentManagementUsp_spec.js index 7d3cd48a8e4..6dc46192128 100644 --- a/test/spec/modules/consentManagementUsp_spec.js +++ b/test/spec/modules/consentManagementUsp_spec.js @@ -8,9 +8,9 @@ import { } from 'modules/consentManagementUsp.js'; import * as utils from 'src/utils.js'; import { config } from 'src/config.js'; -import { uspDataHandler } from 'src/adapterManager.js'; +import {gdprDataHandler, uspDataHandler} from 'src/adapterManager.js'; +import 'src/prebid.js'; -let assert = require('chai').assert; let expect = require('chai').expect; function createIFrameMarker() { @@ -58,6 +58,12 @@ describe('consentManagement', function () { sinon.assert.notCalled(utils.logInfo); }); + it('should not produce any USP metadata', function() { + setConsentConfig({}); + let consentMeta = uspDataHandler.getConsentMeta(); + expect(consentMeta).to.be.undefined; + }); + it('should exit the consent manager if only config.gdpr is an object', function() { setConsentConfig({ gdpr: { cmpApi: 'iab' } }); expect(consentAPI).to.be.undefined; @@ -91,6 +97,21 @@ describe('consentManagement', function () { expect(consentAPI).to.be.equal('daa'); expect(consentTimeout).to.be.equal(7500); }); + + it('should enable uspDataHandler', () => { + setConsentConfig({usp: {cmpApi: 'daa', timeout: 7500}}); + expect(uspDataHandler.enabled).to.be.true; + }); + + it('should call setConsentData(null) on invalid CMP api', () => { + setConsentConfig({usp: {cmpApi: 'invalid'}}); + let hookRan = false; + requestBidsHook(() => { + hookRan = true; + }, {}); + expect(hookRan).to.be.true; + expect(uspDataHandler.ready).to.be.true; + }); }); describe('static consent string setConsentConfig value', () => { @@ -220,6 +241,32 @@ describe('consentManagement', function () { expect(consent).to.equal(testConsentData.uspString); sinon.assert.called(uspStub); }); + + it('should call uspDataHandler.setConsentData(null) on error', () => { + let hookRan = false; + uspStub = sinon.stub(window, '__uspapi').callsFake((...args) => { + args[2](null, false); + }); + requestBidsHook(() => { + hookRan = true; + }, {}); + expect(hookRan).to.be.true; + expect(uspDataHandler.ready).to.be.true; + expect(uspDataHandler.getConsentData()).to.equal(null); + }); + + it('should call uspDataHandler.setConsentData(null) on timeout', (done) => { + setConsentConfig({usp: {timeout: 10}}); + let hookRan = false; + uspStub = sinon.stub(window, '__uspapi').callsFake(() => {}); + requestBidsHook(() => { hookRan = true; }, {}); + setTimeout(() => { + expect(hookRan).to.be.true; + expect(uspDataHandler.ready).to.be.true; + expect(uspDataHandler.getConsentData()).to.equal(null); + done(); + }, 20) + }); }); describe('USPAPI workflow for iframed page', function () { @@ -366,6 +413,27 @@ describe('consentManagement', function () { expect(didHookReturn).to.be.true; expect(consent).to.equal(testConsentData.uspString); }); + + it('returns USP consent metadata', function () { + let testConsentData = { + uspString: '1NY' + }; + + uspapiStub = sinon.stub(window, '__uspapi').callsFake((...args) => { + args[2](testConsentData, true); + }); + + setConsentConfig(goodConfig); + requestBidsHook(() => { didHookReturn = true; }, {}); + + let consentMeta = uspDataHandler.getConsentMeta(); + + sinon.assert.notCalled(utils.logWarn); + sinon.assert.notCalled(utils.logError); + + expect(consentMeta.usp).to.equal(testConsentData.uspString); + expect(consentMeta.generatedAt).to.be.above(1644367751709); + }); }); }); }); diff --git a/test/spec/modules/consentManagement_spec.js b/test/spec/modules/consentManagement_spec.js index 5e9b0f07f46..d95af454818 100644 --- a/test/spec/modules/consentManagement_spec.js +++ b/test/spec/modules/consentManagement_spec.js @@ -2,6 +2,7 @@ import { setConsentConfig, requestBidsHook, resetConsentData, userCMP, consentTi import { gdprDataHandler } from 'src/adapterManager.js'; import * as utils from 'src/utils.js'; import { config } from 'src/config.js'; +import 'src/prebid.js'; let expect = require('chai').expect; @@ -45,6 +46,13 @@ describe('consentManagement', function () { expect(userCMP).to.be.undefined; sinon.assert.calledOnce(utils.logWarn); }); + + it('should not produce any consent metadata', function() { + setConsentConfig(undefined) + let consentMetadata = gdprDataHandler.getConsentMeta(); + expect(consentMetadata).to.be.undefined; + sinon.assert.calledOnce(utils.logWarn); + }) }); describe('valid setConsentConfig value', function () { @@ -124,6 +132,11 @@ describe('consentManagement', function () { }); expect(gdprScope).to.be.equal(false); }); + + it('should enable gdprDataHandler', () => { + setConsentConfig({gdpr: {}}); + expect(gdprDataHandler.enabled).to.be.true; + }); }); describe('static consent string setConsentConfig value', () => { @@ -318,6 +331,14 @@ describe('consentManagement', function () { expect(consent).to.be.null; }); + it('should call gpdrDataHandler.setConsentData() when unknown CMP api is used', () => { + setConsentConfig({gdpr: {cmpApi: 'invalid'}}); + let hookRan = false; + requestBidsHook(() => { hookRan = true; }, {}); + expect(hookRan).to.be.true; + expect(gdprDataHandler.ready).to.be.true; + }) + it('should throw proper errors when CMP is not found', function () { setConsentConfig(goodConfigWithCancelAuction); @@ -329,6 +350,7 @@ describe('consentManagement', function () { sinon.assert.calledTwice(utils.logError); expect(didHookReturn).to.be.false; expect(consent).to.be.null; + expect(gdprDataHandler.ready).to.be.true; }); }); @@ -667,6 +689,33 @@ describe('consentManagement', function () { expect(consent.apiVersion).to.equal(2); }); + it('produces gdpr metadata', function () { + let testConsentData = { + tcString: 'abc12345234', + gdprApplies: true, + purposeOneTreatment: false, + eventStatus: 'tcloaded', + vendorData: { + tcString: 'abc12345234' + } + }; + cmpStub = sinon.stub(window, '__tcfapi').callsFake((...args) => { + args[2](testConsentData, true); + }); + + setConsentConfig(goodConfigWithAllowAuction); + + requestBidsHook(() => { + didHookReturn = true; + }, {}); + let consentMeta = gdprDataHandler.getConsentMeta(); + sinon.assert.notCalled(utils.logError); + expect(consentMeta.consentStringSize).to.be.above(0) + expect(consentMeta.gdprApplies).to.be.true; + expect(consentMeta.apiVersion).to.equal(2); + expect(consentMeta.generatedAt).to.be.above(1644367751709); + }); + it('performs lookup check and stores consentData for a valid existing user with additional consent', function () { let testConsentData = { tcString: 'abc12345234', @@ -713,6 +762,28 @@ describe('consentManagement', function () { expect(didHookReturn).to.be.false; expect(bidsBackHandlerReturn).to.be.true; expect(consent).to.be.null; + expect(gdprDataHandler.ready).to.be.true; + }); + + it('allows the auction when CMP is unresponsive', (done) => { + setConsentConfig({ + cmpApi: 'iab', + timeout: 10, + defaultGdprScope: true + }); + + requestBidsHook(() => { + didHookReturn = true; + }, {}); + + setTimeout(() => { + expect(didHookReturn).to.be.true; + const consent = gdprDataHandler.getConsentData(); + expect(consent.gdprApplies).to.be.true; + expect(consent.consentString).to.be.undefined; + expect(gdprDataHandler.ready).to.be.true; + done(); + }, 20); }); it('It still considers it a valid cmp response if gdprApplies is not a boolean', function () { diff --git a/test/spec/modules/conversantBidAdapter_spec.js b/test/spec/modules/conversantBidAdapter_spec.js index e871ab3f9c6..53169326d3b 100644 --- a/test/spec/modules/conversantBidAdapter_spec.js +++ b/test/spec/modules/conversantBidAdapter_spec.js @@ -2,6 +2,8 @@ import {expect} from 'chai'; import {spec, storage} from 'modules/conversantBidAdapter.js'; import * as utils from 'src/utils.js'; import {createEidsArray} from 'modules/userId/eids.js'; +import { config } from '../../../src/config.js'; +import {deepAccess} from 'src/utils'; describe('Conversant adapter tests', function() { const siteId = '108060'; @@ -119,7 +121,34 @@ describe('Conversant adapter tests', function() { bidId: 'bid005', bidderRequestId: '117d765b87bed38', auctionId: 'req000' - }]; + }, + // video with first party data + { + bidder: 'conversant', + params: { + site_id: siteId + }, + mediaTypes: { + video: { + context: 'instream', + mimes: ['video/mp4', 'video/x-flv'] + } + }, + ortb2Imp: { + instl: 1, + ext: { + data: { + pbadslot: 'homepage-top-rect' + } + } + }, + placementCode: 'pcode006', + transactionId: 'tx006', + bidId: 'bid006', + bidderRequestId: '117d765b87bed38', + auctionId: 'req000' + } + ]; const bidResponses = { body: { @@ -216,7 +245,7 @@ describe('Conversant adapter tests', function() { expect(payload).to.have.property('id', 'req000'); expect(payload).to.have.property('at', 1); expect(payload).to.have.property('imp'); - expect(payload.imp).to.be.an('array').with.lengthOf(6); + expect(payload.imp).to.be.an('array').with.lengthOf(7); expect(payload.imp[0]).to.have.property('id', 'bid000'); expect(payload.imp[0]).to.have.property('secure', 1); @@ -306,6 +335,16 @@ describe('Conversant adapter tests', function() { expect(payload.imp[5].video).to.not.have.property('maxduration'); expect(payload.imp[5]).to.not.have.property('banner'); + expect(payload.imp[6]).to.have.property('id', 'bid006'); + expect(payload.imp[6]).to.have.property('video'); + expect(payload.imp[6].video).to.have.property('mimes'); + expect(payload.imp[6].video.mimes).to.deep.equal(['video/mp4', 'video/x-flv']); + expect(payload.imp[6]).to.not.have.property('banner'); + expect(payload.imp[6]).to.have.property('instl'); + expect(payload.imp[6]).to.have.property('ext'); + expect(payload.imp[6].ext).to.have.property('data'); + expect(payload.imp[6].ext.data).to.have.property('pbadslot'); + expect(payload).to.have.property('site'); expect(payload.site).to.have.property('id', siteId); expect(payload.site).to.have.property('mobile').that.is.oneOf([0, 1]); @@ -321,6 +360,34 @@ describe('Conversant adapter tests', function() { expect(payload).to.not.have.property('user'); // there should be no user by default }); + it('Verify first party data', () => { + const bidderRequest = {refererInfo: {referer: 'http://test.com?a=b&c=123'}}; + const cfg = {ortb2: {site: {content: {series: 'MySeries', season: 'MySeason', episode: 3, title: 'MyTitle'}}}}; + config.setConfig(cfg); + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = request.data; + expect(payload.site).to.have.property('content'); + expect(payload.site.content).to.have.property('series'); + expect(payload.site.content).to.have.property('season'); + expect(payload.site.content).to.have.property('episode'); + expect(payload.site.content).to.have.property('title'); + config.resetConfig(); + }); + + it('Verify supply chain data', () => { + const bidderRequest = {refererInfo: {referer: 'http://test.com?a=b&c=123'}}; + const schain = {complete: 1, ver: '1.0', nodes: [{asi: 'bidderA.com', sid: '00001', hp: 1}]}; + const bidsWithSchain = bidRequests.map((bid) => { + return Object.assign({ + schain: schain + }, bid); + }); + const request = spec.buildRequests(bidsWithSchain, bidderRequest); + const payload = request.data; + expect(deepAccess(payload, 'source.ext.schain.nodes')).to.exist; + expect(payload.source.ext.schain.nodes[0].asi).equals(schain.nodes[0].asi); + }); + it('Verify override url', function() { const testUrl = 'https://someurl?name=value'; const request = spec.buildRequests([{params: {white_label_url: testUrl}}]); diff --git a/test/spec/modules/criteoBidAdapter_spec.js b/test/spec/modules/criteoBidAdapter_spec.js index aa995b3c9a0..8793d9351d4 100755 --- a/test/spec/modules/criteoBidAdapter_spec.js +++ b/test/spec/modules/criteoBidAdapter_spec.js @@ -430,7 +430,11 @@ describe('The Criteo bidding adapter', function () { bidder: 'criteo', adUnitCode: 'bid-123', transactionId: 'transaction-123', - sizes: [[728, 90]], + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, params: {} }, ]; @@ -445,7 +449,11 @@ describe('The Criteo bidding adapter', function () { bidder: 'criteo', adUnitCode: 'bid-123', transactionId: 'transaction-123', - sizes: [[728, 90]], + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, params: { zoneId: 123, publisherSubId: '123', @@ -473,7 +481,11 @@ describe('The Criteo bidding adapter', function () { it('should keep undefined sizes for non native banner', function () { const bidRequests = [ { - sizes: [[undefined, undefined]], + mediaTypes: { + banner: { + sizes: [[undefined, undefined]] + } + }, params: {}, }, ]; @@ -486,7 +498,11 @@ describe('The Criteo bidding adapter', function () { it('should keep undefined size for non native banner', function () { const bidRequests = [ { - sizes: [undefined, undefined], + mediaTypes: { + banner: { + sizes: [undefined, undefined] + } + }, params: {}, }, ]; @@ -499,7 +515,11 @@ describe('The Criteo bidding adapter', function () { it('should properly detect and get sizes of native sizeless banner', function () { const bidRequests = [ { - sizes: [[undefined, undefined]], + mediaTypes: { + banner: { + sizes: [[undefined, undefined]] + } + }, params: { nativeCallback: function() {} }, @@ -514,7 +534,11 @@ describe('The Criteo bidding adapter', function () { it('should properly detect and get size of native sizeless banner', function () { const bidRequests = [ { - sizes: [undefined, undefined], + mediaTypes: { + banner: { + sizes: [undefined, undefined] + } + }, params: { nativeCallback: function() {} }, @@ -585,7 +609,11 @@ describe('The Criteo bidding adapter', function () { bidder: 'criteo', adUnitCode: 'bid-123', transactionId: 'transaction-123', - sizes: [[728, 90]], + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, params: { zoneId: 123, }, @@ -594,7 +622,11 @@ describe('The Criteo bidding adapter', function () { bidder: 'criteo', adUnitCode: 'bid-234', transactionId: 'transaction-234', - sizes: [[300, 250], [728, 90]], + mediaTypes: { + banner: { + sizes: [[300, 250], [728, 90]] + } + }, params: { networkId: 456, }, @@ -625,7 +657,11 @@ describe('The Criteo bidding adapter', function () { bidder: 'criteo', adUnitCode: 'bid-123', transactionId: 'transaction-123', - sizes: [[728, 90]], + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, params: { zoneId: 123, }, @@ -647,7 +683,11 @@ describe('The Criteo bidding adapter', function () { bidder: 'criteo', adUnitCode: 'bid-123', transactionId: 'transaction-123', - sizes: [[728, 90]], + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, params: { zoneId: 123, }, @@ -663,13 +703,42 @@ describe('The Criteo bidding adapter', function () { expect(request.data.user.uspIab).to.equal('1YNY'); }); + it('should properly build a request with schain object', function () { + const expectedSchain = { + someProperty: 'someValue' + }; + const bidRequests = [ + { + bidder: 'criteo', + schain: expectedSchain, + adUnitCode: 'bid-123', + transactionId: 'transaction-123', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + params: { + zoneId: 123, + }, + }, + ]; + + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.source.ext.schain).to.equal(expectedSchain); + }); + it('should properly build a request with if ccpa consent field is not provided', function () { const bidRequests = [ { bidder: 'criteo', adUnitCode: 'bid-123', transactionId: 'transaction-123', - sizes: [[728, 90]], + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, params: { zoneId: 123, }, @@ -690,7 +759,7 @@ describe('The Criteo bidding adapter', function () { bidder: 'criteo', adUnitCode: 'bid-123', transactionId: 'transaction-123', - sizes: [[728, 90]], + sizes: [[640, 480]], mediaTypes: { video: { playerSize: [640, 480], @@ -717,6 +786,7 @@ describe('The Criteo bidding adapter', function () { expect(request.method).to.equal('POST'); const ortbRequest = request.data; expect(ortbRequest.slots[0].video.mimes).to.deep.equal(['video/mp4', 'video/x-flv']); + expect(ortbRequest.slots[0].sizes).to.deep.equal([]); expect(ortbRequest.slots[0].video.playersizes).to.deep.equal(['640x480']); expect(ortbRequest.slots[0].video.maxduration).to.equal(30); expect(ortbRequest.slots[0].video.api).to.deep.equal([1, 2]); @@ -734,7 +804,7 @@ describe('The Criteo bidding adapter', function () { bidder: 'criteo', adUnitCode: 'bid-123', transactionId: 'transaction-123', - sizes: [[728, 90]], + sizes: [[640, 480], [800, 600]], mediaTypes: { video: { playerSize: [[640, 480], [800, 600]], @@ -760,6 +830,7 @@ describe('The Criteo bidding adapter', function () { expect(request.url).to.match(/^https:\/\/bidder\.criteo\.com\/cdb\?profileId=207&av=\d+&wv=[^&]+&cb=\d/); expect(request.method).to.equal('POST'); const ortbRequest = request.data; + expect(ortbRequest.slots[0].sizes).to.deep.equal([]); expect(ortbRequest.slots[0].video.mimes).to.deep.equal(['video/mp4', 'video/x-flv']); expect(ortbRequest.slots[0].video.playersizes).to.deep.equal(['640x480', '800x600']); expect(ortbRequest.slots[0].video.maxduration).to.equal(30); @@ -778,7 +849,7 @@ describe('The Criteo bidding adapter', function () { bidder: 'criteo', adUnitCode: 'bid-123', transactionId: 'transaction-123', - sizes: [[728, 90]], + sizes: [[300, 250]], mediaTypes: { video: { playerSize: [ [300, 250] ], @@ -800,6 +871,7 @@ describe('The Criteo bidding adapter', function () { expect(request.url).to.match(/^https:\/\/bidder\.criteo\.com\/cdb\?profileId=207&av=\d+&wv=[^&]+&cb=\d/); expect(request.method).to.equal('POST'); const ortbRequest = request.data; + expect(ortbRequest.slots[0].sizes).to.deep.equal([]); expect(ortbRequest.slots[0].video.playersizes).to.deep.equal(['300x250']); expect(ortbRequest.slots[0].video.mimes).to.deep.equal(['video/mp4', 'video/MPV', 'video/H264', 'video/webm', 'video/ogg']); expect(ortbRequest.slots[0].video.minduration).to.equal(1); @@ -816,7 +888,11 @@ describe('The Criteo bidding adapter', function () { bidder: 'criteo', adUnitCode: 'bid-123', transactionId: 'transaction-123', - sizes: [[728, 90]], + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, params: { zoneId: 123, }, @@ -838,7 +914,11 @@ describe('The Criteo bidding adapter', function () { bidder: 'criteo', adUnitCode: 'bid-123', transactionId: 'transaction-123', - sizes: [[728, 90]], + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, params: { zoneId: 123 } @@ -863,7 +943,11 @@ describe('The Criteo bidding adapter', function () { bidder: 'criteo', adUnitCode: 'bid-123', transactionId: 'transaction-123', - sizes: [[728, 90]], + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, params: { zoneId: 123, ext: { @@ -907,7 +991,11 @@ describe('The Criteo bidding adapter', function () { bidder: 'criteo', adUnitCode: 'bid-123', transactionId: 'transaction-123', - sizes: [[728, 90]], + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, params: { zoneId: 123, ext: { diff --git a/test/spec/modules/criteoIdSystem_spec.js b/test/spec/modules/criteoIdSystem_spec.js index 828b8401af1..d50eadebb55 100644 --- a/test/spec/modules/criteoIdSystem_spec.js +++ b/test/spec/modules/criteoIdSystem_spec.js @@ -116,16 +116,19 @@ describe('CriteoId module', function () { expect(setCookieStub.calledWith('cto_bundle')).to.be.false; expect(setLocalStorageStub.calledWith('cto_bundle')).to.be.false; } else if (response.bundle) { - expect(setCookieStub.calledWith('cto_bundle', response.bundle, expirationTs)).to.be.true; + expect(setCookieStub.calledWith('cto_bundle', response.bundle, expirationTs, null, '.com')).to.be.true; + expect(setCookieStub.calledWith('cto_bundle', response.bundle, expirationTs, null, '.testdev.com')).to.be.true; expect(setLocalStorageStub.calledWith('cto_bundle', response.bundle)).to.be.true; expect(triggerPixelStub.called).to.be.false; } if (response.bidId) { - expect(setCookieStub.calledWith('cto_bidid', response.bidId, expirationTs)).to.be.true; + expect(setCookieStub.calledWith('cto_bidid', response.bidId, expirationTs, null, '.com')).to.be.true; + expect(setCookieStub.calledWith('cto_bidid', response.bidId, expirationTs, null, '.testdev.com')).to.be.true; expect(setLocalStorageStub.calledWith('cto_bidid', response.bidId)).to.be.true; } else { - expect(setCookieStub.calledWith('cto_bidid', '', pastDateString)).to.be.true; + expect(setCookieStub.calledWith('cto_bidid', '', pastDateString, null, '.com')).to.be.true; + expect(setCookieStub.calledWith('cto_bidid', '', pastDateString, null, '.testdev.com')).to.be.true; expect(removeFromLocalStorageStub.calledWith('cto_bidid')).to.be.true; } }); diff --git a/test/spec/modules/currency_spec.js b/test/spec/modules/currency_spec.js index ccd205964a9..928c252943c 100644 --- a/test/spec/modules/currency_spec.js +++ b/test/spec/modules/currency_spec.js @@ -9,8 +9,11 @@ import { setConfig, addBidResponseHook, currencySupportEnabled, - currencyRates + currencyRates, + ready } from 'modules/currency.js'; +import {createBid} from '../../../src/bidfactory.js'; +import CONSTANTS from '../../../src/constants.json'; var assert = require('chai').assert; var expect = require('chai').expect; @@ -22,8 +25,13 @@ describe('currency', function () { let fn = sinon.spy(); + function makeBid(bidProps) { + return Object.assign(createBid(CONSTANTS.STATUS.GOOD), bidProps); + } + beforeEach(function () { fakeCurrencyFileServer = sinon.fakeServer.create(); + ready.reset(); }); afterEach(function () { @@ -286,7 +294,7 @@ describe('currency', function () { }); describe('currency.addBidResponseDecorator bidResponseQueue', function () { - it('not run until currency rates file is loaded', function () { + it('not run until currency rates file is loaded', function (done) { setConfig({}); fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); @@ -296,21 +304,34 @@ describe('currency', function () { setConfig({ 'adServerCurrency': 'JPY' }); var marker = false; - addBidResponseHook(function() { + let promiseResolved = false; + addBidResponseHook(Object.assign(function() { marker = true; - }, 'elementId', bid); + }, { + bail: function (promise) { + promise.then(() => promiseResolved = true); + } + }), 'elementId', bid); expect(marker).to.equal(false); - fakeCurrencyFileServer.respond(); - expect(marker).to.equal(true); + setTimeout(() => { + expect(promiseResolved).to.be.false; + fakeCurrencyFileServer.respond(); + + setTimeout(() => { + expect(marker).to.equal(true); + expect(promiseResolved).to.be.true; + done(); + }); + }); }); }); describe('currency.addBidResponseDecorator', function () { it('should leave bid at 1 when currency support is not enabled and fromCurrency is USD', function () { setConfig({}); - var bid = { 'cpm': 1, 'currency': 'USD' }; + var bid = makeBid({ 'cpm': 1, 'currency': 'USD' }); var innerBid; addBidResponseHook(function(adCodeId, bid) { innerBid = bid; @@ -321,7 +342,7 @@ describe('currency', function () { it('should result in NO_BID when currency support is not enabled and fromCurrency is not USD', function () { setConfig({}); - var bid = { 'cpm': 1, 'currency': 'GBP' }; + var bid = makeBid({ 'cpm': 1, 'currency': 'GBP' }); var innerBid; addBidResponseHook(function(adCodeId, bid) { innerBid = bid; @@ -333,7 +354,7 @@ describe('currency', function () { setConfig({ 'adServerCurrency': 'USD' }); - var bid = { 'cpm': 1, 'currency': 'USD' }; + var bid = makeBid({ 'cpm': 1, 'currency': 'USD' }); var innerBid; addBidResponseHook(function(adCodeId, bid) { innerBid = bid; @@ -348,7 +369,7 @@ describe('currency', function () { fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); setConfig({ 'adServerCurrency': 'JPY' }); fakeCurrencyFileServer.respond(); - var bid = { 'cpm': 1, 'currency': 'ABC' }; + var bid = makeBid({ 'cpm': 1, 'currency': 'ABC' }); var innerBid; addBidResponseHook(function(adCodeId, bid) { innerBid = bid; @@ -360,7 +381,7 @@ describe('currency', function () { fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); setConfig({ 'adServerCurrency': 'ABC' }); fakeCurrencyFileServer.respond(); - var bid = { 'cpm': 1, 'currency': 'GBP' }; + var bid = makeBid({ 'cpm': 1, 'currency': 'GBP' }); var innerBid; addBidResponseHook(function(adCodeId, bid) { innerBid = bid; @@ -372,7 +393,7 @@ describe('currency', function () { fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); setConfig({ 'adServerCurrency': 'JPY' }); fakeCurrencyFileServer.respond(); - var bid = { 'cpm': 1, 'currency': 'JPY' }; + var bid = makeBid({ 'cpm': 1, 'currency': 'JPY' }); var innerBid; addBidResponseHook(function(adCodeId, bid) { innerBid = bid; @@ -385,7 +406,7 @@ describe('currency', function () { fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); setConfig({ 'adServerCurrency': 'GBP' }); fakeCurrencyFileServer.respond(); - var bid = { 'cpm': 1, 'currency': 'USD' }; + var bid = makeBid({ 'cpm': 1, 'currency': 'USD' }); var innerBid; addBidResponseHook(function(adCodeId, bid) { innerBid = bid; @@ -398,7 +419,7 @@ describe('currency', function () { fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); setConfig({ 'adServerCurrency': 'GBP' }); fakeCurrencyFileServer.respond(); - var bid = { 'cpm': 1, 'currency': 'CNY' }; + var bid = makeBid({ 'cpm': 1, 'currency': 'CNY' }); var innerBid; addBidResponseHook(function(adCodeId, bid) { innerBid = bid; @@ -411,7 +432,7 @@ describe('currency', function () { fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); setConfig({ 'adServerCurrency': 'CNY' }); fakeCurrencyFileServer.respond(); - var bid = { 'cpm': 1, 'currency': 'JPY' }; + var bid = makeBid({ 'cpm': 1, 'currency': 'JPY' }); var innerBid; addBidResponseHook(function(adCodeId, bid) { innerBid = bid; diff --git a/test/spec/modules/cwireBidAdapter_spec.js b/test/spec/modules/cwireBidAdapter_spec.js index 4ed19cf7b74..b21e73e1561 100644 --- a/test/spec/modules/cwireBidAdapter_spec.js +++ b/test/spec/modules/cwireBidAdapter_spec.js @@ -1,5 +1,6 @@ import { expect } from 'chai'; import * as utils from '../../../src/utils.js'; +import { config } from '../../../src/config.js'; import { spec, CW_PAGE_VIEW_ID, @@ -24,7 +25,6 @@ const BID_DEFAULTS = { params: { placementId: 123456, pageId: 777, - adUnitElementId: 'target-div' }, sizes: [[300, 250], [1, 1]], }; @@ -36,11 +36,6 @@ const BidderRequestBuilder = function BidderRequestBuilder(options) { bidderRequestId: BID_DEFAULTS.request.bidderRequestId, transactionId: BID_DEFAULTS.request.transactionId, timeout: 3000, - refererInfo: { - numIframes: 0, - reachedTop: true, - referer: 'http://test.io/index.html?pbjs_debug=true' - } }; const request = { @@ -82,17 +77,15 @@ const BidRequestBuilder = function BidRequestBuilder(options, deleteKeys) { }; describe('C-WIRE bid adapter', () => { - let utilsMock; let sandbox; beforeEach(() => { - utilsMock = sinon.mock(utils); sandbox = sinon.createSandbox(); }); afterEach(() => { - utilsMock.restore(); sandbox.restore(); + config.resetConfig(); }); // START TESTING @@ -127,25 +120,149 @@ describe('C-WIRE bid adapter', () => { bid01.params.pageId = '3320'; expect(spec.isBidRequestValid(bid01)).to.equal(false); }); + }); + + describe('C-WIRE - buildRequests()', function () { + it('creates a valid request', function () { + const bid01 = new BidRequestBuilder({ + mediaTypes: { + banner: { + sizes: [[1, 1]], + } + } + }).withParams({ + cwcreative: 54321, + cwapikey: 'xxx-xxx-yyy-zzz-uuid', + refgroups: 'group_1', + }).build(); + + const bidderRequest01 = new BidderRequestBuilder().build(); + + const requests = spec.buildRequests([bid01], bidderRequest01); + + expect(requests.data.slots.length).to.equal(1); + expect(requests.data.cwid).to.be.null; + expect(requests.data.cwid).to.be.null; + expect(requests.data.slots[0].sizes[0]).to.equal('1x1'); + expect(requests.data.cwcreative).to.equal(54321); + expect(requests.data.cwapikey).to.equal('xxx-xxx-yyy-zzz-uuid'); + expect(requests.data.refgroups[0]).to.equal('group_1'); + }); - it('should use params.adUnitElementId if provided', function () { + it('creates a valid request - read debug params from second bid', function () { const bid01 = new BidRequestBuilder().withParams().build(); - expect(spec.isBidRequestValid(bid01)).to.equal(true); - expect(bid01.params.adUnitElementId).to.exist; - expect(bid01.params.adUnitElementId).to.equal('target-div'); + const bid02 = new BidRequestBuilder({ + mediaTypes: { + banner: { + sizes: [[1, 1]], + } + } + }).withParams({ + cwcreative: 1234, + cwapikey: 'api_key_5', + refgroups: 'group_5', + }).build(); + + const bidderRequest01 = new BidderRequestBuilder().build(); + + const requests = spec.buildRequests([bid01, bid02], bidderRequest01); + + expect(requests.data.slots.length).to.equal(2); + expect(requests.data.cwid).to.be.null; + expect(requests.data.cwid).to.be.null; + expect(requests.data.cwcreative).to.equal(1234); + expect(requests.data.cwapikey).to.equal('api_key_5'); + expect(requests.data.refgroups[0]).to.equal('group_5'); }); - it('should use default adUnitCode if no adUnitElementId provided', function () { - const bid01 = new BidRequestBuilder().withParams({}, ['adUnitElementId']).build(); - expect(spec.isBidRequestValid(bid01)).to.equal(true); - expect(bid01.params.adUnitElementId).to.exist; - expect(bid01.params.adUnitElementId).to.equal('original-div'); + it('creates a valid request - read debug params from first bid, ignore second', function () { + const bid01 = new BidRequestBuilder() + .withParams({ + cwcreative: 33, + cwapikey: 'api_key_33', + refgroups: 'group_33', + }).build(); + + const bid02 = new BidRequestBuilder() + .withParams({ + cwcreative: 1234, + cwapikey: 'api_key_5', + refgroups: 'group_5', + }).build(); + + const bidderRequest01 = new BidderRequestBuilder().build(); + + const requests = spec.buildRequests([bid01, bid02], bidderRequest01); + + expect(requests.data.slots.length).to.equal(2); + expect(requests.data.cwid).to.be.null; + expect(requests.data.cwid).to.be.null; + expect(requests.data.cwcreative).to.equal(33); + expect(requests.data.cwapikey).to.equal('api_key_33'); + expect(requests.data.refgroups[0]).to.equal('group_33'); }); - }); - describe('C-WIRE - buildRequests()', function () { - it('creates a valid request', function () { + it('creates a valid request - read debug params from 3 different slots', function () { + const bid01 = new BidRequestBuilder() + .withParams({ + cwcreative: 33, + }).build(); + + const bid02 = new BidRequestBuilder() + .withParams({ + cwapikey: 'api_key_5', + }).build(); + + const bid03 = new BidRequestBuilder() + .withParams({ + refgroups: 'group_5', + }).build(); + const bidderRequest01 = new BidderRequestBuilder().build(); + + const requests = spec.buildRequests([bid01, bid02, bid03], bidderRequest01); + + expect(requests.data.slots.length).to.equal(3); + expect(requests.data.cwid).to.be.null; + expect(requests.data.cwid).to.be.null; + expect(requests.data.cwcreative).to.equal(33); + expect(requests.data.cwapikey).to.equal('api_key_5'); + expect(requests.data.refgroups[0]).to.equal('group_5'); + }); + + it('creates a valid request - config is overriden by URL params', function () { + // for whatever reason stub for getWindowLocation does not work + // so this was the closest way to test for get params + const params = sandbox.stub(utils, 'getParameterByName'); + params.withArgs('cwgroups').returns('group_2'); + params.withArgs('cwcreative').returns('654321'); + + const bid01 = new BidRequestBuilder({ + mediaTypes: { + banner: { + sizes: [[1, 1]], + } + } + }).withParams({ + cwcreative: 54321, + cwapikey: 'xxx-xxx-yyy-zzz', + refgroups: 'group_1', + }).build(); + + const bidderRequest01 = new BidderRequestBuilder().build(); + + const requests = spec.buildRequests([bid01], bidderRequest01); + + expect(requests.data.slots.length).to.equal(1); + expect(requests.data.cwid).to.be.null; + expect(requests.data.cwid).to.be.null; + expect(requests.data.slots[0].sizes[0]).to.equal('1x1'); + expect(requests.data.cwcreative).to.equal(654321); + expect(requests.data.cwapikey).to.equal('xxx-xxx-yyy-zzz'); + expect(requests.data.refgroups[0]).to.equal('group_2'); + }); + + it('creates a valid request - if params are not set, null or empty array are sent to the API', function () { const bid01 = new BidRequestBuilder({ mediaTypes: { banner: { @@ -153,12 +270,18 @@ describe('C-WIRE bid adapter', () => { } } }).withParams().build(); + const bidderRequest01 = new BidderRequestBuilder().build(); const requests = spec.buildRequests([bid01], bidderRequest01); + expect(requests.data.slots.length).to.equal(1); expect(requests.data.cwid).to.be.null; + expect(requests.data.cwid).to.be.null; expect(requests.data.slots[0].sizes[0]).to.equal('1x1'); + expect(requests.data.cwcreative).to.equal(null); + expect(requests.data.cwapikey).to.equal(null); + expect(requests.data.refgroups.length).to.equal(0); }); }); diff --git a/test/spec/modules/dacIdSystem_spec.js b/test/spec/modules/dacIdSystem_spec.js new file mode 100644 index 00000000000..d78b4a69000 --- /dev/null +++ b/test/spec/modules/dacIdSystem_spec.js @@ -0,0 +1,51 @@ +import { dacIdSystemSubmodule, storage, cookieKey } from 'modules/dacIdSystem.js'; + +const DACID_DUMMY_VALUE = 'dacIdTest'; +const DACID_DUMMY_OBJ = { + dacId: DACID_DUMMY_VALUE +}; + +describe('dacId module', function () { + let getCookieStub; + + beforeEach(function (done) { + getCookieStub = sinon.stub(storage, 'getCookie'); + done(); + }); + + afterEach(function () { + getCookieStub.restore(); + }); + + const cookieTestCasesForEmpty = [ + undefined, + null, + '' + ] + + describe('getId()', function () { + it('should return the uid when it exists in cookie', function () { + getCookieStub.withArgs(cookieKey).returns(DACID_DUMMY_VALUE); + const id = dacIdSystemSubmodule.getId(); + expect(id).to.be.deep.equal({id: {dacId: DACID_DUMMY_VALUE}}); + }); + + cookieTestCasesForEmpty.forEach(testCase => it('should return the uid when it not exists in cookie', function () { + getCookieStub.withArgs(cookieKey).returns(testCase); + const id = dacIdSystemSubmodule.getId(); + expect(id).to.be.deep.equal(undefined); + })); + }); + + describe('decode()', function () { + it('should return the uid when it exists in cookie', function () { + const decoded = dacIdSystemSubmodule.decode(DACID_DUMMY_OBJ); + expect(decoded).to.be.deep.equal({dacId: {id: DACID_DUMMY_VALUE}}); + }); + + it('should return the undefined when decode id is not "string"', function () { + const decoded = dacIdSystemSubmodule.decode(1); + expect(decoded).to.equal(undefined); + }); + }); +}); diff --git a/test/spec/modules/dailyhuntBidAdapter_spec.js b/test/spec/modules/dailyhuntBidAdapter_spec.js new file mode 100644 index 00000000000..f347d6cec5b --- /dev/null +++ b/test/spec/modules/dailyhuntBidAdapter_spec.js @@ -0,0 +1,404 @@ +import { expect } from 'chai'; +import { spec } from 'modules/dailyhuntBidAdapter.js'; + +const PROD_PREBID_ENDPOINT_URL = 'https://pbs.dailyhunt.in/openrtb2/auction?partner=dailyhunt'; +const PROD_PREBID_TEST_ENDPOINT_URL = 'https://qa-pbs-van.dailyhunt.in/openrtb2/auction?partner=dailyhunt'; + +const _encodeURIComponent = function (a) { + if (!a) { return } + let b = window.encodeURIComponent(a); + b = b.replace(/'/g, '%27'); + return b; +} + +describe('DailyhuntAdapter', function () { + describe('isBidRequestValid', function () { + let bid = { + 'bidder': 'dailyhunt', + 'params': { + placement_id: 1, + publisher_id: 1, + partner_name: 'dailyhunt' + } + }; + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + let bid = Object.assign({}, bid); + delete bid.params; + bid.params = {}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + describe('buildRequests', function() { + let bidRequests = [ + { + bidder: 'dailyhunt', + params: { + placement_id: 1, + publisher_id: 1, + partner_name: 'dailyhunt', + bidfloor: 0.1, + device: { + ip: '47.9.247.217' + }, + site: { + cat: ['1', '2', '3'] + } + }, + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + adUnitCode: 'adunit-code', + sizes: [[300, 50]], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + transactionId: '04f2659e-c005-4eb1-a57c-fa93145e3843' + } + ]; + let nativeBidRequests = [ + { + bidder: 'dailyhunt', + params: { + placement_id: 1, + publisher_id: 1, + partner_name: 'dailyhunt', + }, + nativeParams: { + title: { + required: true, + len: 80 + }, + image: { + required: true, + sizes: [150, 50] + }, + }, + mediaTypes: { + native: { + title: { + required: true + }, + } + }, + adUnitCode: 'adunit-code', + sizes: [[300, 250], [300, 50]], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + transactionId: '04f2659e-c005-4eb1-a57c-fa93145e3843' + } + ]; + let videoBidRequests = [ + { + bidder: 'dailyhunt', + params: { + placement_id: 1, + publisher_id: 1, + partner_name: 'dailyhunt' + }, + nativeParams: { + video: { + context: 'instream' + } + }, + mediaTypes: { + video: { + context: 'instream' + } + }, + adUnitCode: 'adunit-code', + sizes: [[300, 250], [300, 50]], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + transactionId: '04f2659e-c005-4eb1-a57c-fa93145e3843' + } + ]; + let bidderRequest = { + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'bidderCode': 'dailyhunt', + 'bids': [ + { + ...bidRequests[0] + } + ], + 'refererInfo': { + 'referer': 'http://m.dailyhunt.in/' + } + }; + let nativeBidderRequest = { + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'bidderCode': 'dailyhunt', + 'bids': [ + { + ...nativeBidRequests[0] + } + ], + 'refererInfo': { + 'referer': 'http://m.dailyhunt.in/' + } + }; + let videoBidderRequest = { + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'bidderCode': 'dailyhunt', + 'bids': [ + { + ...videoBidRequests[0] + } + ], + 'refererInfo': { + 'referer': 'http://m.dailyhunt.in/' + } + }; + + it('sends display bid request to ENDPOINT via POST', function () { + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.url).to.equal(PROD_PREBID_ENDPOINT_URL); + expect(request.method).to.equal('POST'); + }); + + it('sends native bid request to ENDPOINT via POST', function () { + const request = spec.buildRequests(nativeBidRequests, nativeBidderRequest)[0]; + expect(request.url).to.equal(PROD_PREBID_ENDPOINT_URL); + expect(request.method).to.equal('POST'); + }); + + it('sends video bid request to ENDPOINT via POST', function () { + const request = spec.buildRequests(videoBidRequests, videoBidderRequest)[0]; + expect(request.url).to.equal(PROD_PREBID_ENDPOINT_URL); + expect(request.method).to.equal('POST'); + }); + }); + describe('interpretResponse', function () { + let bidResponses = { + id: 'da32def7-6779-403c-ada7-0b201dbc9744', + seatbid: [ + { + bid: [ + { + id: 'id1', + impid: 'banner-impid', + price: 1.4, + adm: 'adm', + adid: '66658', + crid: 'asd5ddbf014cac993.66466212', + dealid: 'asd5ddbf014cac993.66466212', + w: 300, + h: 250, + nurl: 'winUrl', + ext: { + prebid: { + type: 'banner' + } + } + }, + { + id: '5caccc1f-94a6-4230-a1f9-6186ee65da99', + impid: 'video-impid', + price: 1.4, + nurl: 'winUrl', + adm: 'adm', + adid: '980', + crid: '2394', + w: 300, + h: 250, + ext: { + prebid: { + 'type': 'video' + }, + bidder: { + cacheKey: 'cache_key', + vastUrl: 'vastUrl' + } + } + }, + { + id: '74973faf-cce7-4eff-abd0-b59b8e91ca87', + impid: 'native-impid', + price: 50, + nurl: 'winUrl', + adm: '{"native":{"link":{"url":"url","clicktrackers":[]},"assets":[{"id":1,"required":1,"img":{},"video":{},"data":{},"title":{"text":"TITLE"},"link":{}},{"id":1,"required":1,"img":{},"video":{},"data":{"type":2,"value":"Lorem Ipsum Lorem Ipsum Lorem Ipsum."},"title":{},"link":{}},{"id":1,"required":1,"img":{},"video":{},"data":{"type":12,"value":"Install Here"},"title":{},"link":{}},{"id":1,"required":1,"img":{"type":3,"url":"urk","w":990,"h":505},"video":{},"data":{},"title":{},"link":{}}],"imptrackers":[]}}', + adid: '968', + crid: '2370', + w: 300, + h: 250, + ext: { + prebid: { + type: 'native' + }, + bidder: null + } + }, + { + id: '5caccc1f-94a6-4230-a1f9-6186ee65da99', + impid: 'video-outstream-impid', + price: 1.4, + nurl: 'winUrl', + adm: 'adm', + adid: '980', + crid: '2394', + w: 300, + h: 250, + ext: { + prebid: { + 'type': 'video' + }, + bidder: { + cacheKey: 'cache_key', + vastUrl: 'vastUrl' + } + } + }, + ], + seat: 'dailyhunt' + } + ], + ext: { + responsetimemillis: { + dailyhunt: 119 + } + } + }; + + it('should get correct bid response', function () { + let expectedResponse = [ + { + requestId: '1', + cpm: 1.4, + creativeId: 'asd5ddbf014cac993.66466212', + width: 300, + height: 250, + ttl: 360, + netRevenue: true, + currency: 'USD', + ad: 'adm', + mediaType: 'banner', + winUrl: 'winUrl', + adomain: 'dailyhunt' + }, + { + requestId: '2', + cpm: 1.4, + creativeId: '2394', + width: 300, + height: 250, + ttl: 360, + netRevenue: true, + currency: 'USD', + mediaType: 'video', + winUrl: 'winUrl', + adomain: 'dailyhunt', + videoCacheKey: 'cache_key', + vastUrl: 'vastUrl', + }, + { + requestId: '3', + cpm: 1.4, + creativeId: '2370', + width: 300, + height: 250, + ttl: 360, + netRevenue: true, + currency: 'USD', + mediaType: 'native', + winUrl: 'winUrl', + adomain: 'dailyhunt', + native: { + clickUrl: 'https%3A%2F%2Fmontu1996.github.io%2F', + clickTrackers: [], + impressionTrackers: [], + javascriptTrackers: [], + title: 'TITLE', + body: 'Lorem Ipsum Lorem Ipsum Lorem Ipsum.', + cta: 'Install Here', + image: { + url: 'url', + height: 505, + width: 990 + } + } + }, + { + requestId: '4', + cpm: 1.4, + creativeId: '2394', + width: 300, + height: 250, + ttl: 360, + netRevenue: true, + currency: 'USD', + mediaType: 'video', + winUrl: 'winUrl', + adomain: 'dailyhunt', + vastXml: 'adm', + }, + ]; + let bidderRequest = { + bids: [ + { + bidId: 'banner-impid', + adUnitCode: 'code1', + requestId: '1' + }, + { + bidId: 'video-impid', + adUnitCode: 'code2', + requestId: '2', + mediaTypes: { + video: { + context: 'instream' + } + } + }, + { + bidId: 'native-impid', + adUnitCode: 'code3', + requestId: '3' + }, + { + bidId: 'video-outstream-impid', + adUnitCode: 'code4', + requestId: '4', + mediaTypes: { + video: { + context: 'outstream' + } + } + }, + ] + } + let result = spec.interpretResponse({ body: bidResponses }, bidderRequest); + result.forEach((r, i) => { + expect(Object.keys(r)).to.have.members(Object.keys(expectedResponse[i])); + }); + }); + }) + describe('onBidWon', function () { + it('should hit win url when bid won', function () { + let bid = { + requestId: '1', + cpm: 1.4, + creativeId: 'asd5ddbf014cac993.66466212', + width: 300, + height: 250, + ttl: 360, + netRevenue: true, + currency: 'USD', + ad: 'adm', + mediaType: 'banner', + winUrl: 'winUrl' + }; + expect(spec.onBidWon(bid)).to.equal(undefined); + }); + }) +}) diff --git a/test/spec/modules/dchain_spec.js b/test/spec/modules/dchain_spec.js new file mode 100644 index 00000000000..45061c539c1 --- /dev/null +++ b/test/spec/modules/dchain_spec.js @@ -0,0 +1,329 @@ +import { checkDchainSyntax, addBidResponseHook } from '../../../modules/dchain.js'; +import { config } from '../../../src/config.js'; +import { expect } from 'chai'; + +describe('dchain module', function () { + const STRICT = 'strict'; + const RELAX = 'relaxed'; + const OFF = 'off'; + + describe('checkDchainSyntax', function () { + let bid; + + beforeEach(function () { + bid = { + meta: { + dchain: { + 'ver': '1.0', + 'complete': 0, + 'ext': {}, + 'nodes': [{ + 'asi': 'domain.com', + 'bsid': '12345', + }, { + 'name': 'bidder', + 'domain': 'bidder.com', + 'ext': {} + }] + } + } + }; + }); + + it('Returns false if complete param is not 0 or 1', function () { + let dchainConfig = bid.meta.dchain; + dchainConfig.complete = 0; // integer + expect(checkDchainSyntax(bid, STRICT)).to.true; + dchainConfig.complete = 1; // integer + expect(checkDchainSyntax(bid, STRICT)).to.true; + dchainConfig.complete = '1'; // string + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.complete = 1.1; // float + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.complete = {}; // object + expect(checkDchainSyntax(bid, STRICT)).to.false; + delete dchainConfig.complete; // undefined + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.complete = true; // boolean + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.complete = []; // array + expect(checkDchainSyntax(bid, STRICT)).to.false; + }); + + it('Returns false if ver param is not a String', function () { + let dchainConfig = bid.meta.dchain; + dchainConfig.ver = 1; // integer + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.ver = '1'; // string + expect(checkDchainSyntax(bid, STRICT)).to.true; + dchainConfig.ver = 1.1; // float + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.ver = {}; // object + expect(checkDchainSyntax(bid, STRICT)).to.false; + delete dchainConfig.ver; // undefined + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.ver = true; // boolean + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.ver = []; // array + expect(checkDchainSyntax(bid, STRICT)).to.false; + }); + + it('Returns false if ext param is not an Object', function () { + let dchainConfig = bid.meta.dchain; + dchainConfig.ext = 1; // integer + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.ext = '1'; // string + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.ext = 1.1; // float + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.ext = {}; // object + expect(checkDchainSyntax(bid, STRICT)).to.true; + delete dchainConfig.ext; // undefined + expect(checkDchainSyntax(bid, STRICT)).to.true; + dchainConfig.ext = true; // boolean + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.ext = []; // array + expect(checkDchainSyntax(bid, STRICT)).to.false; + }); + + it('Returns false if nodes param is not an Array', function () { + let dchainConfig = bid.meta.dchain; + expect(checkDchainSyntax(bid, STRICT)).to.true; + dchainConfig.nodes = 1; // integer + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes = '1'; // string + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes = 1.1; // float + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes = {}; // object + expect(checkDchainSyntax(bid, STRICT)).to.false; + delete dchainConfig.nodes; // undefined + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes = true; // boolean + expect(checkDchainSyntax(bid, STRICT)).to.false; + }); + + it('Returns false if unknown field is used in main dchain', function () { + let dchainConfig = bid.meta.dchain; + dchainConfig.test = '1'; // String + expect(checkDchainSyntax(bid, STRICT)).to.false; + }); + + it('Returns false if nodes[].asi is not a String', function () { + let dchainConfig = bid.meta.dchain; + expect(checkDchainSyntax(bid, STRICT)).to.true; + dchainConfig.nodes[0].asi = 1; // Integer + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes[0].asi = 1.1; // float + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes[0].asi = {}; // object + expect(checkDchainSyntax(bid, STRICT)).to.false; + delete dchainConfig.nodes[0].asi; // undefined + expect(checkDchainSyntax(bid, STRICT)).to.true; + dchainConfig.nodes[0].asi = true; // boolean + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes[0].asi = []; // array + expect(checkDchainSyntax(bid, STRICT)).to.false; + }); + + it('Returns false if nodes[].bsid is not a String', function () { + let dchainConfig = bid.meta.dchain; + expect(checkDchainSyntax(bid, STRICT)).to.true; + dchainConfig.nodes[0].bsid = 1; // Integer + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes[0].bsid = 1.1; // float + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes[0].bsid = {}; // object + expect(checkDchainSyntax(bid, STRICT)).to.false; + delete dchainConfig.nodes[0].bsid; // undefined + expect(checkDchainSyntax(bid, STRICT)).to.true; + dchainConfig.nodes[0].bsid = true; // boolean + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes[0].bsid = []; // array + expect(checkDchainSyntax(bid, STRICT)).to.false; + }); + + it('Returns false if nodes[].rid is not a String', function () { + let dchainConfig = bid.meta.dchain; + expect(checkDchainSyntax(bid, STRICT)).to.true; + dchainConfig.nodes[0].rid = 1; // Integer + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes[0].rid = 1.1; // float + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes[0].rid = {}; // object + expect(checkDchainSyntax(bid, STRICT)).to.false; + delete dchainConfig.nodes[0].rid; // undefined + expect(checkDchainSyntax(bid, STRICT)).to.true; + dchainConfig.nodes[0].rid = true; // boolean + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes[0].rid = []; // array + expect(checkDchainSyntax(bid, STRICT)).to.false; + }); + + it('Returns false if nodes[].name is not a String', function () { + let dchainConfig = bid.meta.dchain; + expect(checkDchainSyntax(bid, STRICT)).to.true; + dchainConfig.nodes[0].name = 1; // Integer + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes[0].name = 1.1; // float + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes[0].name = {}; // object + expect(checkDchainSyntax(bid, STRICT)).to.false; + delete dchainConfig.nodes[0].name; // undefined + expect(checkDchainSyntax(bid, STRICT)).to.true; + dchainConfig.nodes[0].name = true; // boolean + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes[0].name = []; // array + expect(checkDchainSyntax(bid, STRICT)).to.false; + }); + + it('Returns false if nodes[].domain is not a String', function () { + let dchainConfig = bid.meta.dchain; + expect(checkDchainSyntax(bid, STRICT)).to.true; + dchainConfig.nodes[0].domain = 1; // Integer + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes[0].domain = 1.1; // float + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes[0].domain = {}; // object + expect(checkDchainSyntax(bid, STRICT)).to.false; + delete dchainConfig.nodes[0].domain; // undefined + expect(checkDchainSyntax(bid, STRICT)).to.true; + dchainConfig.nodes[0].domain = true; // boolean + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes[0].domain = []; // array + expect(checkDchainSyntax(bid, STRICT)).to.false; + }); + + it('Returns false if nodes[].ext is not an Object', function () { + let dchainConfig = bid.meta.dchain; + dchainConfig.nodes[0].ext = '1'; // String + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes[0].ext = 1; // Integer + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes[0].ext = 1.1; // float + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes[0].ext = {}; // object + expect(checkDchainSyntax(bid, STRICT)).to.true; + delete dchainConfig.nodes[0].ext; // undefined + expect(checkDchainSyntax(bid, STRICT)).to.true; + dchainConfig.nodes[0].ext = true; // boolean + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes[0].ext = []; // array + expect(checkDchainSyntax(bid, STRICT)).to.false; + }); + + it('Returns false if unknown field is used in nodes[]', function () { + let dchainConfig = bid.meta.dchain; + dchainConfig.nodes[0].test = '1'; // String + expect(checkDchainSyntax(bid, STRICT)).to.false; + }); + + it('Relaxed mode: returns true even for invalid config', function () { + bid.meta.dchain = { + ver: 1.1, + complete: '0', + nodes: [{ + name: 'asdf', + domain: ['domain.com'] + }] + }; + + expect(checkDchainSyntax(bid, RELAX)).to.true; + }); + }); + + describe('addBidResponseHook', function () { + let bid; + let adUnitCode = 'adUnit1'; + + beforeEach(function () { + bid = { + bidderCode: 'bidderA', + meta: { + dchain: { + 'ver': '1.0', + 'complete': 0, + 'ext': {}, + 'nodes': [{ + 'asi': 'domain.com', + 'bsid': '12345', + }, { + 'name': 'bidder', + 'domain': 'bidder.com', + 'ext': {} + }] + }, + networkName: 'myNetworkName', + networkId: 8475 + } + }; + }); + + afterEach(function () { + config.resetConfig(); + }); + + it('good strict config should append a node object to existing bid.meta.dchain object', function () { + function testCallback(adUnitCode, bid) { + expect(bid.meta.dchain).to.exist; + expect(bid.meta.dchain.nodes[1]).to.exist.and.to.deep.equal({ + 'name': 'bidder', + 'domain': 'bidder.com', + 'ext': {} + }); + expect(bid.meta.dchain.nodes[2]).to.exist.and.to.deep.equal({ asi: 'bidderA' }); + } + + config.setConfig({ dchain: { validation: STRICT } }); + addBidResponseHook(testCallback, adUnitCode, bid); + }); + + it('bad strict config should delete the bid.meta.dchain object', function () { + function testCallback(adUnitCode, bid) { + expect(bid.meta.dchain).to.not.exist; + } + + config.setConfig({ dchain: { validation: STRICT } }); + bid.meta.dchain.complete = 3; + addBidResponseHook(testCallback, adUnitCode, bid); + }); + + it('relaxed config should allow bid.meta.dchain to proceed, even with bad values', function () { + function testCallback(adUnitCode, bid) { + expect(bid.meta.dchain).to.exist; + expect(bid.meta.dchain.complete).to.exist.and.to.equal(3); + expect(bid.meta.dchain.nodes[2]).to.exist.and.to.deep.equal({ asi: 'bidderA' }); + } + + config.setConfig({ dchain: { validation: RELAX } }); + bid.meta.dchain.complete = 3; + addBidResponseHook(testCallback, adUnitCode, bid); + }); + + it('off config should allow the bid.meta.dchain to proceed', function () { + // check for missing nodes + function testCallback(adUnitCode, bid) { + expect(bid.meta.dchain).to.exist; + expect(bid.meta.dchain.complete).to.exist.and.to.equal(0); + expect(bid.meta.dchain.nodes).to.exist.and.to.deep.equal({ test: 123 }); + } + + config.setConfig({ dchain: { validation: OFF } }); + bid.meta.dchain.nodes = { test: 123 }; + addBidResponseHook(testCallback, adUnitCode, bid); + }); + + it('no bidder dchain', function () { + function testCallback(adUnitCode, bid) { + expect(bid.meta.dchain).to.exist; + expect(bid.meta.dchain.ver).to.exist.and.to.equal('1.0'); + expect(bid.meta.dchain.complete).to.exist.and.to.equal(0); + expect(bid.meta.dchain.nodes).to.exist.and.to.deep.equal([{ name: 'myNetworkName', bsid: '8475' }, { name: 'bidderA' }]); + } + + delete bid.meta.dchain; + config.setConfig({ dchain: { validation: RELAX } }); + addBidResponseHook(testCallback, adUnitCode, bid); + }); + }); +}); diff --git a/test/spec/modules/debugging_mod_spec.js b/test/spec/modules/debugging_mod_spec.js new file mode 100644 index 00000000000..79866d023e9 --- /dev/null +++ b/test/spec/modules/debugging_mod_spec.js @@ -0,0 +1,451 @@ +import {expect} from 'chai'; +import {BidInterceptor} from '../../../modules/debugging/bidInterceptor.js'; +import {bidderBidInterceptor} from '../../../modules/debugging/index.js'; +import {pbsBidInterceptor} from '../../../modules/debugging/pbsInterceptor.js'; + +describe('bid interceptor', () => { + let interceptor, mockSetTimeout; + beforeEach(() => { + mockSetTimeout = sinon.stub().callsFake((fn) => fn()); + interceptor = new BidInterceptor({setTimeout: mockSetTimeout}); + }); + + function setRules(...rules) { + interceptor.updateConfig({ + intercept: rules + }); + } + + describe('serializeConfig', () => { + Object.entries({ + regexes: /pat/, + functions: () => ({}) + }).forEach(([test, arg]) => { + it(`should filter out ${test}`, () => { + const valid = [{key1: 'value'}, {key2: 'value'}]; + const ser = interceptor.serializeConfig([...valid, {outer: {inner: arg}}]); + expect(ser).to.eql(valid); + }); + }); + }); + + describe('match()', () => { + Object.entries({ + value: {key: 'value'}, + regex: {key: /^value$/}, + 'function': (o) => o.key === 'value' + }).forEach(([test, matcher]) => { + describe(`by ${test}`, () => { + it('should work on matching top-level properties', () => { + setRules({when: matcher}); + const rule = interceptor.match({key: 'value'}); + expect(rule).to.not.eql(null); + }); + + it('should work on matching nested properties', () => { + setRules({when: {outer: {inner: matcher}}}); + const rule = interceptor.match({outer: {inner: {key: 'value'}}}); + expect(rule).to.not.eql(null); + }); + + it('should not work on non-matching inputs', () => { + setRules({when: matcher}); + expect(interceptor.match({key: 'different-value'})).to.not.be.ok; + expect(interceptor.match({differentKey: 'value'})).to.not.be.ok; + }); + }); + }); + + it('should respect rule order', () => { + setRules({when: {key: 'value'}}, {when: {}}, {when: {}}); + const rule = interceptor.match({}); + expect(rule.no).to.equal(2); + }); + + it('should pass extra arguments to property function matchers', () => { + let matchDef = { + key: sinon.stub(), + outer: {inner: {key: sinon.stub()}} + }; + const extraArgs = [{}, {}]; + setRules({when: matchDef}); + interceptor.match({key: {}, outer: {inner: {key: {}}}}, ...extraArgs); + [matchDef.key, matchDef.outer.inner.key].forEach((fn) => { + expect(fn.calledOnceWith(sinon.match.any, ...extraArgs.map(sinon.match.same))).to.be.true; + }); + }); + + it('should pass extra arguments to single-function matcher', () => { + let matchDef = sinon.stub(); + setRules({when: matchDef}); + const args = [{}, {}, {}]; + interceptor.match(...args); + expect(matchDef.calledOnceWith(...args.map(sinon.match.same))).to.be.true; + }); + }); + + describe('rule', () => { + function matchingRule({replace, options}) { + setRules({when: {}, then: replace, options: options}); + return interceptor.match({}); + } + + describe('.replace()', () => { + const REQUIRED_KEYS = [ + // https://docs.prebid.org/dev-docs/bidder-adaptor.html#bidder-adaptor-Interpreting-the-Response + 'requestId', 'cpm', 'currency', 'width', 'height', 'ttl', + 'creativeId', 'netRevenue', 'meta', 'ad' + ]; + it('should include required bid response keys by default', () => { + expect(matchingRule({}).replace({})).to.include.keys(REQUIRED_KEYS); + }); + + Object.entries({ + value: {key: 'value'}, + 'function': () => ({key: 'value'}) + }).forEach(([test, replDef]) => { + describe(`by ${test}`, () => { + it('should merge top-level properties with replace definition', () => { + const result = matchingRule({replace: replDef}).replace({}); + expect(result).to.include.keys(REQUIRED_KEYS); + expect(result.key).to.equal('value'); + }); + + it('should merge nested properties with replace definition', () => { + const result = matchingRule({replace: {outer: {inner: replDef}}}).replace({}); + expect(result).to.include.keys(REQUIRED_KEYS); + expect(result.outer.inner).to.eql({key: 'value'}); + }); + }); + }); + + it('should pass extra arguments to single function replacer', () => { + const replDef = sinon.stub(); + const args = [{}, {}, {}]; + matchingRule({replace: replDef}).replace(...args); + expect(replDef.calledOnceWith(...args.map(sinon.match.same))).to.be.true; + }); + + it('should pass extra arguments to function property replacers', () => { + const replDef = { + key: sinon.stub(), + outer: {inner: {key: sinon.stub()}} + }; + const args = [{}, {}, {}]; + matchingRule({replace: replDef}).replace(...args); + [replDef.key, replDef.outer.inner.key].forEach((repl) => { + expect(repl.calledOnceWith(...args.map(sinon.match.same))).to.be.true; + }); + }); + }); + + describe('.options', () => { + it('should include default rule options', () => { + const optDef = {someOption: 'value'}; + const ruleOptions = matchingRule({options: optDef}).options; + expect(ruleOptions).to.include(optDef); + expect(ruleOptions).to.include(interceptor.DEFAULT_RULE_OPTIONS); + }); + + it('should override defaults', () => { + const optDef = {delay: 123}; + const ruleOptions = matchingRule({options: optDef}).options; + expect(ruleOptions).to.eql(optDef); + }); + }); + }); + + describe('intercept()', () => { + let done, addBid; + + function intercept(args = {}) { + const bidRequest = {bids: args.bids || []}; + return interceptor.intercept(Object.assign({bidRequest, done, addBid}, args)); + } + + beforeEach(() => { + done = sinon.spy(); + addBid = sinon.spy(); + }); + + describe('on no match', () => { + it('should return untouched bids and bidRequest', () => { + const bids = [{}, {}]; + const bidRequest = {}; + const result = intercept({bids, bidRequest}); + expect(result.bids).to.equal(bids); + expect(result.bidRequest).to.equal(bidRequest); + }); + + it('should call done() immediately', () => { + intercept(); + expect(done.calledOnce).to.be.true; + expect(mockSetTimeout.args[0][1]).to.equal(0); + }); + + it('should not call addBid', () => { + intercept(); + expect(addBid.called).to.not.be.ok; + }); + }); + + describe('on match', () => { + let match1, match2, repl1, repl2; + const DELAY_1 = 123; + const DELAY_2 = 321; + const REQUEST = { + bids: [ + {id: 1, match: false}, + {id: 2, match: 1}, + {id: 3, match: 2} + ] + }; + + beforeEach(() => { + match1 = sinon.stub().callsFake((bid) => bid.match === 1); + match2 = sinon.stub().callsFake((bid) => bid.match === 2); + repl1 = sinon.stub().returns({replace: 1}); + repl2 = sinon.stub().returns({replace: 2}); + setRules( + {when: match1, then: repl1, options: {delay: DELAY_1}}, + {when: match2, then: repl2, options: {delay: DELAY_2}}, + ); + }); + + it('should return only non-matching bids', () => { + const {bids, bidRequest} = intercept({bidRequest: REQUEST}); + expect(bids).to.eql([REQUEST.bids[0]]); + expect(bidRequest.bids).to.eql([REQUEST.bids[0]]); + }); + + it('should call addBid for each matching bid', () => { + intercept({bidRequest: REQUEST}); + expect(addBid.callCount).to.equal(2); + expect(addBid.calledWith(sinon.match({replace: 1, isDebug: true}), REQUEST.bids[1])).to.be.true; + expect(addBid.calledWith(sinon.match({replace: 2, isDebug: true}), REQUEST.bids[2])).to.be.true; + [DELAY_1, DELAY_2].forEach((delay) => { + expect(mockSetTimeout.calledWith(sinon.match.any, delay)).to.be.true; + }); + }); + + it('should call done()', () => { + intercept({bidRequest: REQUEST}); + expect(done.calledOnce).to.be.true; + }); + + it('should pass bid and bidRequest to match and replace functions', () => { + intercept({bidRequest: REQUEST}); + Object.entries({ + 1: [match1, repl1], + 2: [match2, repl2] + }).forEach(([index, fns]) => { + fns.forEach((fn) => { + expect(fn.calledWith(REQUEST.bids[index], REQUEST)).to.be.true; + }); + }); + }); + }); + }); +}); + +describe('bidderBidInterceptor', () => { + let next, interceptBids, onCompletion, interceptResult, done, addBid; + + function interceptorArgs({spec = {}, bids = [], bidRequest = {}, ajax = {}, wrapCallback = {}, cbs = {}} = {}) { + return [next, interceptBids, spec, bids, bidRequest, ajax, wrapCallback, Object.assign({onCompletion}, cbs)]; + } + + beforeEach(() => { + next = sinon.spy(); + interceptBids = sinon.stub().callsFake((opts) => { + done = opts.done; + addBid = opts.addBid; + return interceptResult; + }); + onCompletion = sinon.spy(); + interceptResult = {bids: [], bidRequest: {}}; + }); + + it('should pass to interceptBid an addBid that triggers onBid', () => { + const onBid = sinon.spy(); + bidderBidInterceptor(...interceptorArgs({cbs: {onBid}})); + const bid = {}; + addBid(bid); + expect(onBid.calledWith(sinon.match.same(bid))).to.be.true; + }); + + describe('with no remaining bids', () => { + it('should pass a done callback that triggers onCompletion', () => { + bidderBidInterceptor(...interceptorArgs()); + expect(onCompletion.calledOnce).to.be.false; + interceptBids.args[0][0].done(); + expect(onCompletion.calledOnce).to.be.true; + }); + + it('should not call next()', () => { + bidderBidInterceptor(...interceptorArgs()); + expect(next.called).to.be.false; + }); + }); + + describe('with remaining bids', () => { + const REMAINING_BIDS = [{id: 1}, {id: 2}]; + beforeEach(() => { + interceptResult = {bids: REMAINING_BIDS, bidRequest: {bids: REMAINING_BIDS}}; + }); + + it('should call next', () => { + const callbacks = { + onResponse: {}, + onRequest: {}, + onBid: {} + }; + const args = interceptorArgs({cbs: callbacks}); + const expectedNextArgs = [ + args[2], + interceptResult.bids, + interceptResult.bidRequest, + ...args.slice(5, args.length - 1), + ].map(sinon.match.same) + .concat([sinon.match({ + onResponse: sinon.match.same(callbacks.onResponse), + onRequest: sinon.match.same(callbacks.onRequest), + onBid: sinon.match.same(callbacks.onBid) + })]); + bidderBidInterceptor(...args); + expect(next.calledOnceWith(...expectedNextArgs)).to.be.true; + }); + + it('should trigger onCompletion once both interceptBids.done and next.cbs.onCompletion are called ', () => { + bidderBidInterceptor(...interceptorArgs()); + expect(onCompletion.calledOnce).to.be.false; + next.args[0][next.args[0].length - 1].onCompletion(); + expect(onCompletion.calledOnce).to.be.false; + done(); + expect(onCompletion.calledOnce).to.be.true; + }); + }); +}); + +describe('pbsBidInterceptor', () => { + const EMPTY_INT_RES = {bids: [], bidRequest: {bids: []}}; + let next, interceptBids, s2sBidRequest, bidRequests, ajax, onResponse, onError, onBid, interceptResults, + addBids, dones, reqIdx; + + beforeEach(() => { + reqIdx = 0; + [addBids, dones] = [[], []]; + next = sinon.spy(); + ajax = sinon.spy(); + onResponse = sinon.spy(); + onError = sinon.spy(); + onBid = sinon.spy(); + interceptBids = sinon.stub().callsFake((opts) => { + addBids.push(opts.addBid); + dones.push(opts.done); + return interceptResults[reqIdx++]; + }); + s2sBidRequest = {}; + bidRequests = [{bids: []}, {bids: []}]; + interceptResults = [EMPTY_INT_RES, EMPTY_INT_RES]; + }); + + function callInterceptor() { + return pbsBidInterceptor(next, interceptBids, s2sBidRequest, bidRequests, ajax, {onResponse, onError, onBid}); + } + + it('passes addBids that trigger onBid', () => { + callInterceptor(); + bidRequests.forEach((_, i) => { + const bid = {adUnitCode: i, prop: i}; + const bidRequest = {req: i}; + addBids[i](bid, bidRequest); + expect(onBid.calledWith({adUnit: i, bid: sinon.match(bid)})); + }); + }); + + describe('on no match', () => { + it('should not call next', () => { + callInterceptor(); + expect(next.called).to.be.false; + }); + + it('should pass done callbacks that trigger a dummy onResponse once they all run', () => { + callInterceptor(); + expect(onResponse.called).to.be.false; + bidRequests.forEach((_, i) => { + dones[i](); + expect(onResponse.called).to.equal(i === bidRequests.length - 1); + }); + expect(onResponse.calledWith(true, [])).to.be.true; + }); + }); + + describe('on match', () => { + let matchingBids; + beforeEach(() => { + matchingBids = [ + [{bidId: 1, matching: true}, {bidId: 2, matching: true}], + [], + [{bidId: 3, matching: true}] + ]; + interceptResults = matchingBids.map((bids) => ({bids, bidRequest: {bids}})); + s2sBidRequest = { + ad_units: [ + {bids: [{bid_id: 1, matching: true}, {bid_id: 3, matching: true}, {bid_id: 100}, {bid_id: 101}]}, + {bids: [{bid_id: 2, matching: true}, {bid_id: 110}, {bid_id: 111}]}, + {bids: [{bid_id: 120}]} + ] + }; + bidRequests = matchingBids.map((mBids, i) => [ + {bidId: 100 + (i * 10)}, + {bidId: 101 + (i * 10)}, + ...mBids + ]); + }); + + it('should call next', () => { + callInterceptor(); + expect(next.calledOnceWith( + sinon.match.any, + sinon.match.any, + ajax, + sinon.match({ + onError, + onBid + }) + )).to.be.true; + }); + + it('should filter out intercepted bids from s2sBidRequest', () => { + callInterceptor(); + const interceptedS2SReq = next.args[0][0]; + const allMatching = interceptedS2SReq.ad_units.every((u) => u.bids.length > 0 && u.bids.every((b) => b.matching)); + expect(allMatching).to.be.true; + }); + + it('should pass bidRequests as returned by interceptBids', () => { + callInterceptor(); + const passedBidReqs = next.args[0][1]; + interceptResults + .filter((r) => r.bids.length > 0) + .forEach(({bidRequest}, i) => { + expect(passedBidReqs[i]).to.equal(bidRequest); + }); + }); + + it('should pass an onResponse that triggers original onResponse only once all intercept dones are called', () => { + callInterceptor(); + const interceptedOnResponse = next.args[0][next.args[0].length - 1].onResponse; + expect(onResponse.called).to.be.false; + const responseArgs = ['dummy', 'args']; + interceptedOnResponse(...responseArgs); + expect(onResponse.called).to.be.false; + dones.forEach((f, i) => { + f(); + expect(onResponse.called).to.equal(i === dones.length - 1); + }); + expect(onResponse.calledOnceWith(...responseArgs)).to.be.true; + }); + }); +}); diff --git a/test/spec/modules/deepintentDpesIdsystem_spec.js b/test/spec/modules/deepintentDpesIdsystem_spec.js index 7ea5553393c..4c26b118a98 100644 --- a/test/spec/modules/deepintentDpesIdsystem_spec.js +++ b/test/spec/modules/deepintentDpesIdsystem_spec.js @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import find from 'core-js-pure/features/array/find.js'; +import {find} from 'src/polyfill.js'; import { storage, deepintentDpesSubmodule } from 'modules/deepintentDpesIdSystem.js'; import { init, requestBidsHook, setSubmoduleRegistry } from 'modules/userId/index.js'; import { config } from 'src/config.js'; diff --git a/test/spec/modules/dfpAdServerVideo_spec.js b/test/spec/modules/dfpAdServerVideo_spec.js index eaffca01e06..300e2104fae 100644 --- a/test/spec/modules/dfpAdServerVideo_spec.js +++ b/test/spec/modules/dfpAdServerVideo_spec.js @@ -410,6 +410,59 @@ describe('The DFP video support module', function () { expect(customParams).to.have.property('hb_cache_id', 'def'); }); + it('should keep the url protocol, host, and pathname when using url and params', function () { + const url = parse(buildDfpVideoUrl({ + adUnit: adUnit, + bid: bid, + url: 'http://video.adserver.example/ads?sz=640x480&iu=/123/aduniturl&impl=s', + params: { + cust_params: { + hb_rand: 'random' + } + } + })); + + expect(url.protocol).to.equal('http:'); + expect(url.host).to.equal('video.adserver.example'); + expect(url.pathname).to.equal('/ads'); + }); + + it('should append to the url size param', () => { + const url = parse(buildDfpVideoUrl({ + adUnit: adUnit, + bid: bid, + url: 'http://video.adserver.example/ads?sz=360x240&iu=/123/aduniturl&impl=s', + params: { + cust_params: { + hb_rand: 'random' + } + } + })); + + const queryObject = utils.parseQS(url.query); + expect(queryObject.sz).to.equal('360x240|640x480'); + }); + + it('should append to the existing url cust params', () => { + const url = parse(buildDfpVideoUrl({ + adUnit: adUnit, + bid: bid, + url: 'http://video.adserver.example/ads?sz=360x240&iu=/123/aduniturl&impl=s&cust_params=existing_key%3Dexisting_value%26other_key%3Dother_value', + params: { + cust_params: { + hb_rand: 'random' + } + } + })); + + const queryObject = utils.parseQS(url.query); + const customParams = utils.parseQS('?' + decodeURIComponent(queryObject.cust_params)); + + expect(customParams).to.have.property('existing_key', 'existing_value'); + expect(customParams).to.have.property('other_key', 'other_value'); + expect(customParams).to.have.property('hb_rand', 'random'); + }); + describe('adpod unit tests', function () { let amStub; let amGetAdUnitsStub; diff --git a/test/spec/modules/displayioBidAdapter_spec.js b/test/spec/modules/displayioBidAdapter_spec.js new file mode 100644 index 00000000000..dfeb67fb467 --- /dev/null +++ b/test/spec/modules/displayioBidAdapter_spec.js @@ -0,0 +1,239 @@ +import { expect } from 'chai' +import {spec} from 'modules/displayioBidAdapter.js' + +describe('Displayio adapter', function () { + const BIDDER = 'displayio' + const bidRequests = [{ + bidId: 'bidId_001', + bidder: BIDDER, + adUnitCode: 'adUnit_001', + auctionId: 'auctionId_001', + bidderRequestId: 'bidderRequestId_001', + mediaTypes: { + banner: { + sizes: [[320, 480]] + }, + video: { + sizes: [[360, 640]] + }, + }, + params: { + siteId: 1, + placementId: 1, + adsSrvDomain: 'adsSrvDomain', + cdnDomain: 'cdnDomain', + } + }] + const bidderRequest = { + refererInfo: { + referer: 'testprebid.com' + } + } + + describe('isBidRequestValid', function () { + it('should return true when required params found', function() { + const validBid = spec.isBidRequestValid(bidRequests[0]) + expect(validBid).to.be.true + }) + + const bidRequestsNoParams = [{ + bidder: BIDDER, + }] + it('should not validate without params', function () { + const request = spec.isBidRequestValid(bidRequestsNoParams, bidderRequest) + expect(request).to.be.false + }) + + const noSiteId = { + bidder: BIDDER, + params: { + placementId: 1, + adsSrvDomain: 'adsSrvDomain', + cdnDomain: 'cdnDomain', + } + } + it('should not validate without siteId', function() { + const invalidBid = spec.isBidRequestValid(noSiteId) + expect(invalidBid).to.be.false + }) + + const noPlacementId = { + bidder: BIDDER, + params: { + siteId: 1, + adsSrvDomain: 'adsSrvDomain', + cdnDomain: 'cdnDomain', + } + } + it('should not validate without placementId', function() { + const invalidBid = spec.isBidRequestValid(noPlacementId) + expect(invalidBid).to.be.false + }) + + const noAdsSrvDomain = { + bidder: BIDDER, + params: { + siteId: 1, + placementId: 1, + cdnDomain: 'cdnDomain', + } + } + it('should not validate without adsSrvDomain', function() { + const invalidBid = spec.isBidRequestValid(noAdsSrvDomain) + expect(invalidBid).to.be.false + }) + + const noCdnDomain = { + bidder: BIDDER, + params: { + siteId: 1, + placementId: 1, + adsSrvDomain: 'adsSrvDomain', + } + } + it('should not validate without cdnDomain', function() { + const invalidBid = spec.isBidRequestValid(noCdnDomain) + expect(invalidBid).to.be.false + }) + }) + + describe('buildRequests', function () { + it('should build request', function() { + const request = spec.buildRequests(bidRequests, bidderRequest) + expect(request).to.not.be.empty + }) + + it('sends bid request to the endpoint via POST', function () { + const request = spec.buildRequests(bidRequests, bidderRequest) + expect(request[0].method).to.equal('POST') + }) + + it('sends all bid parameters', function () { + const request = spec.buildRequests(bidRequests, bidderRequest) + expect(request[0]).to.have.keys(['headers', 'data', 'method', 'url']) + }) + + it('should not crash when there is no media types', function () { + const bidRequestsNoMediaTypes = [{ + bidder: BIDDER, + params: { + siteId: 1, + placementId: 1, + adsSrvDomain: 'adsSrvDomain', + cdnDomain: 'cdnDomain', + } + }] + const request = spec.buildRequests(bidRequestsNoMediaTypes, bidderRequest) + expect(request[0]).to.have.keys(['headers', 'data', 'method', 'url']) + }) + }) + + describe('_getPayload', function () { + const payload = spec._getPayload(bidRequests[0], bidderRequest) + it('should not be empty', function() { + expect(payload).to.not.be.empty + }) + + it('should have userSession', function() { + expect(payload.userSession).to.be.a('string') + }) + + it('should have data object', function() { + expect(payload.data).to.be.a('object') + }) + + it('should have complianceData object', function() { + expect(payload.data.complianceData).to.be.a('object') + }) + + it('should have device object', function() { + expect(payload.data.device).to.be.a('object') + }) + + it('should have omidpn', function() { + expect(payload.data.omidpn).to.be.a('string') + }) + + it('should have integration', function() { + expect(payload.data.integration).to.be.a('string') + }) + + it('should have bidId', function() { + expect(payload.data.id).to.not.be.empty + }) + + it('should have action getPlacement', function() { + expect(payload.data.action).to.be.equal('getPlacement') + }) + + it('should have app parameter', function() { + expect(payload.data.app).to.be.a('number') + }) + + it('should have placement parameter', function() { + expect(payload.data.placement).to.be.a('number') + }) + }) + + describe('interpretResponse', function () { + const response = { + body: { + status: 'ok', + data: { + ads: [{ + ad: { + data: { + id: '001', + ecpm: 100, + w: 32, + h: 480, + markup: 'test ad' + } + }, + subtype: 'html' + }], + } + } + } + const serverRequest = { + data: { + data: { + id: 'id_001', + data: { + ref: 'testprebid.com' + } + } + } + } + + let ir = spec.interpretResponse(response, serverRequest) + + expect(ir.length).to.equal(1) + + ir = ir[0] + + it('should have requestId', function() { + expect(ir.requestId).to.be.a('string') + }) + + it('should have cpm', function() { + expect(ir.cpm).to.be.a('number') + }) + + it('should have width', function() { + expect(ir.width).to.be.a('number') + }) + + it('should have height', function() { + expect(ir.height).to.be.a('number') + }) + + it('should have creativeId', function() { + expect(ir.creativeId).to.be.a('number') + }) + + it('should have ad', function() { + expect(ir.ad).to.be.a('string') + }) + }) +}) diff --git a/test/spec/modules/docereeBidAdapter_spec.js b/test/spec/modules/docereeBidAdapter_spec.js index efff2efa319..dadbb56b0c0 100644 --- a/test/spec/modules/docereeBidAdapter_spec.js +++ b/test/spec/modules/docereeBidAdapter_spec.js @@ -31,6 +31,8 @@ describe('BidlabBidAdapter', function () { bidder: 'doceree', params: { placementId: 'DOC_7jm9j5eqkl0xvc5w', + gdpr: '1', + gdprConsent: 'CPQfU1jPQfU1jG0AAAENAwCAAAAAAAAAAAAAAAAAAAAA.IGLtV_T9fb2vj-_Z99_tkeYwf95y3p-wzhheMs-8NyZeH_B4Wv2MyvBX4JiQKGRgksjLBAQdtHGlcTQgBwIlViTLMYk2MjzNKJrJEilsbO2dYGD9Pn8HT3ZCY70-vv__7v3ff_3g' } }; @@ -44,6 +46,12 @@ describe('BidlabBidAdapter', function () { }); }); + describe('isGdprConsentPresent', function () { + it('Should return true if gdpr consent is present', function () { + expect(spec.isGdprConsentPresent(bid)).to.be.true; + }); + }); + describe('buildRequests', function () { let serverRequest = spec.buildRequests([bid]); serverRequest = serverRequest[0] @@ -56,7 +64,7 @@ describe('BidlabBidAdapter', function () { expect(serverRequest.method).to.equal('GET'); }); it('Returns valid URL', function () { - expect(serverRequest.url).to.equal('https://bidder.doceree.com/v1/adrequest?id=DOC_7jm9j5eqkl0xvc5w&pubRequestedURL=undefined&loggedInUser=JTdCJTIyZ2VuZGVyJTIyJTNBJTIyJTIyJTJDJTIyZW1haWwlMjIlM0ElMjIlMjIlMkMlMjJoYXNoZWRFbWFpbCUyMiUzQSUyMiUyMiUyQyUyMmZpcnN0TmFtZSUyMiUzQSUyMiUyMiUyQyUyMmxhc3ROYW1lJTIyJTNBJTIyJTIyJTJDJTIybnBpJTIyJTNBJTIyJTIyJTJDJTIyaGFzaGVkTlBJJTIyJTNBJTIyJTIyJTJDJTIyY2l0eSUyMiUzQSUyMiUyMiUyQyUyMnppcENvZGUlMjIlM0ElMjIlMjIlMkMlMjJzcGVjaWFsaXphdGlvbiUyMiUzQSUyMiUyMiU3RA%3D%3D&prebidjs=true&requestId=testing&'); + expect(serverRequest.url).to.equal('https://bidder.doceree.com/v1/adrequest?id=DOC_7jm9j5eqkl0xvc5w&pubRequestedURL=undefined&loggedInUser=JTdCJTIyZ2VuZGVyJTIyJTNBJTIyJTIyJTJDJTIyZW1haWwlMjIlM0ElMjIlMjIlMkMlMjJoYXNoZWRFbWFpbCUyMiUzQSUyMiUyMiUyQyUyMmZpcnN0TmFtZSUyMiUzQSUyMiUyMiUyQyUyMmxhc3ROYW1lJTIyJTNBJTIyJTIyJTJDJTIybnBpJTIyJTNBJTIyJTIyJTJDJTIyaGFzaGVkTlBJJTIyJTNBJTIyJTIyJTJDJTIyY2l0eSUyMiUzQSUyMiUyMiUyQyUyMnppcENvZGUlMjIlM0ElMjIlMjIlMkMlMjJzcGVjaWFsaXphdGlvbiUyMiUzQSUyMiUyMiU3RA%3D%3D&prebidjs=true&requestId=testing&gdpr=1&gdpr_consent=CPQfU1jPQfU1jG0AAAENAwCAAAAAAAAAAAAAAAAAAAAA.IGLtV_T9fb2vj-_Z99_tkeYwf95y3p-wzhheMs-8NyZeH_B4Wv2MyvBX4JiQKGRgksjLBAQdtHGlcTQgBwIlViTLMYk2MjzNKJrJEilsbO2dYGD9Pn8HT3ZCY70-vv__7v3ff_3g&'); }); }); describe('interpretResponse', function () { diff --git a/test/spec/modules/dspxBidAdapter_spec.js b/test/spec/modules/dspxBidAdapter_spec.js index 09f40895ec9..2869385d7e7 100644 --- a/test/spec/modules/dspxBidAdapter_spec.js +++ b/test/spec/modules/dspxBidAdapter_spec.js @@ -62,6 +62,7 @@ describe('dspxAdapter', function () { 'bidId': '30b31c1838de1e1', 'bidderRequestId': '22edbae2733bf61', 'auctionId': '1d1a030790a475', + 'adUnitCode': 'testDiv1', 'userId': { 'netId': '123', 'uid2': '456' @@ -98,7 +99,8 @@ describe('dspxAdapter', function () { ], 'bidId': '30b31c1838de1e3', 'bidderRequestId': '22edbae2733bf69', - 'auctionId': '1d1a030790a477' + 'auctionId': '1d1a030790a477', + 'adUnitCode': 'testDiv2' }, { 'bidder': 'dspx', @@ -120,13 +122,15 @@ describe('dspxAdapter', function () { 'bidId': '30b31c1838de1e4', 'bidderRequestId': '22edbae2733bf67', - 'auctionId': '1d1a030790a478' + 'auctionId': '1d1a030790a478', + 'adUnitCode': 'testDiv3' }, { 'bidder': 'dspx', 'params': { 'placement': '101', - 'devMode': true + 'devMode': true, + 'vastFormat': 'vast4' }, 'mediaTypes': { 'video': { @@ -136,7 +140,8 @@ describe('dspxAdapter', function () { }, 'bidId': '30b31c1838de1e41', 'bidderRequestId': '22edbae2733bf67', - 'auctionId': '1d1a030790a478' + 'auctionId': '1d1a030790a478', + 'adUnitCode': 'testDiv4' } ]; @@ -157,16 +162,16 @@ describe('dspxAdapter', function () { it('sends bid request to our endpoint via GET', function () { expect(request1.method).to.equal('GET'); expect(request1.url).to.equal(ENDPOINT_URL); - let data = request1.data.replace(/rnd=\d+\&/g, '').replace(/ref=.*\&bid/g, 'bid'); - expect(data).to.equal('_f=html&alternative=prebid_js&inventory_item_id=6682&srw=300&srh=250&idt=100&bid_id=30b31c1838de1e1&pfilter%5Bfloorprice%5D=1000000&pfilter%5Bprivate_auction%5D=0&pfilter%5Bgeo%5D%5Bcountry%5D=DE&pfilter%5Bgdpr_consent%5D=BOJ%2FP2HOJ%2FP2HABABMAAAAAZ%2BA%3D%3D&pfilter%5Bgdpr%5D=true&bcat=IAB2%2CIAB4&dvt=desktop&did_netid=123&did_uid2=456'); + let data = request1.data.replace(/rnd=\d+\&/g, '').replace(/ref=.*\&bid/g, 'bid').replace(/pbver=.*?&/g, 'pbver=test&'); + expect(data).to.equal('_f=auto&alternative=prebid_js&inventory_item_id=6682&srw=300&srh=250&idt=100&bid_id=30b31c1838de1e1&pbver=test&pfilter%5Bfloorprice%5D=1000000&pfilter%5Bprivate_auction%5D=0&pfilter%5Bgeo%5D%5Bcountry%5D=DE&pfilter%5Bgdpr_consent%5D=BOJ%2FP2HOJ%2FP2HABABMAAAAAZ%2BA%3D%3D&pfilter%5Bgdpr%5D=true&bcat=IAB2%2CIAB4&dvt=desktop&did_netid=123&did_uid2=456&auctionId=1d1a030790a475&pbcode=testDiv1&media_types%5Bbanner%5D=300x250'); }); var request2 = spec.buildRequests([bidRequests[1]], bidderRequest)[0]; it('sends bid request to our DEV endpoint via GET', function () { expect(request2.method).to.equal('GET'); expect(request2.url).to.equal(ENDPOINT_URL_DEV); - let data = request2.data.replace(/rnd=\d+\&/g, '').replace(/ref=.*\&bid/g, 'bid'); - expect(data).to.equal('_f=html&alternative=prebid_js&inventory_item_id=101&srw=300&srh=250&idt=100&bid_id=30b31c1838de1e2&pfilter%5Bgdpr_consent%5D=BOJ%2FP2HOJ%2FP2HABABMAAAAAZ%2BA%3D%3D&pfilter%5Bgdpr%5D=true&prebidDevMode=1'); + let data = request2.data.replace(/rnd=\d+\&/g, '').replace(/ref=.*\&bid/g, 'bid').replace(/pbver=.*?&/g, 'pbver=test&'); + expect(data).to.equal('_f=auto&alternative=prebid_js&inventory_item_id=101&srw=300&srh=250&idt=100&bid_id=30b31c1838de1e2&pbver=test&pfilter%5Bgdpr_consent%5D=BOJ%2FP2HOJ%2FP2HABABMAAAAAZ%2BA%3D%3D&pfilter%5Bgdpr%5D=true&prebidDevMode=1&auctionId=1d1a030790a476&media_types%5Bbanner%5D=300x250'); }); // Without gdprConsent @@ -179,23 +184,23 @@ describe('dspxAdapter', function () { it('sends bid request without gdprConsent to our endpoint via GET', function () { expect(request3.method).to.equal('GET'); expect(request3.url).to.equal(ENDPOINT_URL); - let data = request3.data.replace(/rnd=\d+\&/g, '').replace(/ref=.*\&bid/g, 'bid'); - expect(data).to.equal('_f=html&alternative=prebid_js&inventory_item_id=6682&srw=300&srh=250&idt=100&bid_id=30b31c1838de1e3&pfilter%5Bfloorprice%5D=1000000&pfilter%5Bprivate_auction%5D=0&pfilter%5Bgeo%5D%5Bcountry%5D=DE&bcat=IAB2%2CIAB4&dvt=desktop'); + let data = request3.data.replace(/rnd=\d+\&/g, '').replace(/ref=.*\&bid/g, 'bid').replace(/pbver=.*?&/g, 'pbver=test&'); + expect(data).to.equal('_f=auto&alternative=prebid_js&inventory_item_id=6682&srw=300&srh=250&idt=100&bid_id=30b31c1838de1e3&pbver=test&pfilter%5Bfloorprice%5D=1000000&pfilter%5Bprivate_auction%5D=0&pfilter%5Bgeo%5D%5Bcountry%5D=DE&bcat=IAB2%2CIAB4&dvt=desktop&auctionId=1d1a030790a477&pbcode=testDiv2&media_types%5Bbanner%5D=300x250'); }); var request4 = spec.buildRequests([bidRequests[3]], bidderRequestWithoutGdpr)[0]; it('sends bid request without gdprConsent to our DEV endpoint via GET', function () { expect(request4.method).to.equal('GET'); expect(request4.url).to.equal(ENDPOINT_URL_DEV); - let data = request4.data.replace(/rnd=\d+\&/g, '').replace(/ref=.*\&bid/g, 'bid'); - expect(data).to.equal('_f=html&alternative=prebid_js&inventory_item_id=101&srw=300&srh=250&idt=100&bid_id=30b31c1838de1e4&prebidDevMode=1'); + let data = request4.data.replace(/rnd=\d+\&/g, '').replace(/ref=.*\&bid/g, 'bid').replace(/pbver=.*?&/g, 'pbver=test&'); + expect(data).to.equal('_f=auto&alternative=prebid_js&inventory_item_id=101&srw=300&srh=250&idt=100&bid_id=30b31c1838de1e4&pbver=test&prebidDevMode=1&auctionId=1d1a030790a478&pbcode=testDiv3&media_types%5Bvideo%5D=640x480&media_types%5Bbanner%5D=300x250'); }); var request5 = spec.buildRequests([bidRequests[4]], bidderRequestWithoutGdpr)[0]; - it('sends bid video request to our rads endpoint via GET', function () { + it('sends bid video request to our endpoint via GET', function () { expect(request5.method).to.equal('GET'); - let data = request5.data.replace(/rnd=\d+\&/g, '').replace(/ref=.*\&bid/g, 'bid'); - expect(data).to.equal('_f=vast2&alternative=prebid_js&inventory_item_id=101&srw=640&srh=480&idt=100&bid_id=30b31c1838de1e41&prebidDevMode=1'); + let data = request5.data.replace(/rnd=\d+\&/g, '').replace(/ref=.*\&bid/g, 'bid').replace(/pbver=.*?&/g, 'pbver=test&'); + expect(data).to.equal('_f=auto&alternative=prebid_js&inventory_item_id=101&srw=640&srh=480&idt=100&bid_id=30b31c1838de1e41&pbver=test&vf=vast4&prebidDevMode=1&auctionId=1d1a030790a478&pbcode=testDiv4&media_types%5Bvideo%5D=640x480'); }); }); diff --git a/test/spec/modules/e_volutionBidAdapter_spec.js b/test/spec/modules/e_volutionBidAdapter_spec.js new file mode 100644 index 00000000000..1f60edda0ef --- /dev/null +++ b/test/spec/modules/e_volutionBidAdapter_spec.js @@ -0,0 +1,242 @@ +import {expect} from 'chai'; +import {spec} from '../../../modules/e_volutionBidAdapter.js'; + +describe('EvolutionTechBidAdapter', function () { + let bid = { + bidId: '23fhj33i987f', + bidder: 'e_volution', + params: { + placementId: 0 + }, + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + } + }; + + describe('isBidRequestValid', function () { + it('Should return true if there are bidId, params and placementId parameters present', function () { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + it('Should return false if at least one of parameters is not present', function () { + delete bid.params.placementId; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests([bid]); + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + it('Returns valid URL', function () { + expect(serverRequest.url).to.equal('https://service.e-volution.ai/?c=o&m=multi'); + }); + it('Returns valid data if array of bids is valid', function () { + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', 'language', 'secure', 'host', 'page', 'placements'); + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.language).to.be.a('string'); + expect(data.secure).to.be.within(0, 1); + expect(data.host).to.be.a('string'); + expect(data.page).to.be.a('string'); + let placement = data['placements'][0]; + expect(placement).to.have.keys('placementId', 'bidId', 'traffic', 'sizes', 'bidfloor'); + expect(placement.placementId).to.equal(0); + expect(placement.bidId).to.equal('23fhj33i987f'); + expect(placement.traffic).to.equal('banner'); + }); + it('Returns empty data if no valid requests are passed', function () { + serverRequest = spec.buildRequests([]); + let data = serverRequest.data; + expect(data.placements).to.be.an('array').that.is.empty; + }); + }); + describe('interpretResponse', function () { + it('Should interpret banner response', function () { + const banner = { + body: [{ + mediaType: 'banner', + width: 300, + height: 250, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: {} + }] + }; + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let dataItem = bannerResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta'); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.width).to.equal(300); + expect(dataItem.height).to.equal(250); + expect(dataItem.ad).to.equal('Test'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + }); + it('Should interpret video response', function () { + const video = { + body: [{ + vastUrl: 'test.com', + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: {} + }] + }; + let videoResponses = spec.interpretResponse(video); + expect(videoResponses).to.be.an('array').that.is.not.empty; + + let dataItem = videoResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'vastUrl', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta'); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.5); + expect(dataItem.vastUrl).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + }); + it('Should interpret native response', function () { + const native = { + body: [{ + mediaType: 'native', + native: { + clickUrl: 'test.com', + title: 'Test', + image: 'test.com', + impressionTrackers: ['test.com'], + }, + ttl: 120, + cpm: 0.4, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + meta: {} + }] + }; + let nativeResponses = spec.interpretResponse(native); + expect(nativeResponses).to.be.an('array').that.is.not.empty; + + let dataItem = nativeResponses[0]; + expect(dataItem).to.have.keys('requestId', 'cpm', 'ttl', 'creativeId', 'netRevenue', 'currency', 'mediaType', 'native', 'meta'); + expect(dataItem.native).to.have.keys('clickUrl', 'impressionTrackers', 'title', 'image') + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.native.clickUrl).to.equal('test.com'); + expect(dataItem.native.title).to.equal('Test'); + expect(dataItem.native.image).to.equal('test.com'); + expect(dataItem.native.impressionTrackers).to.be.an('array').that.is.not.empty; + expect(dataItem.native.impressionTrackers[0]).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + }); + it('Should return an empty array if invalid banner response is passed', function () { + const invBanner = { + body: [{ + width: 300, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + + let serverResponses = spec.interpretResponse(invBanner); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid video response is passed', function () { + const invVideo = { + body: [{ + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invVideo); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid native response is passed', function () { + const invNative = { + body: [{ + mediaType: 'native', + clickUrl: 'test.com', + title: 'Test', + impressionTrackers: ['test.com'], + ttl: 120, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }] + }; + let serverResponses = spec.interpretResponse(invNative); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid response is passed', function () { + const invalid = { + body: [{ + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invalid); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); + describe('getUserSyncs', function () { + let userSync = spec.getUserSyncs(); + it('Returns valid URL and type', function () { + if (spec.noSync) { + expect(userSync).to.be.equal(false); + } else { + expect(userSync).to.be.an('array').with.lengthOf(1); + expect(userSync[0].type).to.exist; + expect(userSync[0].url).to.exist; + expect(userSync[0].type).to.be.equal('image'); + expect(userSync[0].url).to.be.equal('https://service.e-volution.ai/?c=o&m=sync'); + } + }); + }); +}); diff --git a/test/spec/modules/eids_spec.js b/test/spec/modules/eids_spec.js index 1277486a154..9edda3c9e95 100644 --- a/test/spec/modules/eids_spec.js +++ b/test/spec/modules/eids_spec.js @@ -240,9 +240,9 @@ describe('eids array generation for known sub-modules', function() { }); }); - it('haloId', function() { + it('hadronId', function() { const userId = { - haloId: 'some-random-id-value' + hadronId: 'some-random-id-value' }; const newEids = createEidsArray(userId); expect(newEids.length).to.equal(1); @@ -348,6 +348,21 @@ describe('eids array generation for known sub-modules', function() { }] }); }); + + it('qid', function() { + const userId = { + qid: 'some-random-id-value' + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'adquery.io', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }] + }); + }); }); describe('Negative case', function() { it('eids array generation for UN-known sub-module', function() { diff --git a/test/spec/modules/emx_digitalBidAdapter_spec.js b/test/spec/modules/emx_digitalBidAdapter_spec.js index 5831a8506c1..043a8a3709e 100644 --- a/test/spec/modules/emx_digitalBidAdapter_spec.js +++ b/test/spec/modules/emx_digitalBidAdapter_spec.js @@ -432,6 +432,16 @@ describe('emx_digital Adapter', function () { }); }); + it('should add gpid to request if present', () => { + const gpid = '/12345/my-gpt-tag-0'; + let bid = utils.deepClone(bidderRequest.bids[0]); + bid.ortb2Imp = { ext: { data: { adserver: { adslot: gpid } } } }; + bid.ortb2Imp = { ext: { data: { pbadslot: gpid } } }; + let requestWithGPID = spec.buildRequests([bid], bidderRequest); + requestWithGPID = JSON.parse(requestWithGPID.data); + expect(requestWithGPID.imp[0].ext.gpid).to.exist.and.equal(gpid); + }); + it('should add UID 2.0 to request', () => { const uid2 = { id: '456' }; const bidRequestWithUID = utils.deepClone(bidderRequest); diff --git a/test/spec/modules/engageyaBidAdapter_spec.js b/test/spec/modules/engageyaBidAdapter_spec.js index 7f07e4b9e4a..283f0148402 100644 --- a/test/spec/modules/engageyaBidAdapter_spec.js +++ b/test/spec/modules/engageyaBidAdapter_spec.js @@ -45,6 +45,108 @@ describe('Engageya adapter', function () { ] }) + describe('isValidSize', function () { + const bid = { + bidder: 'engageya', + params: { + widgetId: 85610, + websiteId: 91140, + pageUrl: '[PAGE_URL]' + } + }; + it('Exact match, 236X202', function () { + bid.sizes = [[236, 202]]; + const isValid = spec.isBidRequestValid(bid); + expect(isValid).to.be.true; + }); + it('Exact match, 300X200', function () { + bid.sizes = [[300, 200]]; + const isValid = spec.isBidRequestValid(bid); + expect(isValid).to.be.true; + }); + it('Exact match, 600X500', function () { + bid.sizes = [[600, 500]]; + const isValid = spec.isBidRequestValid(bid); + expect(isValid).to.be.true; + }); + + it('Ratio max limit, 236X212', function () { + bid.sizes = [[236, 212]]; + const isValid = spec.isBidRequestValid(bid); + expect(isValid).to.be.true; + }); + it('Ratio max limit, 300X209', function () { + bid.sizes = [[300, 209]]; + const isValid = spec.isBidRequestValid(bid); + expect(isValid).to.be.true; + }); + it('Ratio max limit, 600X524', function () { + bid.sizes = [[600, 524]]; + const isValid = spec.isBidRequestValid(bid); + expect(isValid).to.be.true; + }); + + it('Ratio & width max limit, 248X222', function () { + bid.sizes = [[248, 222]]; + const isValid = spec.isBidRequestValid(bid); + expect(isValid).to.be.true; + }); + it('Ratio & width max limit, 315X220', function () { + bid.sizes = [[315, 220]]; + const isValid = spec.isBidRequestValid(bid); + expect(isValid).to.be.true; + }); + it('Ratio & width max limit, 631X551', function () { + bid.sizes = [[631, 551]]; + const isValid = spec.isBidRequestValid(bid); + expect(isValid).to.be.true; + }); + + it('Width too big, 320X285', function () { + bid.sizes = [[320, 285]]; + const isValid = spec.isBidRequestValid(bid); + expect(isValid).to.be.false; + }); + it('Width too big, 316X220', function () { + bid.sizes = [[316, 220]]; + const isValid = spec.isBidRequestValid(bid); + expect(isValid).to.be.false; + }); + it('Width too big, 632X551', function () { + bid.sizes = [[632, 551]]; + const isValid = spec.isBidRequestValid(bid); + expect(isValid).to.be.false; + }); + + it('Ratio too big, 600X525', function () { + bid.sizes = [[600, 525]]; + const isValid = spec.isBidRequestValid(bid); + expect(isValid).to.be.false; + }); + + it('Ratio min limit, 236X192', function () { + bid.sizes = [[236, 192]]; + const isValid = spec.isBidRequestValid(bid); + expect(isValid).to.be.true; + }); + it('Ratio min limit, 300X190', function () { + bid.sizes = [[300, 190]]; + const isValid = spec.isBidRequestValid(bid); + expect(isValid).to.be.true; + }); + it('Ratio min limit, 600X475', function () { + bid.sizes = [[600, 475]]; + const isValid = spec.isBidRequestValid(bid); + expect(isValid).to.be.true; + }); + + it('Ratio too small, 600X474', function () { + bid.sizes = [[600, 474]]; + const isValid = spec.isBidRequestValid(bid); + expect(isValid).to.be.false; + }); + }) + describe('isBidRequestValid', function () { it('Valid bid case', function () { let validBid = { diff --git a/test/spec/modules/eplanningAnalyticsAdapter_spec.js b/test/spec/modules/eplanningAnalyticsAdapter_spec.js index 255d116a0ff..872518f2f27 100644 --- a/test/spec/modules/eplanningAnalyticsAdapter_spec.js +++ b/test/spec/modules/eplanningAnalyticsAdapter_spec.js @@ -1,5 +1,5 @@ import eplAnalyticsAdapter from 'modules/eplanningAnalyticsAdapter.js'; -import includes from 'core-js-pure/features/array/includes.js'; +import {includes} from 'src/polyfill.js'; import { expect } from 'chai'; import { parseUrl } from 'src/utils.js'; import { server } from 'test/mocks/xhr.js'; diff --git a/test/spec/modules/feedadBidAdapter_spec.js b/test/spec/modules/feedadBidAdapter_spec.js index 6b75af0d55d..2739654eb5d 100644 --- a/test/spec/modules/feedadBidAdapter_spec.js +++ b/test/spec/modules/feedadBidAdapter_spec.js @@ -171,6 +171,21 @@ describe('FeedAdAdapter', function () { expect(result.data.bids).to.be.lengthOf(1); expect(result.data.bids[0]).to.deep.equal(bid); }); + it('should pass through additional bid parameters', function () { + let bid = { + code: 'feedad', + mediaTypes: { + banner: { + sizes: [[320, 250]] + } + }, + params: {clientToken: 'clientToken', placementId: 'placement-id', another: 'parameter', more: 'parameters'} + }; + let result = spec.buildRequests([bid], bidderRequest); + expect(result.data.bids).to.be.lengthOf(1); + expect(result.data.bids[0].params.another).to.equal('parameter'); + expect(result.data.bids[0].params.more).to.equal('parameters'); + }); it('should detect empty media types', function () { let bid = { code: 'feedad', diff --git a/test/spec/modules/fintezaAnalyticsAdapter_spec.js b/test/spec/modules/fintezaAnalyticsAdapter_spec.js index 54d1a7e7976..407ceb305a2 100644 --- a/test/spec/modules/fintezaAnalyticsAdapter_spec.js +++ b/test/spec/modules/fintezaAnalyticsAdapter_spec.js @@ -1,5 +1,5 @@ import fntzAnalyticsAdapter from 'modules/fintezaAnalyticsAdapter.js'; -import includes from 'core-js-pure/features/array/includes.js'; +import {includes} from 'src/polyfill.js'; import { expect } from 'chai'; import { parseUrl } from 'src/utils.js'; import { server } from 'test/mocks/xhr.js'; diff --git a/test/spec/modules/fluctBidAdapter_spec.js b/test/spec/modules/fluctBidAdapter_spec.js index 70666532442..8ef99727ce7 100644 --- a/test/spec/modules/fluctBidAdapter_spec.js +++ b/test/spec/modules/fluctBidAdapter_spec.js @@ -83,6 +83,98 @@ describe('fluctAdapter', function () { const request = spec.buildRequests(bidRequests, bidderRequest)[0]; expect(request.url).to.equal('https://hb.adingo.jp/prebid?dfpUnitCode=%2F100000%2Funit_code&tagId=10000%3A100000001&groupId=1000000002'); }); + + it('includes data.user.eids = [] by default', function () { + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.data.user.eids).to.eql([]); + }); + + it('includes no data.params.kv by default', function () { + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.data.params.kv).to.eql(undefined); + }); + + it('includes filtered user.eids if any exists', function () { + const bidRequests2 = bidRequests.map( + (bidReq) => Object.assign(bidReq, { + userIdAsEids: [ + { + source: 'foobar.com', + uids: [ + { id: 'foobar-id' } + ], + }, + { + source: 'adserver.org', + uids: [ + { id: 'tdid' } + ], + }, + { + source: 'criteo.com', + uids: [ + { id: 'criteo-id' } + ], + }, + { + source: 'intimatemerger.com', + uids: [ + { id: 'imuid' } + ], + }, + { + source: 'liveramp.com', + uids: [ + { id: 'idl-env' } + ], + }, + ], + }) + ); + const request = spec.buildRequests(bidRequests2, bidderRequest)[0]; + expect(request.data.user.eids).to.eql([ + { + source: 'adserver.org', + uids: [ + { id: 'tdid' } + ], + }, + { + source: 'criteo.com', + uids: [ + { id: 'criteo-id' } + ], + }, + { + source: 'intimatemerger.com', + uids: [ + { id: 'imuid' } + ], + }, + { + source: 'liveramp.com', + uids: [ + { id: 'idl-env' } + ], + }, + ]); + }); + + it('includes data.params.kv if any exists', function () { + const bidRequests2 = bidRequests.map( + (bidReq) => Object.assign(bidReq, { + params: { + kv: { + imsids: ['imsid1', 'imsid2'] + } + } + }) + ); + const request = spec.buildRequests(bidRequests2, bidderRequest)[0]; + expect(request.data.params.kv).to.eql({ + imsids: ['imsid1', 'imsid2'] + }); + }); }); describe('interpretResponse', function() { diff --git a/test/spec/modules/freewheel-sspBidAdapter_spec.js b/test/spec/modules/freewheel-sspBidAdapter_spec.js index a5b4bd2a03f..81f4ecec074 100644 --- a/test/spec/modules/freewheel-sspBidAdapter_spec.js +++ b/test/spec/modules/freewheel-sspBidAdapter_spec.js @@ -98,6 +98,19 @@ describe('freewheelSSP BidAdapter Test', () => { 'bidId': '30b31c1838de1e', 'bidderRequestId': '22edbae2733bf6', 'auctionId': '1d1a030790a475', + 'schain': { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'example.com', + 'sid': '0', + 'hp': 1, + 'rid': 'bidrequestid', + 'domain': 'example.com' + } + ] + } } ]; @@ -112,6 +125,12 @@ describe('freewheelSSP BidAdapter Test', () => { expect(payload.playerSize).to.equal('300x600'); }); + it('should return a properly formatted request with schain defined', function () { + const request = spec.buildRequests(bidRequests); + const payload = request[0].data; + expect(payload.schain).to.deep.equal(bidRequests[0].schain) + }); + it('sends bid request to ENDPOINT via GET', () => { const request = spec.buildRequests(bidRequests); expect(request[0].url).to.contain(ENDPOINT); diff --git a/test/spec/modules/ftrackIdSystem_spec.js b/test/spec/modules/ftrackIdSystem_spec.js new file mode 100644 index 00000000000..69d66d75bb1 --- /dev/null +++ b/test/spec/modules/ftrackIdSystem_spec.js @@ -0,0 +1,238 @@ +import { ftrackIdSubmodule } from 'modules/ftrackIdSystem.js'; +import * as utils from 'src/utils.js'; +import { uspDataHandler } from 'src/adapterManager.js'; +let expect = require('chai').expect; + +let server; + +let configMock = { + name: 'ftrack', + params: { + url: 'https://d9.flashtalking.com/d9core' + }, + storage: { + name: 'ftrackId', + type: 'html5', + expires: 90, + refreshInSeconds: 8 * 3600 + }, + debug: true +}; + +let consentDataMock = { + gdprApplies: 0, + consentString: '' +}; + +describe('FTRACK ID System', () => { + describe(`Global Module Rules`, () => { + it(`should not use the "PREBID_GLOBAL" variable nor otherwise obtain a pointer to the global PBJS object`, () => { + expect((/PREBID_GLOBAL/gi).test(JSON.stringify(ftrackIdSubmodule))).to.not.be.ok; + }); + }); + + describe('ftrackIdSubmodule.isConfigOk():', () => { + let logWarnStub; + let logErrorStub; + + beforeEach(() => { + logWarnStub = sinon.stub(utils, 'logWarn'); + logErrorStub = sinon.stub(utils, 'logError'); + }); + + afterEach(() => { + logWarnStub.restore(); + logErrorStub.restore(); + }); + + it(`should be rejected if 'config.storage' property is missing`, () => { + let configMock1 = JSON.parse(JSON.stringify(configMock)); + delete configMock1.storage; + delete configMock1.params; + + ftrackIdSubmodule.isConfigOk(configMock1); + expect(logErrorStub.args[0][0]).to.equal(`FTRACK - config.storage required to be set.`); + }); + + it(`should be rejected if 'config.storage.name' property is missing`, () => { + let configMock1 = JSON.parse(JSON.stringify(configMock)); + delete configMock1.storage.name; + + ftrackIdSubmodule.isConfigOk(configMock1); + expect(logErrorStub.args[0][0]).to.equal(`FTRACK - config.storage required to be set.`); + }); + + it(`should be rejected if 'config.storage.name' is not 'ftrackId'`, () => { + let configMock1 = JSON.parse(JSON.stringify(configMock)); + configMock1.storage.name = 'not-ftrack'; + + ftrackIdSubmodule.isConfigOk(configMock1); + expect(logWarnStub.args[0][0]).to.equal(`FTRACK - config.storage.name recommended to be "ftrackId".`); + }); + + it(`should be rejected if 'congig.storage.type' property is missing`, () => { + let configMock1 = JSON.parse(JSON.stringify(configMock)); + delete configMock1.storage.type; + + ftrackIdSubmodule.isConfigOk(configMock1); + expect(logErrorStub.args[0][0]).to.equal(`FTRACK - config.storage required to be set.`); + }); + + it(`should be rejected if 'config.storage.type' is not 'html5'`, () => { + let configMock1 = JSON.parse(JSON.stringify(configMock)); + configMock1.storage.type = 'not-html5'; + + ftrackIdSubmodule.isConfigOk(configMock1); + expect(logWarnStub.args[0][0]).to.equal(`FTRACK - config.storage.type recommended to be "html5".`); + }); + + it(`should be rejected if 'config.params.url' does not exist`, () => { + let configMock1 = JSON.parse(JSON.stringify(configMock)); + delete configMock1.params.url; + + ftrackIdSubmodule.isConfigOk(configMock1); + expect(logWarnStub.args[0][0]).to.equal(`FTRACK - config.params.url is required for ftrack to run. Url should be "https://d9.flashtalking.com/d9core".`); + }); + + it(`should be rejected if 'storage.param.url' does not exist or is not 'https://d9.flashtalking.com/d9core'`, () => { + let configMock1 = JSON.parse(JSON.stringify(configMock)); + configMock1.params.url = 'https://d9.NOT.flashtalking.com/d9core'; + + ftrackIdSubmodule.isConfigOk(configMock1); + expect(logWarnStub.args[0][0]).to.equal(`FTRACK - config.params.url is required for ftrack to run. Url should be "https://d9.flashtalking.com/d9core".`); + }); + }); + + describe(`ftrackIdSubmodule.isThereConsent():`, () => { + let uspDataHandlerStub; + beforeEach(() => { + uspDataHandlerStub = sinon.stub(uspDataHandler, 'getConsentData'); + }); + + afterEach(() => { + uspDataHandlerStub.restore(); + }); + + describe(`returns 'false' if:`, () => { + it(`GDPR: if gdprApplies is truthy`, () => { + expect(ftrackIdSubmodule.isThereConsent({gdprApplies: 1})).to.not.be.ok; + expect(ftrackIdSubmodule.isThereConsent({gdprApplies: true})).to.not.be.ok; + }); + + it(`US_PRIVACY version 1: if 'Opt Out Sale' is 'Y'`, () => { + uspDataHandlerStub.returns('1YYY'); + expect(ftrackIdSubmodule.isThereConsent({})).to.not.be.ok; + }); + }); + + describe(`returns 'true' if`, () => { + it(`GDPR: if gdprApplies is undefined, false or 0`, () => { + expect(ftrackIdSubmodule.isThereConsent({gdprApplies: 0})).to.be.ok; + expect(ftrackIdSubmodule.isThereConsent({gdprApplies: false})).to.be.ok; + expect(ftrackIdSubmodule.isThereConsent({gdprApplies: null})).to.be.ok; + expect(ftrackIdSubmodule.isThereConsent({})).to.be.ok; + }); + + it(`US_PRIVACY version 1: if 'Opt Out Sale' is not 'Y' ('N','-')`, () => { + uspDataHandlerStub.returns('1NNN'); + expect(ftrackIdSubmodule.isThereConsent(null)).to.be.ok; + + uspDataHandlerStub.returns('1---'); + expect(ftrackIdSubmodule.isThereConsent(null)).to.be.ok; + }); + }); + }); + + describe('getId() method', () => { + it(`should be using the StorageManager to set cookies or localstorage, as opposed to doing it directly`, () => { + expect((/localStorage/gi).test(JSON.stringify(ftrackIdSubmodule))).to.not.be.ok; + expect((/cookie/gi).test(JSON.stringify(ftrackIdSubmodule))).to.not.be.ok; + }); + + it(`should be the only method that gets a new ID aka hits the D9 endpoint`, () => { + let appendChildStub = sinon.stub(window.document.body, 'appendChild'); + + ftrackIdSubmodule.getId(configMock, null, null).callback(); + expect(window.document.body.appendChild.called).to.be.ok; + let actualScriptTag = window.document.body.appendChild.args[0][0]; + expect(actualScriptTag.tagName.toLowerCase()).to.equal('script'); + expect(actualScriptTag.getAttribute('src')).to.equal('https://d9.flashtalking.com/d9core'); + appendChildStub.resetHistory(); + + ftrackIdSubmodule.decode('value', configMock); + expect(window.document.body.appendChild.called).to.not.be.ok; + expect(window.document.body.appendChild.args).to.deep.equal([]); + appendChildStub.resetHistory(); + + ftrackIdSubmodule.extendId(configMock, null, {cache: {id: ''}}); + expect(window.document.body.appendChild.called).to.not.be.ok; + expect(window.document.body.appendChild.args).to.deep.equal([]); + + appendChildStub.restore(); + }); + + it(`should populate localstorage and return the IDS (end-to-end test)`, () => { + let ftrackId, + ftrackIdExp, + forceCallback = false; + + // Confirm that our item is not in localStorage yet + expect(window.localStorage.getItem('ftrack-rtd')).to.not.be.ok; + expect(window.localStorage.getItem('ftrack-rtd_exp')).to.not.be.ok; + + ftrackIdSubmodule.getId(configMock, consentDataMock, null).callback(); + return new Promise(function(resolve, reject) { + window.testTimer = function () { + // Sinon fake server is NOT changing the readyState to 4, so instead + // we are forcing the callback to run and just passing in the expected Object + if (!forceCallback && window.hasOwnProperty('D9r')) { + window.D9r.callback({ 'DeviceID': [''], 'SingleDeviceID': [''] }); + forceCallback = true; + } + + ftrackId = window.localStorage.getItem('ftrackId'); + ftrackIdExp = window.localStorage.getItem('ftrackId_exp'); + + if (!!ftrackId && !!ftrackIdExp) { + expect(window.localStorage.getItem('ftrackId')).to.be.ok; + expect(window.localStorage.getItem('ftrackId_exp')).to.be.ok; + resolve(); + } else { + window.setTimeout(window.testTimer, 25); + } + }; + window.testTimer(); + }); + }); + }); + + describe(`decode() method`, () => { + it(`should respond with an object with the key 'ftrackId'`, () => { + expect(ftrackIdSubmodule.decode('value', configMock)).to.deep.equal({ftrackId: 'value'}); + }); + + it(`should not be making requests to retrieve a new ID, it should just be decoding a response`, () => { + server = sinon.createFakeServer(); + ftrackIdSubmodule.decode('value', configMock); + + expect(server.requests).to.have.length(0); + + server.restore(); + }) + }); + + describe(`extendId() method`, () => { + it(`should not be making requests to retrieve a new ID, it should just be adding additional data to the id object`, () => { + server = sinon.createFakeServer(); + ftrackIdSubmodule.extendId(configMock, null, {cache: {id: ''}}); + + expect(server.requests).to.have.length(0); + + server.restore(); + }); + + it(`should return cacheIdObj`, () => { + expect(ftrackIdSubmodule.extendId(configMock, null, {cache: {id: ''}})).to.deep.equal({cache: {id: ''}}); + }); + }); +}); diff --git a/test/spec/modules/gdprEnforcement_spec.js b/test/spec/modules/gdprEnforcement_spec.js index 82cb70f42be..a78f5ac948e 100644 --- a/test/spec/modules/gdprEnforcement_spec.js +++ b/test/spec/modules/gdprEnforcement_spec.js @@ -16,7 +16,7 @@ import { config } from 'src/config.js'; import adapterManager, { gdprDataHandler } from 'src/adapterManager.js'; import * as utils from 'src/utils.js'; import { validateStorageEnforcement } from 'src/storageManager.js'; -import events from 'src/events.js'; +import * as events from 'src/events.js'; describe('gdpr enforcement', function () { let nextFnSpy; diff --git a/test/spec/modules/glimpseBidAdapter_spec.js b/test/spec/modules/glimpseBidAdapter_spec.js index c139a7ab8d3..02f5b502a1b 100644 --- a/test/spec/modules/glimpseBidAdapter_spec.js +++ b/test/spec/modules/glimpseBidAdapter_spec.js @@ -178,24 +178,6 @@ describe('GlimpseProtocolAdapter', () => { const bidRequests = [getBidRequest()] const bidderRequest = getBidderRequest() - it('Adds a demo flag', () => { - const request = spec.buildRequests(bidRequests, bidderRequest) - const payload = JSON.parse(request.data) - expect(payload.data.demo).to.be.false - }) - - it('Adds an account id', () => { - const request = spec.buildRequests(bidRequests, bidderRequest) - const payload = JSON.parse(request.data) - expect(payload.data.account).to.equal(-1) - }) - - it('Adds a demand provider', () => { - const request = spec.buildRequests(bidRequests, bidderRequest) - const payload = JSON.parse(request.data) - expect(payload.data.demand).to.equal('glimpse') - }) - it('Adds GDPR consent', () => { const request = spec.buildRequests(bidRequests, bidderRequest) const payload = JSON.parse(request.data) diff --git a/test/spec/modules/gmosspBidAdapter_spec.js b/test/spec/modules/gmosspBidAdapter_spec.js index c22badb9b4c..87c87600b97 100644 --- a/test/spec/modules/gmosspBidAdapter_spec.js +++ b/test/spec/modules/gmosspBidAdapter_spec.js @@ -1,7 +1,6 @@ import { expect } from 'chai'; import { spec } from 'modules/gmosspBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; -import {getStorageManager} from 'src/storageManager'; import * as utils from 'src/utils.js'; const ENDPOINT = 'https://sp.gmossp-sp.jp/hb/prebid/query.ad'; @@ -36,8 +35,7 @@ describe('GmosspAdapter', function () { }); describe('buildRequests', function () { - const storage = getStorageManager(); - const bidRequests = [ + let bidRequests = [ { bidder: 'gmossp', params: { @@ -52,7 +50,12 @@ describe('GmosspAdapter', function () { bidId: '2b84475b5b636e', bidderRequestId: '1f4001782ac16c', auctionId: 'aba03555-4802-4c45-9f15-05ffa8594cff', - transactionId: '791e9d84-af92-4903-94da-24c7426d9d0c' + transactionId: '791e9d84-af92-4903-94da-24c7426d9d0c', + userId: { + imuid: 'h.0a4749e7ffe09fa6', + pubcid: '1111', + idl_env: '1111', + } } ]; @@ -62,24 +65,24 @@ describe('GmosspAdapter', function () { referer: 'https://hoge.com' } }; - storage.setCookie('_im_uid.1000283', 'h.0a4749e7ffe09fa6'); - const requests = spec.buildRequests(bidRequests, bidderRequest); expect(requests[0].url).to.equal(ENDPOINT); expect(requests[0].method).to.equal('GET'); - expect(requests[0].data).to.equal('tid=791e9d84-af92-4903-94da-24c7426d9d0c&bid=2b84475b5b636e&ver=$prebid.version$&sid=123456&im_uid=h.0a4749e7ffe09fa6&url=https%3A%2F%2Fhoge.com&cur=JPY&dnt=0&'); + expect(requests[0].data).to.equal('tid=791e9d84-af92-4903-94da-24c7426d9d0c&bid=2b84475b5b636e&ver=$prebid.version$&sid=123456&im_uid=h.0a4749e7ffe09fa6&shared_id=1111&idl_env=1111&url=https%3A%2F%2Fhoge.com' + '&ref=' + encodeURIComponent(document.referrer) + '&cur=JPY&dnt=0&'); }); - it('should use fallback if refererInfo.referer in bid request is empty and _im_uid.1000283 cookie is empty', function () { + it('should use fallback if refererInfo.referer in bid request is empty and im_uid ,shared_id, idl_env cookie is empty', function () { const bidderRequest = { refererInfo: { referer: '' - } + }, }; - storage.setCookie('_im_uid.1000283', ''); + bidRequests[0].userId.imuid = ''; + bidRequests[0].userId.pubcid = ''; + bidRequests[0].userId.idl_env = ''; const requests = spec.buildRequests(bidRequests, bidderRequest); - const result = 'tid=791e9d84-af92-4903-94da-24c7426d9d0c&bid=2b84475b5b636e&ver=$prebid.version$&sid=123456&url=' + encodeURIComponent(window.top.location.href) + '&cur=JPY&dnt=0&'; + const result = 'tid=791e9d84-af92-4903-94da-24c7426d9d0c&bid=2b84475b5b636e&ver=$prebid.version$&sid=123456&ref=' + encodeURIComponent(document.referrer) + '&cur=JPY&dnt=0&'; expect(requests[0].data).to.equal(result); }); }); @@ -104,7 +107,7 @@ describe('GmosspAdapter', function () { } ]; - it('should get correct banner bid response', function() { + it('should get correct banner bid response', function () { const response = { bid: '2b84475b5b636e', price: 20, diff --git a/test/spec/modules/gnetBidAdapter_spec.js b/test/spec/modules/gnetBidAdapter_spec.js index eeb33418a82..a69b196bc5c 100644 --- a/test/spec/modules/gnetBidAdapter_spec.js +++ b/test/spec/modules/gnetBidAdapter_spec.js @@ -8,7 +8,7 @@ import { newBidder } from 'src/adapters/bidderFactory.js'; -const ENDPOINT = 'https://adserver.gnetproject.com/prebid.php'; +const ENDPOINT = 'https://service.gnetrtb.com/api/adrequest'; describe('gnetAdapter', function () { const adapter = newBidder(spec); @@ -23,7 +23,7 @@ describe('gnetAdapter', function () { let bid = { bidder: 'gnet', params: { - websiteId: '4' + websiteId: '1', adunitId: '1' } }; @@ -39,11 +39,22 @@ describe('gnetAdapter', function () { }); }); + describe('onBidWon', function () { + const bid = { + requestId: '29d5b1d3a520f8' + }; + + it('return success adserver won bid endpoint', () => { + const result = spec.onBidWon(bid); + assert.ok(result); + }); + }); + describe('buildRequests', function () { const bidRequests = [{ bidder: 'gnet', params: { - websiteId: '4' + websiteId: '1', adunitId: '1' }, adUnitCode: '/150790500/4_ZONA_IAB_300x250_5', sizes: [ @@ -52,12 +63,13 @@ describe('gnetAdapter', function () { bidId: '2a19afd5173318', bidderRequestId: '1f4001782ac16c', auctionId: 'aba03555-4802-4c45-9f15-05ffa8594cff', - transactionId: '894bdff6-61ec-4bec-a5a9-f36a5bfccef5' + transactionId: '894bdff6-61ec-4bec-a5a9-f36a5bfccef5', + gftuid: null }]; const bidderRequest = { refererInfo: { - referer: 'https://gnetproject.com/' + referer: 'https://gnetrtb.com' } }; @@ -66,13 +78,14 @@ describe('gnetAdapter', function () { expect(requests[0].url).to.equal(ENDPOINT); expect(requests[0].method).to.equal('POST'); expect(requests[0].data).to.equal(JSON.stringify({ - 'referer': 'https://gnetproject.com/', + 'referer': 'https://gnetrtb.com', 'adUnitCode': '/150790500/4_ZONA_IAB_300x250_5', 'bidId': '2a19afd5173318', 'transactionId': '894bdff6-61ec-4bec-a5a9-f36a5bfccef5', + 'gftuid': null, 'sizes': ['300x250'], 'params': { - 'websiteId': '4' + 'websiteId': '1', 'adunitId': '1' } })); }); diff --git a/test/spec/modules/goldbachBidAdapter_spec.js b/test/spec/modules/goldbachBidAdapter_spec.js new file mode 100644 index 00000000000..459cda7958f --- /dev/null +++ b/test/spec/modules/goldbachBidAdapter_spec.js @@ -0,0 +1,1359 @@ +import { expect } from 'chai'; +import { spec } from 'modules/goldbachBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import * as bidderFactory from 'src/adapters/bidderFactory.js'; +import { auctionManager } from 'src/auctionManager.js'; +import { deepClone } from 'src/utils.js'; +import { config } from 'src/config.js'; + +const ENDPOINT = 'https://ib.adnxs.com/ut/v3/prebid'; +const PRICING_ENDPOINT = 'https://templates.da-services.ch/01_universal/burda_prebid/1.0/json/sizeCPMMapping.json'; + +describe('GoldbachXandrAdapter', function () { + const adapter = newBidder(spec); + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + let bid = { + 'bidder': 'goldbach', + 'params': { + 'placementId': '10433394' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return true when required params found', function () { + let bid = Object.assign({}, bid); + delete bid.params; + bid.params = { + 'member': '1234', + 'invCode': 'ABCD' + }; + + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + let bid = Object.assign({}, bid); + delete bid.params; + bid.params = { + 'placementId': 0 + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + let getAdUnitsStub; + let bidRequests = [ + { + 'bidder': 'goldbach', + 'params': { + 'placementId': '10433394' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'transactionId': '04f2659e-c005-4eb1-a57c-fa93145e3843' + } + ]; + + beforeEach(function() { + getAdUnitsStub = sinon.stub(auctionManager, 'getAdUnits').callsFake(function() { + return []; + }); + }); + + afterEach(function() { + getAdUnitsStub.restore(); + }); + + it('should parse out private sizes', function () { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { + placementId: '10433394', + privateSizes: [300, 250] + } + } + ); + + const request = spec.buildRequests([bidRequest])[1]; + const payload = JSON.parse(request.data); + + expect(payload.tags[0].private_sizes).to.exist; + expect(payload.tags[0].private_sizes).to.deep.equal([{width: 300, height: 250}]); + }); + + it('should add publisher_id in request', function() { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { + placementId: '10433394', + publisherId: '1231234' + } + }); + const request = spec.buildRequests([bidRequest])[1]; + const payload = JSON.parse(request.data); + + expect(payload.tags[0].publisher_id).to.exist; + expect(payload.tags[0].publisher_id).to.deep.equal(1231234); + expect(payload.publisher_id).to.exist; + expect(payload.publisher_id).to.deep.equal(1231234); + }) + + it('should add source and verison to the tag', function () { + const request = spec.buildRequests(bidRequests)[1]; + const payload = JSON.parse(request.data); + expect(payload.sdk).to.exist; + expect(payload.sdk).to.deep.equal({ + source: 'pbjs', + version: '$prebid.version$' + }); + }); + + it('should populate the ad_types array on all requests', function () { + let adUnits = [{ + code: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bids: [{ + bidder: 'goldbach', + params: { + placementId: '10433394' + } + }], + transactionId: '04f2659e-c005-4eb1-a57c-fa93145e3843' + }]; + + ['banner', 'video', 'native'].forEach(type => { + getAdUnitsStub.callsFake(function(...args) { + return adUnits; + }); + + const bidRequest = Object.assign({}, bidRequests[0]); + bidRequest.mediaTypes = {}; + bidRequest.mediaTypes[type] = {}; + + const request = spec.buildRequests([bidRequest])[1]; + const payload = JSON.parse(request.data); + + expect(payload.tags[0].ad_types).to.deep.equal([type]); + + if (type === 'banner') { + delete adUnits[0].mediaTypes; + } + }); + }); + + it('should not populate the ad_types array when adUnit.mediaTypes is undefined', function() { + const bidRequest = Object.assign({}, bidRequests[0]); + const request = spec.buildRequests([bidRequest])[1]; + const payload = JSON.parse(request.data); + + expect(payload.tags[0].ad_types).to.not.exist; + }); + + it('should populate the ad_types array on outstream requests', function () { + const bidRequest = Object.assign({}, bidRequests[0]); + bidRequest.mediaTypes = {}; + bidRequest.mediaTypes.video = {context: 'outstream'}; + + const request = spec.buildRequests([bidRequest])[1]; + const payload = JSON.parse(request.data); + + expect(payload.tags[0].ad_types).to.deep.equal(['video']); + expect(payload.tags[0].hb_source).to.deep.equal(1); + }); + + it('sends bid request to ENDPOINT via POST', function () { + const request = spec.buildRequests(bidRequests)[1]; + expect(request.url).to.equal(ENDPOINT); + expect(request.method).to.equal('POST'); + }); + + it('sends bid request to ENDPOINT via GET', function () { + const request = spec.buildRequests(bidRequests)[0]; + expect(request.url).to.equal(PRICING_ENDPOINT); + expect(request.method).to.equal('GET'); + }); + + it('should attach valid video params to the tag', function () { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { + placementId: '10433394', + video: { + id: 123, + minduration: 100, + foobar: 'invalid' + } + } + } + ); + + const request = spec.buildRequests([bidRequest])[1]; + const payload = JSON.parse(request.data); + expect(payload.tags[0].video).to.deep.equal({ + id: 123, + minduration: 100 + }); + expect(payload.tags[0].hb_source).to.deep.equal(1); + }); + + it('should include ORTB video values when video params were not set', function() { + let bidRequest = deepClone(bidRequests[0]); + bidRequest.params = { + placementId: '1234235', + video: { + skippable: true, + playback_method: ['auto_play_sound_off', 'auto_play_sound_unknown'], + context: 'outstream' + } + }; + bidRequest.mediaTypes = { + video: { + playerSize: [640, 480], + context: 'outstream', + mimes: ['video/mp4'], + skip: 0, + minduration: 5, + api: [1, 5, 6], + playbackmethod: [2, 4] + } + }; + + const request = spec.buildRequests([bidRequest])[1]; + const payload = JSON.parse(request.data); + + expect(payload.tags[0].video).to.deep.equal({ + minduration: 5, + playback_method: 2, + skippable: true, + context: 4 + }); + expect(payload.tags[0].video_frameworks).to.deep.equal([1, 4]) + }); + + it('should add video property when adUnit includes a renderer', function () { + const videoData = { + mediaTypes: { + video: { + context: 'outstream', + mimes: ['video/mp4'] + } + }, + params: { + placementId: '10433394', + video: { + skippable: true, + playback_method: ['auto_play_sound_off'] + } + } + }; + + let bidRequest1 = deepClone(bidRequests[0]); + bidRequest1 = Object.assign({}, bidRequest1, videoData, { + renderer: { + url: 'https://test.renderer.url', + render: function () {} + } + }); + + let bidRequest2 = deepClone(bidRequests[0]); + bidRequest2.adUnitCode = 'adUnit_code_2'; + bidRequest2 = Object.assign({}, bidRequest2, videoData); + + const request = spec.buildRequests([bidRequest1, bidRequest2])[1]; + const payload = JSON.parse(request.data); + expect(payload.tags[0].video).to.deep.equal({ + skippable: true, + playback_method: 2, + custom_renderer_present: true + }); + expect(payload.tags[1].video).to.deep.equal({ + skippable: true, + playback_method: 2 + }); + }); + + it('should attach valid user params to the tag', function () { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { + placementId: '10433394', + user: { + externalUid: '123', + segments: [123, { id: 987, value: 876 }], + foobar: 'invalid' + } + } + } + ); + + const request = spec.buildRequests([bidRequest])[1]; + const payload = JSON.parse(request.data); + + expect(payload.user).to.exist; + expect(payload.user).to.deep.equal({ + external_uid: '123', + segments: [{id: 123}, {id: 987, value: 876}] + }); + }); + + it('should attach reserve param when either bid param or getFloor function exists', function () { + let getFloorResponse = { currency: 'USD', floor: 3 }; + let request, payload = null; + let bidRequest = deepClone(bidRequests[0]); + + // 1 -> reserve not defined, getFloor not defined > empty + request = spec.buildRequests([bidRequest])[1]; + payload = JSON.parse(request.data); + + expect(payload.tags[0].reserve).to.not.exist; + + // 2 -> reserve is defined, getFloor not defined > reserve is used + bidRequest.params = { + 'placementId': '10433394', + 'reserve': 0.5 + }; + request = spec.buildRequests([bidRequest])[1]; + payload = JSON.parse(request.data); + + expect(payload.tags[0].reserve).to.exist.and.to.equal(0.5); + + // 3 -> reserve is defined, getFloor is defined > getFloor is used + bidRequest.getFloor = () => getFloorResponse; + + request = spec.buildRequests([bidRequest])[1]; + payload = JSON.parse(request.data); + + expect(payload.tags[0].reserve).to.exist.and.to.equal(3); + }); + + it('should duplicate adpod placements into batches and set correct maxduration', function() { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { placementId: '14542875' } + }, + { + mediaTypes: { + video: { + context: 'adpod', + playerSize: [640, 480], + adPodDurationSec: 300, + durationRangeSec: [15, 30], + } + } + } + ); + + const request = spec.buildRequests([bidRequest])[1]; + const payload1 = JSON.parse(request[0].data); + const payload2 = JSON.parse(request[1].data); + + // 300 / 15 = 20 total + expect(payload1.tags.length).to.equal(15); + expect(payload2.tags.length).to.equal(5); + + expect(payload1.tags[0]).to.deep.equal(payload1.tags[1]); + expect(payload1.tags[0].video.maxduration).to.equal(30); + + expect(payload2.tags[0]).to.deep.equal(payload1.tags[1]); + expect(payload2.tags[0].video.maxduration).to.equal(30); + }); + + it('should round down adpod placements when numbers are uneven', function() { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { placementId: '14542875' } + }, + { + mediaTypes: { + video: { + context: 'adpod', + playerSize: [640, 480], + adPodDurationSec: 123, + durationRangeSec: [45], + } + } + } + ); + + const request = spec.buildRequests([bidRequest])[1]; + const payload = JSON.parse(request.data); + expect(payload.tags.length).to.equal(2); + }); + + it('should duplicate adpod placements when requireExactDuration is set', function() { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { placementId: '14542875' } + }, + { + mediaTypes: { + video: { + context: 'adpod', + playerSize: [640, 480], + adPodDurationSec: 300, + durationRangeSec: [15, 30], + requireExactDuration: true, + } + } + } + ); + + // 20 total placements with 15 max impressions = 2 requests + const request = spec.buildRequests([bidRequest])[1]; + expect(request.length).to.equal(2); + + // 20 spread over 2 requests = 15 in first request, 5 in second + const payload1 = JSON.parse(request[0].data); + const payload2 = JSON.parse(request[1].data); + expect(payload1.tags.length).to.equal(15); + expect(payload2.tags.length).to.equal(5); + + // 10 placements should have max/min at 15 + // 10 placemenst should have max/min at 30 + const payload1tagsWith15 = payload1.tags.filter(tag => tag.video.maxduration === 15); + const payload1tagsWith30 = payload1.tags.filter(tag => tag.video.maxduration === 30); + expect(payload1tagsWith15.length).to.equal(10); + expect(payload1tagsWith30.length).to.equal(5); + + // 5 placemenst with min/max at 30 were in the first request + // so 5 remaining should be in the second + const payload2tagsWith30 = payload2.tags.filter(tag => tag.video.maxduration === 30); + expect(payload2tagsWith30.length).to.equal(5); + }); + + it('should set durations for placements when requireExactDuration is set and numbers are uneven', function() { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { placementId: '14542875' } + }, + { + mediaTypes: { + video: { + context: 'adpod', + playerSize: [640, 480], + adPodDurationSec: 105, + durationRangeSec: [15, 30, 60], + requireExactDuration: true, + } + } + } + ); + + const request = spec.buildRequests([bidRequest])[1]; + const payload = JSON.parse(request.data); + expect(payload.tags.length).to.equal(7); + + const tagsWith15 = payload.tags.filter(tag => tag.video.maxduration === 15); + const tagsWith30 = payload.tags.filter(tag => tag.video.maxduration === 30); + const tagsWith60 = payload.tags.filter(tag => tag.video.maxduration === 60); + expect(tagsWith15.length).to.equal(3); + expect(tagsWith30.length).to.equal(3); + expect(tagsWith60.length).to.equal(1); + }); + + it('should break adpod request into batches', function() { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { placementId: '14542875' } + }, + { + mediaTypes: { + video: { + context: 'adpod', + playerSize: [640, 480], + adPodDurationSec: 225, + durationRangeSec: [5], + } + } + } + ); + + const request = spec.buildRequests([bidRequest])[1]; + const payload1 = JSON.parse(request[0].data); + const payload2 = JSON.parse(request[1].data); + const payload3 = JSON.parse(request[2].data); + + expect(payload1.tags.length).to.equal(15); + expect(payload2.tags.length).to.equal(15); + expect(payload3.tags.length).to.equal(15); + }); + + // it('should contain hb_source value for adpod', function() { + // let bidRequest = Object.assign({}, + // bidRequests[0], + // { + // params: { placementId: '14542875' } + // }, + // { + // mediaTypes: { + // video: { + // context: 'adpod', + // playerSize: [640, 480], + // adPodDurationSec: 300, + // durationRangeSec: [15, 30], + // } + // } + // } + // ); + // const request = spec.buildRequests([bidRequest])[1]; + // const payload = JSON.parse(request.data); + // expect(payload.tags[0].hb_source).to.deep.equal(7); + // }); + + it('should contain hb_source value for other media', function() { + let bidRequest = Object.assign({}, + bidRequests[0], + { + mediaType: 'banner', + params: { + sizes: [[300, 250], [300, 600]], + placementId: 13144370 + } + } + ); + const request = spec.buildRequests([bidRequest])[1]; + const payload = JSON.parse(request.data); + expect(payload.tags[0].hb_source).to.deep.equal(1); + }); + + it('adds brand_category_exclusion to request when set', function() { + let bidRequest = Object.assign({}, bidRequests[0]); + sinon + .stub(config, 'getConfig') + .withArgs('adpod.brandCategoryExclusion') + .returns(true); + + const request = spec.buildRequests([bidRequest])[1]; + const payload = JSON.parse(request.data); + + expect(payload.brand_category_uniqueness).to.equal(true); + + config.getConfig.restore(); + }); + + it('should attach native params to the request', function () { + let bidRequest = Object.assign({}, + bidRequests[0], + { + mediaType: 'native', + nativeParams: { + title: {required: true}, + body: {required: true}, + body2: {required: true}, + image: {required: true, sizes: [100, 100]}, + icon: {required: true}, + cta: {required: false}, + rating: {required: true}, + sponsoredBy: {required: true}, + privacyLink: {required: true}, + displayUrl: {required: true}, + address: {required: true}, + downloads: {required: true}, + likes: {required: true}, + phone: {required: true}, + price: {required: true}, + salePrice: {required: true} + } + } + ); + + const request = spec.buildRequests([bidRequest])[1]; + const payload = JSON.parse(request.data); + + expect(payload.tags[0].native.layouts[0]).to.deep.equal({ + title: {required: true}, + description: {required: true}, + desc2: {required: true}, + main_image: {required: true, sizes: [{ width: 100, height: 100 }]}, + icon: {required: true}, + ctatext: {required: false}, + rating: {required: true}, + sponsored_by: {required: true}, + privacy_link: {required: true}, + displayurl: {required: true}, + address: {required: true}, + downloads: {required: true}, + likes: {required: true}, + phone: {required: true}, + price: {required: true}, + saleprice: {required: true}, + privacy_supported: true + }); + expect(payload.tags[0].hb_source).to.equal(1); + }); + + it('should always populated tags[].sizes with 1,1 for native if otherwise not defined', function () { + let bidRequest = Object.assign({}, + bidRequests[0], + { + mediaType: 'native', + nativeParams: { + image: { required: true } + } + } + ); + bidRequest.sizes = [[150, 100], [300, 250]]; + + let request = spec.buildRequests([bidRequest])[1]; + let payload = JSON.parse(request.data); + expect(payload.tags[0].sizes).to.deep.equal([{width: 150, height: 100}, {width: 300, height: 250}]); + + delete bidRequest.sizes; + + request = spec.buildRequests([bidRequest])[1]; + payload = JSON.parse(request.data); + + expect(payload.tags[0].sizes).to.deep.equal([{width: 1, height: 1}]); + }); + + it('should convert keyword params to proper form and attaches to request', function () { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { + placementId: '10433394', + keywords: { + single: 'val', + singleArr: ['val'], + singleArrNum: [5], + multiValMixed: ['value1', 2, 'value3'], + singleValNum: 123, + emptyStr: '', + emptyArr: [''], + badValue: {'foo': 'bar'} // should be dropped + } + } + } + ); + + const request = spec.buildRequests([bidRequest])[1]; + const payload = JSON.parse(request.data); + + expect(payload.tags[0].keywords).to.deep.equal([{ + 'key': 'single', + 'value': ['val'] + }, { + 'key': 'singleArr', + 'value': ['val'] + }, { + 'key': 'singleArrNum', + 'value': ['5'] + }, { + 'key': 'multiValMixed', + 'value': ['value1', '2', 'value3'] + }, { + 'key': 'singleValNum', + 'value': ['123'] + }, { + 'key': 'emptyStr' + }, { + 'key': 'emptyArr' + }]); + }); + + it('should add payment rules to the request', function () { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { + placementId: '10433394', + usePaymentRule: true + } + } + ); + + const request = spec.buildRequests([bidRequest])[1]; + const payload = JSON.parse(request.data); + + expect(payload.tags[0].use_pmt_rule).to.equal(true); + }); + + it('should add gpid to the request', function () { + let testGpid = '/12345/my-gpt-tag-0'; + let bidRequest = deepClone(bidRequests[0]); + bidRequest.ortb2Imp = { ext: { data: { pbadslot: testGpid } } }; + + const request = spec.buildRequests([bidRequest])[1]; + const payload = JSON.parse(request.data); + + expect(payload.tags[0].gpid).to.exist.and.equal(testGpid) + }); + + it('should add gdpr consent information to the request', function () { + let consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; + let bidderRequest = { + 'bidderCode': 'goldbach', + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + consentString: consentString, + gdprApplies: true, + addtlConsent: '1~7.12.35.62.66.70.89.93.108' + } + }; + bidderRequest.bids = bidRequests; + + const request = spec.buildRequests(bidRequests, bidderRequest)[1]; + expect(request.options).to.deep.equal({withCredentials: true}); + const payload = JSON.parse(request.data); + + expect(payload.gdpr_consent).to.exist; + expect(payload.gdpr_consent.consent_string).to.exist.and.to.equal(consentString); + expect(payload.gdpr_consent.consent_required).to.exist.and.to.be.true; + expect(payload.gdpr_consent.addtl_consent).to.exist.and.to.deep.equal([7, 12, 35, 62, 66, 70, 89, 93, 108]); + }); + + it('should add us privacy string to payload', function() { + let consentString = '1YA-'; + let bidderRequest = { + 'bidderCode': 'goldbach', + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'uspConsent': consentString + }; + bidderRequest.bids = bidRequests; + + const request = spec.buildRequests(bidRequests, bidderRequest)[1]; + const payload = JSON.parse(request.data); + + expect(payload.us_privacy).to.exist; + expect(payload.us_privacy).to.exist.and.to.equal(consentString); + }); + + it('supports sending hybrid mobile app parameters', function () { + let appRequest = Object.assign({}, + bidRequests[0], + { + params: { + placementId: '10433394', + app: { + id: 'B1O2W3M4AN.com.prebid.webview', + geo: { + lat: 40.0964439, + lng: -75.3009142 + }, + device_id: { + idfa: '4D12078D-3246-4DA4-AD5E-7610481E7AE', // Apple advertising identifier + aaid: '38400000-8cf0-11bd-b23e-10b96e40000d', // Android advertising identifier + md5udid: '5756ae9022b2ea1e47d84fead75220c8', // MD5 hash of the ANDROID_ID + sha1udid: '4DFAA92388699AC6539885AEF1719293879985BF', // SHA1 hash of the ANDROID_ID + windowsadid: '750c6be243f1c4b5c9912b95a5742fc5' // Windows advertising identifier + } + } + } + } + ); + const request = spec.buildRequests([appRequest])[1]; + const payload = JSON.parse(request.data); + expect(payload.app).to.exist; + expect(payload.app).to.deep.equal({ + appid: 'B1O2W3M4AN.com.prebid.webview' + }); + expect(payload.device.device_id).to.exist; + expect(payload.device.device_id).to.deep.equal({ + aaid: '38400000-8cf0-11bd-b23e-10b96e40000d', + idfa: '4D12078D-3246-4DA4-AD5E-7610481E7AE', + md5udid: '5756ae9022b2ea1e47d84fead75220c8', + sha1udid: '4DFAA92388699AC6539885AEF1719293879985BF', + windowsadid: '750c6be243f1c4b5c9912b95a5742fc5' + }); + expect(payload.device.geo).to.exist; + expect(payload.device.geo).to.deep.equal({ + lat: 40.0964439, + lng: -75.3009142 + }); + }); + + it('should add referer info to payload', function () { + const bidRequest = Object.assign({}, bidRequests[0]) + const bidderRequest = { + refererInfo: { + referer: 'https://example.com/page.html', + reachedTop: true, + numIframes: 2, + stack: [ + 'https://example.com/page.html', + 'https://example.com/iframe1.html', + 'https://example.com/iframe2.html' + ] + } + } + const request = spec.buildRequests([bidRequest], bidderRequest)[1]; + const payload = JSON.parse(request.data); + + expect(payload.referrer_detection).to.exist; + expect(payload.referrer_detection).to.deep.equal({ + rd_ref: 'https%3A%2F%2Fexample.com%2Fpage.html', + rd_top: true, + rd_ifs: 2, + rd_stk: bidderRequest.refererInfo.stack.map((url) => encodeURIComponent(url)).join(',') + }); + }); + + it('should populate schain if available', function () { + const bidRequest = Object.assign({}, bidRequests[0], { + schain: { + ver: '1.0', + complete: 1, + nodes: [ + { + 'asi': 'blob.com', + 'sid': '001', + 'hp': 1 + } + ] + } + }); + + const request = spec.buildRequests([bidRequest])[1]; + const payload = JSON.parse(request.data); + expect(payload.schain).to.deep.equal({ + ver: '1.0', + complete: 1, + nodes: [ + { + 'asi': 'blob.com', + 'sid': '001', + 'hp': 1 + } + ] + }); + }); + + it('should populate coppa if set in config', function () { + let bidRequest = Object.assign({}, bidRequests[0]); + sinon.stub(config, 'getConfig') + .withArgs('coppa') + .returns(true); + + const request = spec.buildRequests([bidRequest])[1]; + const payload = JSON.parse(request.data); + + expect(payload.user.coppa).to.equal(true); + + config.getConfig.restore(); + }); + + it('should set the X-Is-Test customHeader if test flag is enabled', function () { + let bidRequest = Object.assign({}, bidRequests[0]); + sinon.stub(config, 'getConfig') + .withArgs('apn_test') + .returns(true); + + const request = spec.buildRequests([bidRequest])[1]; + expect(request.options.customHeaders).to.deep.equal({'X-Is-Test': 1}); + + config.getConfig.restore(); + }); + + it('should always set withCredentials: true on the request.options', function () { + let bidRequest = Object.assign({}, bidRequests[0]); + const request = spec.buildRequests([bidRequest])[1]; + expect(request.options.withCredentials).to.equal(true); + }); + + it('should set simple domain variant if purpose 1 consent is not given', function () { + let consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; + let bidderRequest = { + 'bidderCode': 'goldbach', + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + consentString: consentString, + gdprApplies: true, + apiVersion: 2, + vendorData: { + purpose: { + consents: { + 1: false + } + } + } + } + }; + bidderRequest.bids = bidRequests; + + const request = spec.buildRequests(bidRequests, bidderRequest)[1]; + expect(request.url).to.equal('https://ib.adnxs-simple.com/ut/v3/prebid'); + }); + + it('should populate eids when supported userIds are available', function () { + const bidRequest = Object.assign({}, bidRequests[0], { + userId: { + tdid: 'sample-userid', + uid2: { id: 'sample-uid2-value' }, + criteoId: 'sample-criteo-userid', + netId: 'sample-netId-userid', + idl_env: 'sample-idl-userid', + flocId: { + id: 'sample-flocid-value', + version: 'chrome.1.0' + } + } + }); + + const request = spec.buildRequests([bidRequest])[1]; + const payload = JSON.parse(request.data); + expect(payload.eids).to.deep.include({ + source: 'adserver.org', + id: 'sample-userid', + rti_partner: 'TDID' + }); + + expect(payload.eids).to.deep.include({ + source: 'criteo.com', + id: 'sample-criteo-userid', + }); + + expect(payload.eids).to.deep.include({ + source: 'chrome.com', + id: 'sample-flocid-value' + }); + + expect(payload.eids).to.deep.include({ + source: 'netid.de', + id: 'sample-netId-userid', + }); + + expect(payload.eids).to.deep.include({ + source: 'liveramp.com', + id: 'sample-idl-userid' + }); + + expect(payload.eids).to.deep.include({ + source: 'uidapi.com', + id: 'sample-uid2-value', + rti_partner: 'UID2' + }); + }); + + it('should populate iab_support object at the root level if omid support is detected', function () { + // with bid.params.frameworks + let bidRequest_A = Object.assign({}, bidRequests[0], { + params: { + frameworks: [1, 2, 5, 6], + video: { + frameworks: [1, 2, 5, 6] + } + } + }); + let request = spec.buildRequests([bidRequest_A])[1]; + let payload = JSON.parse(request.data); + expect(payload.iab_support).to.be.an('object'); + expect(payload.iab_support).to.deep.equal({ + omidpn: 'Appnexus', + omidpv: '$prebid.version$' + }); + expect(payload.tags[0].banner_frameworks).to.be.an('array'); + expect(payload.tags[0].banner_frameworks).to.deep.equal([1, 2, 5, 6]); + expect(payload.tags[0].video_frameworks).to.be.an('array'); + expect(payload.tags[0].video_frameworks).to.deep.equal([1, 2, 5, 6]); + expect(payload.tags[0].video.frameworks).to.not.exist; + + // without bid.params.frameworks + const bidRequest_B = Object.assign({}, bidRequests[0]); + request = spec.buildRequests([bidRequest_B])[1]; + payload = JSON.parse(request.data); + expect(payload.iab_support).to.not.exist; + expect(payload.tags[0].banner_frameworks).to.not.exist; + expect(payload.tags[0].video_frameworks).to.not.exist; + + // with video.frameworks but it is not an array + const bidRequest_C = Object.assign({}, bidRequests[0], { + params: { + video: { + frameworks: "'1', '2', '3', '6'" + } + } + }); + request = spec.buildRequests([bidRequest_C])[1]; + payload = JSON.parse(request.data); + expect(payload.iab_support).to.not.exist; + expect(payload.tags[0].banner_frameworks).to.not.exist; + expect(payload.tags[0].video_frameworks).to.not.exist; + }); + }) + + describe('interpretResponse', function () { + let bfStub; + before(function() { + bfStub = sinon.stub(bidderFactory, 'getIabSubCategory'); + }); + + after(function() { + bfStub.restore(); + }); + + let response = { + 'version': '3.0.0', + 'tags': [ + { + 'uuid': '3db3773286ee59', + 'tag_id': 10433394, + 'auction_id': '4534722592064951574', + 'nobid': false, + 'no_ad_url': 'https://lax1-ib.adnxs.com/no-ad', + 'timeout_ms': 10000, + 'ad_profile_id': 27079, + 'ads': [ + { + 'content_source': 'rtb', + 'ad_type': 'banner', + 'buyer_member_id': 958, + 'creative_id': 29681110, + 'media_type_id': 1, + 'media_subtype_id': 1, + 'cpm': 0.5, + 'cpm_publisher_currency': 0.5, + 'publisher_currency_code': '$', + 'client_initiated_ad_counting': true, + 'viewability': { + 'config': '' + }, + 'rtb': { + 'banner': { + 'content': '', + 'width': 300, + 'height': 250 + }, + 'trackers': [ + { + 'impression_urls': [ + 'https://lax1-ib.adnxs.com/impression' + ], + 'video_events': {} + } + ] + } + } + ] + } + ] + }; + + it('should get correct bid response', function () { + let expectedResponse = [ + { + 'requestId': '3db3773286ee59', + 'cpm': 0.5, + 'creativeId': 29681110, + 'dealId': undefined, + 'width': 300, + 'height': 250, + 'ad': '', + 'mediaType': 'banner', + 'currency': 'USD', + 'ttl': 300, + 'netRevenue': true, + 'adUnitCode': 'code', + 'appnexus': { + 'buyerMemberId': 958 + } + } + ]; + let bidderRequest = { + bids: [{ + bidId: '3db3773286ee59', + adUnitCode: 'code' + }] + } + let result = spec.interpretResponse({ body: response }, {bidderRequest}); + expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); + }); + + it('handles nobid responses', function () { + let response = { + 'version': '0.0.1', + 'tags': [{ + 'uuid': '84ab500420319d', + 'tag_id': 5976557, + 'auction_id': '297492697822162468', + 'nobid': true + }] + }; + let bidderRequest; + + let result = spec.interpretResponse({ body: response }, {bidderRequest}); + expect(result.length).to.equal(0); + }); + + it('handles outstream video responses', function () { + let response = { + 'tags': [{ + 'uuid': '84ab500420319d', + 'ads': [{ + 'ad_type': 'video', + 'cpm': 0.500000, + 'notify_url': 'imptracker.com', + 'rtb': { + 'video': { + 'content': '' + } + }, + 'javascriptTrackers': '' + }] + }] + }; + let bidderRequest = { + bids: [{ + bidId: '84ab500420319d', + adUnitCode: 'code', + mediaTypes: { + video: { + context: 'outstream' + } + } + }] + } + + let result = spec.interpretResponse({ body: response }, {bidderRequest}); + expect(result[0]).to.have.property('vastXml'); + expect(result[0]).to.have.property('vastImpUrl'); + expect(result[0]).to.have.property('mediaType', 'video'); + }); + + it('handles instream video responses', function () { + let response = { + 'tags': [{ + 'uuid': '84ab500420319d', + 'ads': [{ + 'ad_type': 'video', + 'cpm': 0.500000, + 'notify_url': 'imptracker.com', + 'rtb': { + 'video': { + 'asset_url': 'https://sample.vastURL.com/here/vid' + } + }, + 'javascriptTrackers': '' + }] + }] + }; + let bidderRequest = { + bids: [{ + bidId: '84ab500420319d', + adUnitCode: 'code', + mediaTypes: { + video: { + context: 'instream' + } + } + }] + } + + let result = spec.interpretResponse({ body: response }, {bidderRequest}); + expect(result[0]).to.have.property('vastUrl'); + expect(result[0]).to.have.property('vastImpUrl'); + expect(result[0]).to.have.property('mediaType', 'video'); + }); + + it('handles adpod responses', function () { + let response = { + 'tags': [{ + 'uuid': '84ab500420319d', + 'ads': [{ + 'ad_type': 'video', + 'brand_category_id': 10, + 'cpm': 0.500000, + 'notify_url': 'imptracker.com', + 'rtb': { + 'video': { + 'asset_url': 'https://sample.vastURL.com/here/adpod', + 'duration_ms': 30000, + } + }, + 'viewability': { + 'config': '' + } + }] + }] + }; + + let bidderRequest = { + bids: [{ + bidId: '84ab500420319d', + adUnitCode: 'code', + mediaTypes: { + video: { + context: 'adpod' + } + } + }] + }; + bfStub.returns('1'); + + let result = spec.interpretResponse({ body: response }, {bidderRequest}); + expect(result[0]).to.have.property('vastUrl'); + expect(result[0].video.context).to.equal('adpod'); + expect(result[0].video.durationSeconds).to.equal(30); + }); + + it('handles native responses', function () { + let response1 = deepClone(response); + response1.tags[0].ads[0].ad_type = 'native'; + response1.tags[0].ads[0].rtb.native = { + 'title': 'Native Creative', + 'desc': 'Cool description great stuff', + 'desc2': 'Additional body text', + 'ctatext': 'Do it', + 'sponsored': 'AppNexus', + 'icon': { + 'width': 0, + 'height': 0, + 'url': 'https://cdn.adnxs.com/icon.png' + }, + 'main_img': { + 'width': 2352, + 'height': 1516, + 'url': 'https://cdn.adnxs.com/img.png' + }, + 'link': { + 'url': 'https://www.appnexus.com', + 'fallback_url': '', + 'click_trackers': ['https://nym1-ib.adnxs.com/click'] + }, + 'impression_trackers': ['https://example.com'], + 'rating': '5', + 'displayurl': 'https://AppNexus.com/?url=display_url', + 'likes': '38908320', + 'downloads': '874983', + 'price': '9.99', + 'saleprice': 'FREE', + 'phone': '1234567890', + 'address': '28 W 23rd St, New York, NY 10010', + 'privacy_link': 'https://appnexus.com/?url=privacy_url', + 'javascriptTrackers': '' + }; + let bidderRequest = { + bids: [{ + bidId: '3db3773286ee59', + adUnitCode: 'code' + }] + } + + let result = spec.interpretResponse({ body: response1 }, {bidderRequest}); + expect(result[0].native.title).to.equal('Native Creative'); + expect(result[0].native.body).to.equal('Cool description great stuff'); + expect(result[0].native.cta).to.equal('Do it'); + expect(result[0].native.image.url).to.equal('https://cdn.adnxs.com/img.png'); + }); + + it('supports configuring outstream renderers', function () { + const outstreamResponse = deepClone(response); + outstreamResponse.tags[0].ads[0].rtb.video = {}; + outstreamResponse.tags[0].ads[0].renderer_url = 'renderer.js'; + + const bidderRequest = { + bids: [{ + bidId: '3db3773286ee59', + renderer: { + options: { + adText: 'configured' + } + }, + mediaTypes: { + video: { + context: 'outstream' + } + } + }] + }; + + const result = spec.interpretResponse({ body: outstreamResponse }, {bidderRequest}); + expect(result[0].renderer.config).to.deep.equal( + bidderRequest.bids[0].renderer.options + ); + }); + + it('should add deal_priority and deal_code', function() { + let responseWithDeal = deepClone(response); + responseWithDeal.tags[0].ads[0].ad_type = 'video'; + responseWithDeal.tags[0].ads[0].deal_priority = 5; + responseWithDeal.tags[0].ads[0].deal_code = '123'; + responseWithDeal.tags[0].ads[0].rtb.video = { + duration_ms: 1500, + player_width: 640, + player_height: 340, + }; + + let bidderRequest = { + bids: [{ + bidId: '3db3773286ee59', + adUnitCode: 'code', + mediaTypes: { + video: { + context: 'adpod' + } + } + }] + } + let result = spec.interpretResponse({ body: responseWithDeal }, {bidderRequest}); + expect(Object.keys(result[0].appnexus)).to.include.members(['buyerMemberId', 'dealPriority', 'dealCode']); + expect(result[0].video.dealTier).to.equal(5); + }); + + it('should add advertiser id', function() { + let responseAdvertiserId = deepClone(response); + responseAdvertiserId.tags[0].ads[0].advertiser_id = '123'; + + let bidderRequest = { + bids: [{ + bidId: '3db3773286ee59', + adUnitCode: 'code' + }] + } + let result = spec.interpretResponse({ body: responseAdvertiserId }, {bidderRequest}); + expect(Object.keys(result[0].meta)).to.include.members(['advertiserId']); + }); + + it('should add advertiserDomains', function() { + let responseAdvertiserId = deepClone(response); + responseAdvertiserId.tags[0].ads[0].adomain = ['123']; + + let bidderRequest = { + bids: [{ + bidId: '3db3773286ee59', + adUnitCode: 'code' + }] + } + let result = spec.interpretResponse({ body: responseAdvertiserId }, {bidderRequest}); + expect(Object.keys(result[0].meta)).to.include.members(['advertiserDomains']); + expect(Object.keys(result[0].meta.advertiserDomains)).to.deep.equal([]); + }); + }); +}); diff --git a/test/spec/modules/gptPreAuction_spec.js b/test/spec/modules/gptPreAuction_spec.js index 9e81aea80d1..2bfce71806e 100644 --- a/test/spec/modules/gptPreAuction_spec.js +++ b/test/spec/modules/gptPreAuction_spec.js @@ -20,7 +20,9 @@ describe('GPT pre-auction module', () => { const testSlots = [ makeSlot({ code: 'slotCode1', divId: 'div1' }), makeSlot({ code: 'slotCode2', divId: 'div2' }), - makeSlot({ code: 'slotCode3', divId: 'div3' }) + makeSlot({ code: 'slotCode3', divId: 'div3' }), + makeSlot({ code: 'slotCode4', divId: 'div4' }), + makeSlot({ code: 'slotCode4', divId: 'div5' }) ]; describe('appendPbAdSlot', () => { @@ -172,7 +174,9 @@ describe('GPT pre-auction module', () => { expect(_currentConfig).to.deep.equal({ enabled: true, customGptSlotMatching: false, - customPbAdSlot: false + customPbAdSlot: false, + customPreAuction: false, + useDefaultPreAuction: false }); }); }); @@ -266,5 +270,176 @@ describe('GPT pre-auction module', () => { runMakeBidRequests(testAdUnits); expect(returnedAdUnits).to.deep.equal(expectedAdUnits); }); + + it('should use the passed customPreAuction logic', () => { + let counter = 0; + config.setConfig({ + gptPreAuction: { + enabled: true, + customPreAuction: (adUnit, slotName) => { + counter += 1; + return `${adUnit.code}-${slotName || counter}`; + } + } + }); + const testAdUnits = [ + { + code: 'adUnit1', + ortb2Imp: { ext: { data: { pbadslot: '12345' } } } + }, + { + code: 'adUnit2', + }, + { + code: 'slotCode3', + }, + { + code: 'div4', + } + ]; + + // all slots should be passed in same time and have slot-${index} + const expectedAdUnits = [{ + code: 'adUnit1', + ortb2Imp: { + ext: { + // no slotname match so uses adUnit.code-counter + data: { + pbadslot: 'adUnit1-1' + }, + gpid: 'adUnit1-1' + } + } + }, + // second adunit + { + code: 'adUnit2', + ortb2Imp: { + ext: { + // no slotname match so uses adUnit.code-counter + data: { + pbadslot: 'adUnit2-2' + }, + gpid: 'adUnit2-2' + } + } + }, { + code: 'slotCode3', + ortb2Imp: { + ext: { + // slotname found, so uses code + slotname (which is same) + data: { + pbadslot: 'slotCode3-slotCode3', + adserver: { + name: 'gam', + adslot: 'slotCode3' + } + }, + gpid: 'slotCode3-slotCode3' + } + } + }, { + code: 'div4', + ortb2Imp: { + ext: { + // slotname found, so uses code + slotname + data: { + pbadslot: 'div4-slotCode4', + adserver: { + name: 'gam', + adslot: 'slotCode4' + } + }, + gpid: 'div4-slotCode4' + } + } + }]; + + window.googletag.pubads().setSlots(testSlots); + runMakeBidRequests(testAdUnits); + expect(returnedAdUnits).to.deep.equal(expectedAdUnits); + }); + + it('should use useDefaultPreAuction logic', () => { + config.setConfig({ + gptPreAuction: { + enabled: true, + useDefaultPreAuction: true + } + }); + const testAdUnits = [ + // First adUnit should use the preset pbadslot + { + code: 'adUnit1', + ortb2Imp: { ext: { data: { pbadslot: '12345' } } } + }, + // Second adUnit should not match a gam slot, so no slot set + { + code: 'adUnit2', + }, + // third adunit matches a single slot so uses it + { + code: 'slotCode3', + }, + // fourth adunit matches multiple slots so combination + { + code: 'div4', + } + ]; + + const expectedAdUnits = [{ + code: 'adUnit1', + ortb2Imp: { + ext: { + data: { + pbadslot: '12345' + }, + gpid: '12345' + } + } + }, + // second adunit + { + code: 'adUnit2', + ortb2Imp: { + ext: { + data: { + }, + } + } + }, { + code: 'slotCode3', + ortb2Imp: { + ext: { + data: { + pbadslot: 'slotCode3', + adserver: { + name: 'gam', + adslot: 'slotCode3' + } + }, + gpid: 'slotCode3' + } + } + }, { + code: 'div4', + ortb2Imp: { + ext: { + data: { + pbadslot: 'slotCode4#div4', + adserver: { + name: 'gam', + adslot: 'slotCode4' + } + }, + gpid: 'slotCode4#div4' + } + } + }]; + + window.googletag.pubads().setSlots(testSlots); + runMakeBidRequests(testAdUnits); + expect(returnedAdUnits).to.deep.equal(expectedAdUnits); + }); }); }); diff --git a/test/spec/modules/gridBidAdapter_spec.js b/test/spec/modules/gridBidAdapter_spec.js index f31b8f16ef7..9e393a5823d 100644 --- a/test/spec/modules/gridBidAdapter_spec.js +++ b/test/spec/modules/gridBidAdapter_spec.js @@ -118,6 +118,24 @@ describe('TheMediaGrid Adapter', function () { } ]; + it('should be content categories and genre', function () { + const site = { + cat: ['IAB2'], + pagecat: ['IAB2-2'], + content: { + genre: 'Adventure' + } + }; + + const getConfigStub = sinon.stub(config, 'getConfig').callsFake( + arg => arg === 'ortb2.site' ? site : null); + const request = spec.buildRequests([bidRequests[0]], bidderRequest); + const payload = parseRequest(request.data); + expect(payload.site.cat).to.deep.equal([...site.cat, ...site.pagecat]); + expect(payload.site.content.genre).to.deep.equal(site.content.genre); + getConfigStub.restore(); + }); + it('should attach valid params to the tag', function () { const fpdUserIdVal = '0b0f84a1-1596-4165-9742-2e1a7dfac57f'; const getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage').callsFake( @@ -381,6 +399,24 @@ describe('TheMediaGrid Adapter', function () { expect(payload.user.ext.eids).to.deep.equal(eids); }); + it('if userId is present payload must have user.ext param with right keys', function () { + const ortb2UserExtDevice = { + screenWidth: 1200, + screenHeight: 800, + language: 'ru' + }; + const getConfigStub = sinon.stub(config, 'getConfig').callsFake( + arg => arg === 'ortb2.user.ext.device' ? ortb2UserExtDevice : null); + + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload).to.have.property('user'); + expect(payload.user).to.have.property('ext'); + expect(payload.user.ext.device).to.deep.equal(ortb2UserExtDevice); + getConfigStub.restore(); + }); + it('if schain is present payload must have source.ext.schain param', function () { const schain = { complete: 1, @@ -510,7 +546,71 @@ describe('TheMediaGrid Adapter', function () { getConfigStub.restore(); }); - it('shold be right tmax when timeout in config is less then timeout in bidderRequest', function() { + it('should have user.data filled from config ortb2.user.data', function () { + const userData = [ + { + name: 'someName', + segment: [1, 2, { anyKey: 'anyVal' }, 'segVal', { id: 'segId' }, { value: 'segValue' }, { id: 'segId2', name: 'segName' }] + }, + { + name: 'permutive.com', + segment: [1, 2, 'segVal', { id: 'segId' }, { anyKey: 'anyVal' }, { value: 'segValue' }, { id: 'segId2', name: 'segName' }] + }, + { + someKey: 'another data' + } + ]; + + const getConfigStub = sinon.stub(config, 'getConfig').callsFake( + arg => arg === 'ortb2.user.data' ? userData : null); + const request = spec.buildRequests([bidRequests[0]], bidderRequest); + const payload = parseRequest(request.data); + expect(payload.user.data).to.deep.equal(userData); + getConfigStub.restore(); + }); + + it('should have right value in user.data when jwpsegments are present', function () { + const userData = [ + { + name: 'someName', + segment: [1, 2, { anyKey: 'anyVal' }, 'segVal', { id: 'segId' }, { value: 'segValue' }, { id: 'segId2', name: 'segName' }] + }, + { + name: 'permutive.com', + segment: [1, 2, 'segVal', { id: 'segId' }, { anyKey: 'anyVal' }, { value: 'segValue' }, { id: 'segId2', name: 'segName' }] + }, + { + someKey: 'another data' + } + ]; + const getConfigStub = sinon.stub(config, 'getConfig').callsFake( + arg => arg === 'ortb2.user.data' ? userData : null); + + const jsContent = {id: 'test_jw_content_id'}; + const jsSegments = ['test_seg_1', 'test_seg_2']; + const bidRequestsWithJwTargeting = Object.assign({}, bidRequests[0], { + rtd: { + jwplayer: { + targeting: { + segments: jsSegments, + content: jsContent + } + } + } + }); + const request = spec.buildRequests([bidRequestsWithJwTargeting], bidderRequest); + const payload = parseRequest(request.data); + expect(payload.user.data).to.deep.equal([{ + name: 'iow_labs_pub_data', + segment: [ + {name: 'jwpseg', value: jsSegments[0]}, + {name: 'jwpseg', value: jsSegments[1]} + ] + }, ...userData]); + getConfigStub.restore(); + }); + + it('should be right tmax when timeout in config is less then timeout in bidderRequest', function() { const getConfigStub = sinon.stub(config, 'getConfig').callsFake( arg => arg === 'bidderTimeout' ? 2000 : null); const request = spec.buildRequests([bidRequests[0]], bidderRequest); @@ -519,7 +619,7 @@ describe('TheMediaGrid Adapter', function () { expect(payload.tmax).to.equal(2000); getConfigStub.restore(); }); - it('shold be right tmax when timeout in bidderRequest is less then timeout in config', function() { + it('should be right tmax when timeout in bidderRequest is less then timeout in config', function() { const getConfigStub = sinon.stub(config, 'getConfig').callsFake( arg => arg === 'bidderTimeout' ? 5000 : null); const request = spec.buildRequests([bidRequests[0]], bidderRequest); @@ -581,6 +681,32 @@ describe('TheMediaGrid Adapter', function () { }); }); + it('should contain imp[].instl if available', function() { + const ortb2Imp = [{ + instl: 1 + }, { + instl: 2, + ext: { + data: { + adserver: { + name: 'ad_server_name', + adslot: '/222222/slot' + }, + pbadslot: '/222222/slot' + } + } + }]; + const bidRequestsWithOrtb2Imp = bidRequests.slice(0, 3).map((bid, ind) => { + return Object.assign(ortb2Imp[ind] ? { ortb2Imp: ortb2Imp[ind] } : {}, bid); + }); + const request = spec.buildRequests(bidRequestsWithOrtb2Imp, bidderRequest); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload.imp[0].instl).to.equal(1); + expect(payload.imp[1].instl).to.equal(2); + expect(payload.imp[2].instl).to.be.undefined; + }); + it('all id must be a string', function() { const fpdUserIdNumVal = 2345543345; const getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage').callsFake( diff --git a/test/spec/modules/gumgumBidAdapter_spec.js b/test/spec/modules/gumgumBidAdapter_spec.js index dfd7db7d922..ebbc1c230f1 100644 --- a/test/spec/modules/gumgumBidAdapter_spec.js +++ b/test/spec/modules/gumgumBidAdapter_spec.js @@ -152,6 +152,12 @@ describe('gumgumAdapter', function () { const zoneParam = { 'zone': '123a' }; const pubIdParam = { 'pubId': 123 }; + it('should set aun if the adUnitCode is available', function () { + const request = { ...bidRequests[0] }; + const bidRequest = spec.buildRequests([request])[0]; + expect(bidRequest.data.aun).to.equal(bidRequests[0].adUnitCode); + }); + it('should set pubId param if found', function () { const request = { ...bidRequests[0], params: pubIdParam }; const bidRequest = spec.buildRequests([request])[0]; @@ -192,8 +198,8 @@ describe('gumgumAdapter', function () { slotRequest.params.slot = invalidSlotId; legacySlotRequest.params.inSlot = invalidSlotId; - req = spec.buildRequests([ slotRequest ])[0]; - legReq = spec.buildRequests([ legacySlotRequest ])[0]; + req = spec.buildRequests([slotRequest])[0]; + legReq = spec.buildRequests([legacySlotRequest])[0]; expect(req.data.si).to.equal(invalidSlotId); expect(legReq.data.si).to.equal(invalidSlotId); @@ -538,6 +544,52 @@ describe('gumgumAdapter', function () { const bidRequest = spec.buildRequests(bidRequests)[0]; expect(!!bidRequest.data.lt).to.be.true; }); + + it('should handle no gg params', function () { + const bidRequest = spec.buildRequests(bidRequests, { refererInfo: { referer: 'https://www.prebid.org/?param1=foo¶m2=bar¶m3=baz' } })[0]; + + // no params are in object + expect(bidRequest.data.hasOwnProperty('eAdBuyId')).to.be.false; + expect(bidRequest.data.hasOwnProperty('adBuyId')).to.be.false; + expect(bidRequest.data.hasOwnProperty('ggdeal')).to.be.false; + }); + + it('should handle encrypted ad buy id', function () { + const bidRequest = spec.buildRequests(bidRequests, { refererInfo: { referer: 'https://www.prebid.org/?param1=foo&ggad=bar¶m3=baz' } })[0]; + + // correct params are in object + expect(bidRequest.data.hasOwnProperty('eAdBuyId')).to.be.true; + expect(bidRequest.data.hasOwnProperty('adBuyId')).to.be.false; + expect(bidRequest.data.hasOwnProperty('ggdeal')).to.be.false; + + // params are stripped from pu property + expect(bidRequest.data.pu.includes('ggad')).to.be.false; + }); + + it('should handle unencrypted ad buy id', function () { + const bidRequest = spec.buildRequests(bidRequests, { refererInfo: { referer: 'https://www.prebid.org/?param1=foo&ggad=123¶m3=baz' } })[0]; + + // correct params are in object + expect(bidRequest.data.hasOwnProperty('eAdBuyId')).to.be.false; + expect(bidRequest.data.hasOwnProperty('adBuyId')).to.be.true; + expect(bidRequest.data.hasOwnProperty('ggdeal')).to.be.false; + + // params are stripped from pu property + expect(bidRequest.data.pu.includes('ggad')).to.be.false; + }); + + it('should handle multiple gg params', function () { + const bidRequest = spec.buildRequests(bidRequests, { refererInfo: { referer: 'https://www.prebid.org/?ggdeal=foo&ggad=bar¶m3=baz' } })[0]; + + // correct params are in object + expect(bidRequest.data.hasOwnProperty('eAdBuyId')).to.be.true; + expect(bidRequest.data.hasOwnProperty('adBuyId')).to.be.false; + expect(bidRequest.data.hasOwnProperty('ggdeal')).to.be.true; + + // params are stripped from pu property + expect(bidRequest.data.pu.includes('ggad')).to.be.false; + expect(bidRequest.data.pu.includes('ggdeal')).to.be.false; + }); }) describe('interpretResponse', function () { @@ -690,7 +742,7 @@ describe('gumgumAdapter', function () { it('uses request size that nearest matches response size for in-screen', function () { const request = { ...bidRequest }; const body = { ...serverResponse }; - const expectedSize = [ 300, 50 ]; + const expectedSize = [300, 50]; let result; request.pi = 2; diff --git a/test/spec/modules/hadronIdSystem_spec.js b/test/spec/modules/hadronIdSystem_spec.js new file mode 100644 index 00000000000..77eeb70326b --- /dev/null +++ b/test/spec/modules/hadronIdSystem_spec.js @@ -0,0 +1,57 @@ +import { hadronIdSubmodule, storage } from 'modules/hadronIdSystem.js'; +import { server } from 'test/mocks/xhr.js'; +import * as utils from 'src/utils.js'; + +describe('HadronIdSystem', function () { + describe('getId', function() { + let getDataFromLocalStorageStub; + + beforeEach(function() { + getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); + }); + + afterEach(function () { + getDataFromLocalStorageStub.restore(); + }); + + it('gets a hadronId', function() { + const config = { + params: {} + }; + const callbackSpy = sinon.spy(); + const callback = hadronIdSubmodule.getId(config).callback; + callback(callbackSpy); + const request = server.requests[0]; + expect(request.url).to.eq(`https://id.hadron.ad.gt/api/v1/pbhid`); + request.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ hadronId: 'testHadronId1' })); + expect(callbackSpy.lastCall.lastArg).to.deep.equal({hadronId: 'testHadronId1'}); + }); + + it('gets a cached hadronid', function() { + const config = { + params: {} + }; + getDataFromLocalStorageStub.withArgs('auHadronId').returns('tstCachedHadronId1'); + + const callbackSpy = sinon.spy(); + const callback = hadronIdSubmodule.getId(config).callback; + callback(callbackSpy); + expect(callbackSpy.lastCall.lastArg).to.deep.equal({hadronId: 'tstCachedHadronId1'}); + }); + + it('allows configurable id url', function() { + const config = { + params: { + url: 'https://hadronid.publync.com' + } + }; + const callbackSpy = sinon.spy(); + const callback = hadronIdSubmodule.getId(config).callback; + callback(callbackSpy); + const request = server.requests[0]; + expect(request.url).to.eq('https://hadronid.publync.com'); + request.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ hadronId: 'testHadronId1' })); + expect(callbackSpy.lastCall.lastArg).to.deep.equal({hadronId: 'testHadronId1'}); + }); + }); +}); diff --git a/test/spec/modules/hadronRtdProvider_spec.js b/test/spec/modules/hadronRtdProvider_spec.js new file mode 100644 index 00000000000..30e4947566f --- /dev/null +++ b/test/spec/modules/hadronRtdProvider_spec.js @@ -0,0 +1,762 @@ +import {config} from 'src/config.js'; +import {HALOID_LOCAL_NAME, RTD_LOCAL_NAME, addRealTimeData, getRealTimeData, hadronSubmodule, storage} from 'modules/hadronRtdProvider.js'; +import {server} from 'test/mocks/xhr.js'; + +const responseHeader = {'Content-Type': 'application/json'}; + +describe('hadronRtdProvider', function() { + let getDataFromLocalStorageStub; + + beforeEach(function() { + config.resetConfig(); + getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); + }); + + afterEach(function () { + getDataFromLocalStorageStub.restore(); + }); + + describe('hadronSubmodule', function() { + it('successfully instantiates', function () { + expect(hadronSubmodule.init()).to.equal(true); + }); + }); + + describe('Add Real-Time Data', function() { + it('merges ortb2 data', function() { + let rtdConfig = {}; + let bidConfig = {}; + + const setConfigUserObj1 = { + name: 'www.dataprovider1.com', + ext: { taxonomyname: 'iab_audience_taxonomy' }, + segment: [{ + id: '1776' + }] + }; + + const setConfigUserObj2 = { + name: 'www.dataprovider2.com', + ext: { taxonomyname: 'iab_audience_taxonomy' }, + segment: [{ + id: '1914' + }] + }; + + const setConfigSiteObj1 = { + name: 'www.dataprovider3.com', + ext: { + taxonomyname: 'iab_audience_taxonomy' + }, + segment: [ + { + id: '1812' + }, + { + id: '1955' + } + ] + } + + config.setConfig({ + ortb2: { + user: { + data: [setConfigUserObj1, setConfigUserObj2] + }, + site: { + content: { + data: [setConfigSiteObj1] + } + } + } + }); + + const rtdUserObj1 = { + name: 'www.dataprovider4.com', + ext: { + taxonomyname: 'iab_audience_taxonomy' + }, + segment: [ + { + id: '1918' + }, + { + id: '1939' + } + ] + }; + + const rtdSiteObj1 = { + name: 'www.dataprovider5.com', + ext: { + taxonomyname: 'iab_audience_taxonomy' + }, + segment: [ + { + id: '1945' + }, + { + id: '2003' + } + ] + }; + + const rtd = { + ortb2: { + user: { + data: [rtdUserObj1] + }, + site: { + content: { + data: [rtdSiteObj1] + } + } + } + }; + + addRealTimeData(bidConfig, rtd, rtdConfig); + + let ortb2Config = config.getConfig().ortb2; + + expect(ortb2Config.user.data).to.deep.include.members([setConfigUserObj1, setConfigUserObj2, rtdUserObj1]); + expect(ortb2Config.site.content.data).to.deep.include.members([setConfigSiteObj1, rtdSiteObj1]); + }); + + it('merges ortb2 data without duplication', function() { + let rtdConfig = {}; + let bidConfig = {}; + + const userObj1 = { + name: 'www.dataprovider1.com', + ext: { taxonomyname: 'iab_audience_taxonomy' }, + segment: [{ + id: '1776' + }] + }; + + const userObj2 = { + name: 'www.dataprovider2.com', + ext: { taxonomyname: 'iab_audience_taxonomy' }, + segment: [{ + id: '1914' + }] + }; + + const siteObj1 = { + name: 'www.dataprovider3.com', + ext: { + taxonomyname: 'iab_audience_taxonomy' + }, + segment: [ + { + id: '1812' + }, + { + id: '1955' + } + ] + } + + config.setConfig({ + ortb2: { + user: { + data: [userObj1, userObj2] + }, + site: { + content: { + data: [siteObj1] + } + } + } + }); + + const rtd = { + ortb2: { + user: { + data: [userObj1] + }, + site: { + content: { + data: [siteObj1] + } + } + } + }; + + addRealTimeData(bidConfig, rtd, rtdConfig); + + let ortb2Config = config.getConfig().ortb2; + + expect(ortb2Config.user.data).to.deep.include.members([userObj1, userObj2]); + expect(ortb2Config.site.content.data).to.deep.include.members([siteObj1]); + expect(ortb2Config.user.data).to.have.lengthOf(2); + expect(ortb2Config.site.content.data).to.have.lengthOf(1); + }); + + it('merges bidder-specific ortb2 data', function() { + let rtdConfig = {}; + let bidConfig = {}; + + const configUserObj1 = { + name: 'www.dataprovider1.com', + ext: { segtax: 3 }, + segment: [{ + id: '1776' + }] + }; + + const configUserObj2 = { + name: 'www.dataprovider2.com', + ext: { segtax: 3 }, + segment: [{ + id: '1914' + }] + }; + + const configUserObj3 = { + name: 'www.dataprovider1.com', + ext: { segtax: 3 }, + segment: [{ + id: '2003' + }] + }; + + const configSiteObj1 = { + name: 'www.dataprovider3.com', + ext: { + segtax: 1 + }, + segment: [ + { + id: '1812' + }, + { + id: '1955' + } + ] + }; + + const configSiteObj2 = { + name: 'www.dataprovider3.com', + ext: { + segtax: 1 + }, + segment: [ + { + id: '1812' + } + ] + }; + + config.setBidderConfig({ + bidders: ['adbuzz'], + config: { + ortb2: { + user: { + data: [configUserObj1, configUserObj2] + }, + site: { + content: { + data: [configSiteObj1] + } + } + } + } + }); + + config.setBidderConfig({ + bidders: ['pubvisage'], + config: { + ortb2: { + user: { + data: [configUserObj3] + }, + site: { + content: { + data: [configSiteObj2] + } + } + } + } + }); + + const rtdUserObj1 = { + name: 'www.dataprovider4.com', + ext: { + segtax: 501 + }, + segment: [ + { + id: '1918' + }, + { + id: '1939' + } + ] + }; + + const rtdUserObj2 = { + name: 'www.dataprovider2.com', + ext: { + segtax: 502 + }, + segment: [ + { + id: '1939' + } + ] + }; + + const rtdSiteObj1 = { + name: 'www.dataprovider5.com', + ext: { + segtax: 1 + }, + segment: [ + { + id: '441' + }, + { + id: '442' + } + ] + }; + + const rtdSiteObj2 = { + name: 'www.dataprovider6.com', + ext: { + segtax: 2 + }, + segment: [ + { + id: '676' + } + ] + }; + + const rtd = { + ortb2b: { + adbuzz: { + ortb2: { + user: { + data: [rtdUserObj1] + }, + site: { + content: { + data: [rtdSiteObj1] + } + } + } + }, + pubvisage: { + ortb2: { + user: { + data: [rtdUserObj2] + }, + site: { + content: { + data: [rtdSiteObj2] + } + } + } + } + } + }; + + addRealTimeData(bidConfig, rtd, rtdConfig); + + let ortb2Config = config.getBidderConfig().adbuzz.ortb2; + + expect(ortb2Config.user.data).to.deep.include.members([configUserObj1, configUserObj2, rtdUserObj1]); + expect(ortb2Config.site.content.data).to.deep.include.members([configSiteObj1, rtdSiteObj1]); + + ortb2Config = config.getBidderConfig().pubvisage.ortb2; + + expect(ortb2Config.user.data).to.deep.include.members([configUserObj3, rtdUserObj2]); + expect(ortb2Config.site.content.data).to.deep.include.members([configSiteObj2, rtdSiteObj2]); + }); + + it('merges bidder-specific ortb2 data without duplication', function() { + let rtdConfig = {}; + let bidConfig = {}; + + const userObj1 = { + name: 'www.dataprovider1.com', + ext: { segtax: 3 }, + segment: [{ + id: '1776' + }] + }; + + const userObj2 = { + name: 'www.dataprovider2.com', + ext: { segtax: 3 }, + segment: [{ + id: '1914' + }] + }; + + const userObj3 = { + name: 'www.dataprovider1.com', + ext: { segtax: 3 }, + segment: [{ + id: '2003' + }] + }; + + const siteObj1 = { + name: 'www.dataprovider3.com', + ext: { + segtax: 1 + }, + segment: [ + { + id: '1812' + }, + { + id: '1955' + } + ] + }; + + const siteObj2 = { + name: 'www.dataprovider3.com', + ext: { + segtax: 1 + }, + segment: [ + { + id: '1812' + } + ] + }; + + config.setBidderConfig({ + bidders: ['adbuzz'], + config: { + ortb2: { + user: { + data: [userObj1, userObj2] + }, + site: { + content: { + data: [siteObj1] + } + } + } + } + }); + + config.setBidderConfig({ + bidders: ['pubvisage'], + config: { + ortb2: { + user: { + data: [userObj3] + }, + site: { + content: { + data: [siteObj2] + } + } + } + } + }); + + const rtd = { + ortb2b: { + adbuzz: { + ortb2: { + user: { + data: [userObj1] + }, + site: { + content: { + data: [siteObj1] + } + } + } + }, + pubvisage: { + ortb2: { + user: { + data: [userObj2, userObj3] + }, + site: { + content: { + data: [siteObj1, siteObj2] + } + } + } + } + } + }; + + addRealTimeData(bidConfig, rtd, rtdConfig); + + let ortb2Config = config.getBidderConfig().adbuzz.ortb2; + + expect(ortb2Config.user.data).to.deep.include.members([userObj1]); + expect(ortb2Config.site.content.data).to.deep.include.members([siteObj1]); + + expect(ortb2Config.user.data).to.have.lengthOf(2); + expect(ortb2Config.site.content.data).to.have.lengthOf(1); + + ortb2Config = config.getBidderConfig().pubvisage.ortb2; + + expect(ortb2Config.user.data).to.deep.include.members([userObj3, userObj3]); + expect(ortb2Config.site.content.data).to.deep.include.members([siteObj1, siteObj2]); + + expect(ortb2Config.user.data).to.have.lengthOf(2); + expect(ortb2Config.site.content.data).to.have.lengthOf(2); + }); + + it('allows publisher defined rtd ortb2 logic', function() { + const rtdConfig = { + params: { + handleRtd: function(bidConfig, rtd, rtdConfig, pbConfig) { + if (rtd.ortb2.user.data[0].segment[0].id == '1776') { + pbConfig.setConfig({ortb2: rtd.ortb2}); + } else { + pbConfig.setConfig({ortb2: {}}); + } + } + } + }; + + let bidConfig = {}; + + const rtdUserObj1 = { + name: 'www.dataprovider.com', + ext: { taxonomyname: 'iab_audience_taxonomy' }, + segment: [{ + id: '1776' + }] + }; + + let rtd = { + ortb2: { + user: { + data: [rtdUserObj1] + } + } + }; + + config.resetConfig(); + + let pbConfig = config.getConfig(); + addRealTimeData(bidConfig, rtd, rtdConfig); + expect(config.getConfig().ortb2.user.data).to.deep.include.members([rtdUserObj1]); + + const rtdUserObj2 = { + name: 'www.audigent.com', + ext: { + segtax: '1', + taxprovider: '1' + }, + segment: [{ + id: 'pubseg1' + }] + }; + + rtd = { + ortb2: { + user: { + data: [rtdUserObj2] + } + } + }; + + config.resetConfig(); + + pbConfig = config.getConfig(); + addRealTimeData(bidConfig, rtd, rtdConfig); + expect(config.getConfig().ortb2).to.deep.equal({}); + }); + + it('allows publisher defined adunit logic', function() { + const rtdConfig = { + params: { + handleRtd: function(bidConfig, rtd, rtdConfig, pbConfig) { + var adUnits = bidConfig.adUnits; + for (var i = 0; i < adUnits.length; i++) { + var adUnit = adUnits[i]; + for (var j = 0; j < adUnit.bids.length; j++) { + var bid = adUnit.bids[j]; + if (bid.bidder == 'adBuzz') { + for (var k = 0; k < rtd.adBuzz.length; k++) { + bid.adBuzzData.segments.adBuzz.push(rtd.adBuzz[k]); + } + } else if (bid.bidder == 'trueBid') { + for (var k = 0; k < rtd.trueBid.length; k++) { + bid.trueBidSegments.push(rtd.trueBid[k]); + } + } + } + } + } + } + }; + + let bidConfig = { + adUnits: [ + { + bids: [ + { + bidder: 'adBuzz', + adBuzzData: { + segments: { + adBuzz: [ + { + id: 'adBuzzSeg1' + } + ] + } + } + }, + { + bidder: 'trueBid', + trueBidSegments: [] + } + ] + } + ] + }; + + const rtd = { + adBuzz: [{id: 'adBuzzSeg2'}, {id: 'adBuzzSeg3'}], + trueBid: [{id: 'truebidSeg1'}, {id: 'truebidSeg2'}, {id: 'truebidSeg3'}] + }; + + addRealTimeData(bidConfig, rtd, rtdConfig); + + expect(bidConfig.adUnits[0].bids[0].adBuzzData.segments.adBuzz[0].id).to.equal('adBuzzSeg1'); + expect(bidConfig.adUnits[0].bids[0].adBuzzData.segments.adBuzz[1].id).to.equal('adBuzzSeg2'); + expect(bidConfig.adUnits[0].bids[0].adBuzzData.segments.adBuzz[2].id).to.equal('adBuzzSeg3'); + expect(bidConfig.adUnits[0].bids[1].trueBidSegments[0].id).to.equal('truebidSeg1'); + expect(bidConfig.adUnits[0].bids[1].trueBidSegments[1].id).to.equal('truebidSeg2'); + expect(bidConfig.adUnits[0].bids[1].trueBidSegments[2].id).to.equal('truebidSeg3'); + }); + }); + + describe('Get Real-Time Data', function() { + it('gets rtd from local storage cache', function() { + const rtdConfig = { + params: { + segmentCache: true + } + }; + + const bidConfig = {}; + + const rtdUserObj1 = { + name: 'www.dataprovider3.com', + ext: { + taxonomyname: 'iab_audience_taxonomy' + }, + segment: [ + { + id: '1918' + }, + { + id: '1939' + } + ] + }; + + const cachedRtd = { + rtd: { + ortb2: { + user: { + data: [rtdUserObj1] + } + } + } + }; + + getDataFromLocalStorageStub.withArgs(RTD_LOCAL_NAME).returns(JSON.stringify(cachedRtd)); + + expect(config.getConfig().ortb2).to.be.undefined; + getRealTimeData(bidConfig, () => {}, rtdConfig, {}); + expect(config.getConfig().ortb2.user.data).to.deep.include.members([rtdUserObj1]); + }); + + it('gets real-time data via async request', function() { + const setConfigSiteObj1 = { + name: 'www.audigent.com', + ext: { + segtax: '1', + taxprovider: '1' + }, + segment: [ + { + id: 'pubseg1' + }, + { + id: 'pubseg2' + } + ] + } + + config.setConfig({ + ortb2: { + site: { + content: { + data: [setConfigSiteObj1] + } + } + } + }); + + const rtdConfig = { + params: { + segmentCache: false, + usePubHadron: true, + requestParams: { + publisherId: 'testPub1' + } + } + }; + + let bidConfig = {}; + + const rtdUserObj1 = { + name: 'www.audigent.com', + ext: { + segtax: '1', + taxprovider: '1' + }, + segment: [ + { + id: 'pubseg1' + }, + { + id: 'pubseg2' + } + ] + }; + + const data = { + rtd: { + ortb2: { + user: { + data: [rtdUserObj1] + } + } + } + }; + + getDataFromLocalStorageStub.withArgs(HALOID_LOCAL_NAME).returns('testHadronId1'); + getRealTimeData(bidConfig, () => {}, rtdConfig, {}); + + let request = server.requests[0]; + let postData = JSON.parse(request.requestBody); + expect(postData.config).to.have.deep.property('publisherId', 'testPub1'); + expect(postData.userIds).to.have.deep.property('hadronId', 'testHadronId1'); + + request.respond(200, responseHeader, JSON.stringify(data)); + + expect(config.getConfig().ortb2.user.data).to.deep.include.members([rtdUserObj1]); + }); + }); +}); diff --git a/test/spec/modules/id5AnalyticsAdapter_spec.js b/test/spec/modules/id5AnalyticsAdapter_spec.js index be5998967c9..83951c3a6e9 100644 --- a/test/spec/modules/id5AnalyticsAdapter_spec.js +++ b/test/spec/modules/id5AnalyticsAdapter_spec.js @@ -2,7 +2,7 @@ import adapterManager from '../../../src/adapterManager.js'; import id5AnalyticsAdapter from '../../../modules/id5AnalyticsAdapter.js'; import { expect } from 'chai'; import sinon from 'sinon'; -import events from '../../../src/events.js'; +import * as events from '../../../src/events.js'; import constants from '../../../src/constants.json'; import { generateUUID } from '../../../src/utils.js'; diff --git a/test/spec/modules/id5IdSystem_spec.js b/test/spec/modules/id5IdSystem_spec.js index debde20e4c0..02020fc2d3a 100644 --- a/test/spec/modules/id5IdSystem_spec.js +++ b/test/spec/modules/id5IdSystem_spec.js @@ -13,7 +13,7 @@ import { import { init, requestBidsHook, setSubmoduleRegistry, coreStorage } from 'modules/userId/index.js'; import { config } from 'src/config.js'; import { server } from 'test/mocks/xhr.js'; -import events from 'src/events.js'; +import * as events from 'src/events.js'; import CONSTANTS from 'src/constants.json'; import * as utils from 'src/utils.js'; diff --git a/test/spec/modules/idImportLibrary_spec.js b/test/spec/modules/idImportLibrary_spec.js index 699c2c43a94..9f7c0363571 100644 --- a/test/spec/modules/idImportLibrary_spec.js +++ b/test/spec/modules/idImportLibrary_spec.js @@ -3,15 +3,20 @@ import * as idImportlibrary from 'modules/idImportLibrary.js'; var expect = require('chai').expect; -describe('currency', function () { - let fakeCurrencyFileServer; +const mockMutationObserver = { + observe: () => { + return null + } +} + +describe('IdImportLibrary Tests', function () { + let fakeServer; let sandbox; let clock; - let fn = sinon.spy(); beforeEach(function () { - fakeCurrencyFileServer = sinon.fakeServer.create(); + fakeServer = sinon.fakeServer.create(); sinon.stub(utils, 'logInfo'); sinon.stub(utils, 'logError'); }); @@ -19,7 +24,7 @@ describe('currency', function () { afterEach(function () { utils.logInfo.restore(); utils.logError.restore(); - fakeCurrencyFileServer.restore(); + fakeServer.restore(); idImportlibrary.setConfig({}); }); @@ -34,28 +39,210 @@ describe('currency', function () { clock.restore(); }); - it('results when no config available', function () { + it('results when no config is set', function () { + idImportlibrary.setConfig(); + sinon.assert.called(utils.logError); + }); + it('results when config is empty', function () { idImportlibrary.setConfig({}); sinon.assert.called(utils.logError); }); - it('results with config available', function () { - idImportlibrary.setConfig({ 'url': 'URL' }); + it('results with config available with url and debounce', function () { + idImportlibrary.setConfig({ 'url': 'URL', 'debounce': 0 }); sinon.assert.called(utils.logInfo); }); + it('results with config debounce ', function () { + let config = { 'url': 'URL', 'debounce': 300 } + idImportlibrary.setConfig(config); + expect(config.debounce).to.be.equal(300); + }); + it('results with config default debounce ', function () { let config = { 'url': 'URL' } idImportlibrary.setConfig(config); expect(config.debounce).to.be.equal(250); }); it('results with config default fullscan ', function () { - let config = { 'url': 'URL' } + let config = { 'url': 'URL', 'debounce': 0 } idImportlibrary.setConfig(config); expect(config.fullscan).to.be.equal(false); }); it('results with config fullscan ', function () { - let config = { 'url': 'URL', 'fullscan': true } + let config = { 'url': 'URL', 'fullscan': true, 'debounce': 0 } + idImportlibrary.setConfig(config); + expect(config.fullscan).to.be.equal(true); + expect(config.inputscan).to.be.equal(false); + }); + it('results with config inputscan ', function () { + let config = { 'inputscan': true, 'debounce': 0 } + idImportlibrary.setConfig(config); + expect(config.inputscan).to.be.equal(true); + }); + }); + describe('Test with email is found', function () { + let mutationObserverStub; + let userId; + let refreshUserIdSpy; + beforeEach(function() { + let sandbox = sinon.createSandbox(); + refreshUserIdSpy = sinon.spy(window.$$PREBID_GLOBAL$$, 'refreshUserIds'); + clock = sinon.useFakeTimers(1046952000000); // 2003-03-06T12:00:00Z + mutationObserverStub = sinon.stub(window, 'MutationObserver').returns(mockMutationObserver); + userId = sandbox.stub(window.$$PREBID_GLOBAL$$, 'getUserIds').returns({id: {'MOCKID': '1111'}}); + fakeServer.respondWith('POST', 'URL', [200, + { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*' + }, + '' + ]); + }); + afterEach(function () { + sandbox.restore(); + clock.restore(); + userId.restore(); + refreshUserIdSpy.restore(); + mutationObserverStub.restore(); + document.body.innerHTML = ''; + }); + + it('results with config fullscan with email found in html ', function () { + document.body.innerHTML = '
test@test.com
'; + let config = { 'url': 'URL', 'fullscan': true, 'debounce': 0 } + idImportlibrary.setConfig(config); + expect(config.fullscan).to.be.equal(true); + expect(config.inputscan).to.be.equal(false); + expect(refreshUserIdSpy.calledOnce).to.equal(true); + }); + + it('results with config fullscan with no email found in html ', function () { + document.body.innerHTML = '
test
'; + let config = { 'url': 'URL', 'fullscan': true, 'debounce': 0 } + idImportlibrary.setConfig(config); + expect(config.fullscan).to.be.equal(true); + expect(config.inputscan).to.be.equal(false); + expect(refreshUserIdSpy.calledOnce).to.equal(false); + }); + + it('results with config formElementId without listner ', function () { + let config = { url: 'testUrl', 'formElementId': 'userid', 'debounce': 0 } + document.body.innerHTML = ''; + idImportlibrary.setConfig(config); + expect(config.formElementId).to.be.equal('userid'); + expect(refreshUserIdSpy.calledOnce).to.equal(true); + }); + + it('results with config formElementId with listner ', function () { + let config = { url: 'testUrl', 'formElementId': 'userid', 'debounce': 0 } + document.body.innerHTML = ''; + idImportlibrary.setConfig(config); + expect(config.formElementId).to.be.equal('userid'); + expect(refreshUserIdSpy.calledOnce).to.equal(false); + }); + + it('results with config target without listner ', function () { + let config = { url: 'testUrl', 'target': 'userid', 'debounce': 0 } + document.body.innerHTML = '
test@test.com
'; + idImportlibrary.setConfig(config); + expect(config.target).to.be.equal('userid'); + expect(refreshUserIdSpy.calledOnce).to.equal(true); + }); + it('results with config target with listner ', function () { + let config = { url: 'testUrl', 'target': 'userid', 'debounce': 0 } + document.body.innerHTML = '
'; + idImportlibrary.setConfig(config); + + expect(config.target).to.be.equal('userid'); + expect(refreshUserIdSpy.calledOnce).to.equal(false); + }); + + it('results with config target with listner', function () { + let config = { url: 'testUrl', 'target': 'userid', 'debounce': 0 } + idImportlibrary.setConfig(config); + document.body.innerHTML = '
test@test.com
'; + expect(config.target).to.be.equal('userid'); + expect(refreshUserIdSpy.calledOnce).to.equal(false); + }); + it('results with config fullscan ', function () { + let config = { url: 'testUrl', 'fullscan': true, 'debounce': 0 } idImportlibrary.setConfig(config); + document.body.innerHTML = '
'; expect(config.fullscan).to.be.equal(true); + expect(refreshUserIdSpy.calledOnce).to.equal(false); + }); + it('results with config inputscan with listner', function () { + let config = { url: 'testUrl', 'inputscan': true, 'debounce': 0 } + var input = document.createElement('input'); + input.setAttribute('type', 'text'); + document.body.appendChild(input); + idImportlibrary.setConfig(config); + expect(config.inputscan).to.be.equal(true); + input.setAttribute('value', 'text@text.com'); + const inputEvent = new InputEvent('blur'); + input.dispatchEvent(inputEvent); + expect(refreshUserIdSpy.calledOnce).to.equal(true); + }); + + it('results with config inputscan with listner and no user ids ', function () { + let config = { 'url': 'testUrl', 'inputscan': true, 'debounce': 0 } + document.body.innerHTML = ''; + idImportlibrary.setConfig(config); + expect(config.inputscan).to.be.equal(true); + expect(refreshUserIdSpy.calledOnce).to.equal(false); + }); + + it('results with config inputscan with listner ', function () { + let config = { 'url': 'testUrl', 'inputscan': true, 'debounce': 0 } + document.body.innerHTML = ''; + idImportlibrary.setConfig(config); + expect(config.inputscan).to.be.equal(true); + expect(refreshUserIdSpy.calledOnce).to.equal(false); + }); + + it('results with config inputscan without listner ', function () { + let config = { 'url': 'testUrl', 'inputscan': true, 'debounce': 0 } + document.body.innerHTML = ''; + idImportlibrary.setConfig(config); + expect(config.inputscan).to.be.equal(true); + expect(refreshUserIdSpy.calledOnce).to.equal(true); + }); + }); + describe('Tests with no user ids', function () { + let mutationObserverStub; + let userId; + let jsonSpy; + beforeEach(function() { + let sandbox = sinon.createSandbox(); + clock = sinon.useFakeTimers(1046952000000); // 2003-03-06T12:00:00Z + mutationObserverStub = sinon.stub(window, 'MutationObserver'); + jsonSpy = sinon.spy(JSON, 'stringify'); + fakeServer.respondWith('POST', 'URL', [200, + { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*' + }, + '' + ]); + }); + afterEach(function () { + sandbox.restore(); + clock.restore(); + jsonSpy.restore(); + mutationObserverStub.restore(); + }); + it('results with config inputscan without listner with no user ids ', function () { + let config = { 'url': 'testUrl', 'inputscan': true, 'debounce': 0 } + document.body.innerHTML = ''; + idImportlibrary.setConfig(config); + expect(config.inputscan).to.be.equal(true); + expect(jsonSpy.calledOnce).to.equal(false); + }); + it('results with config inputscan without listner with no user ids ', function () { + let config = { 'url': 'testUrl', 'inputscan': true, 'debounce': 0 } + document.body.innerHTML = ''; + idImportlibrary.setConfig(config); + expect(config.inputscan).to.be.equal(true); + expect(jsonSpy.calledOnce).to.equal(false); }); }); }); diff --git a/test/spec/modules/idWardRtdProvider_spec.js b/test/spec/modules/idWardRtdProvider_spec.js new file mode 100644 index 00000000000..949365baec6 --- /dev/null +++ b/test/spec/modules/idWardRtdProvider_spec.js @@ -0,0 +1,113 @@ +import {config} from 'src/config.js'; +import {getRealTimeData, idWardRtdSubmodule, storage} from 'modules/idWardRtdProvider.js'; + +describe('idWardRtdProvider', function() { + let getDataFromLocalStorageStub; + + const testReqBidsConfigObj = { + adUnits: [ + { + bids: ['bid1', 'bid2'] + } + ] + }; + + const onDone = function() { return true }; + + const cmoduleConfig = { + 'name': 'idWard', + 'params': { + 'cohortStorageKey': 'cohort_ids' + } + } + + beforeEach(function() { + config.resetConfig(); + getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage') + }); + + afterEach(function () { + getDataFromLocalStorageStub.restore(); + }); + + describe('idWardRtdSubmodule', function() { + it('successfully instantiates', function () { + expect(idWardRtdSubmodule.init()).to.equal(true); + }); + }); + + describe('Get Real-Time Data', function() { + it('gets rtd from local storage', function() { + const rtdConfig = { + params: { + cohortStorageKey: 'cohort_ids', + segtax: 503 + } + }; + + const bidConfig = {}; + + const rtdUserObj1 = { + name: 'id-ward.com', + ext: { + segtax: 503 + }, + segment: [ + { + id: 'TCZPQOWPEJG3MJOTUQUF793A' + }, + { + id: '93SUG3H540WBJMYNT03KX8N3' + } + ] + }; + + getDataFromLocalStorageStub.withArgs('cohort_ids') + .returns(JSON.stringify(['TCZPQOWPEJG3MJOTUQUF793A', '93SUG3H540WBJMYNT03KX8N3'])); + + expect(config.getConfig().ortb2).to.be.undefined; + getRealTimeData(bidConfig, () => {}, rtdConfig, {}); + expect(config.getConfig().ortb2.user.data).to.deep.include.members([rtdUserObj1]); + }); + + it('do not set rtd if local storage empty', function() { + const rtdConfig = { + params: { + cohortStorageKey: 'cohort_ids', + segtax: 503 + } + }; + + const bidConfig = {}; + + getDataFromLocalStorageStub.withArgs('cohort_ids') + .returns(null); + + expect(config.getConfig().ortb2).to.be.undefined; + getRealTimeData(bidConfig, () => {}, rtdConfig, {}); + expect(config.getConfig().ortb2).to.be.undefined; + }); + + it('do not set rtd if local storage has incorrect value', function() { + const rtdConfig = { + params: { + cohortStorageKey: 'cohort_ids', + segtax: 503 + } + }; + + const bidConfig = {}; + + getDataFromLocalStorageStub.withArgs('cohort_ids') + .returns('wrong cohort ids value'); + + expect(config.getConfig().ortb2).to.be.undefined; + getRealTimeData(bidConfig, () => {}, rtdConfig, {}); + expect(config.getConfig().ortb2).to.be.undefined; + }); + + it('should initalise and return with config', function () { + expect(getRealTimeData(testReqBidsConfigObj, onDone, cmoduleConfig)).to.equal(undefined) + }); + }); +}); diff --git a/test/spec/modules/idxIdSystem_spec.js b/test/spec/modules/idxIdSystem_spec.js index 14cd9a88d13..5cecc12c253 100644 --- a/test/spec/modules/idxIdSystem_spec.js +++ b/test/spec/modules/idxIdSystem_spec.js @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import find from 'core-js-pure/features/array/find.js'; +import {find} from 'src/polyfill.js'; import { config } from 'src/config.js'; import { init, requestBidsHook, setSubmoduleRegistry } from 'modules/userId/index.js'; import { storage, idxIdSubmodule } from 'modules/idxIdSystem.js'; diff --git a/test/spec/modules/imRtdProvider_spec.js b/test/spec/modules/imRtdProvider_spec.js index e3365e8eaf2..6d92440a144 100644 --- a/test/spec/modules/imRtdProvider_spec.js +++ b/test/spec/modules/imRtdProvider_spec.js @@ -51,7 +51,8 @@ describe('imRtdProvider', function () { describe('getBidderFunction', function () { const assumedBidder = [ 'ix', - 'pubmatic' + 'pubmatic', + 'fluct' ]; assumedBidder.forEach(bidderName => { it(`should return bidderFunction with assumed bidder: ${bidderName}`, function () { @@ -70,6 +71,34 @@ describe('imRtdProvider', function () { it(`should return null with unexpected bidder`, function () { expect(getBidderFunction('test')).to.equal(null); }); + describe('fluct bidder function', function () { + it('should return a bid w/o im_segments if not any exists', function () { + const bid = {bidder: 'fluct'}; + expect(getBidderFunction('fluct')(bid, '')).to.eql(bid); + }); + it('should return a bid w/ im_segments if any exists', function () { + const bid = { + bidder: 'fluct', + params: { + kv: { + foo: ['foo1'] + } + } + }; + expect(getBidderFunction('fluct')(bid, {im_segments: ['12345', '67890']})) + .to.eql( + { + bidder: 'fluct', + params: { + kv: { + foo: ['foo1'], + imsids: ['12345', '67890'] + } + } + } + ); + }); + }); }) describe('getCustomBidderFunction', function () { diff --git a/test/spec/modules/improvedigitalBidAdapter_spec.js b/test/spec/modules/improvedigitalBidAdapter_spec.js index 095e50f0c66..64c276ce806 100644 --- a/test/spec/modules/improvedigitalBidAdapter_spec.js +++ b/test/spec/modules/improvedigitalBidAdapter_spec.js @@ -92,7 +92,8 @@ describe('Improve Digital Adapter Tests', function () { gdprConsent: { consentString: 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==', vendorData: {}, - gdprApplies: true + gdprApplies: true, + addtlConsent: '1~1.35.41.101', }, }; @@ -278,6 +279,7 @@ describe('Improve Digital Adapter Tests', function () { const request = spec.buildRequests([bidRequest], bidderRequestGdpr)[0]; const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); expect(params.bid_request.gdpr).to.equal('BOJ/P2HOJ/P2HABABMAAAAAZ+A=='); + expect(params.bid_request.user.ext.consented_providers_settings.consented_providers).to.exist.and.to.deep.equal([1, 35, 41, 101]); }); it('should add CCPA consent string', function () { @@ -513,6 +515,67 @@ describe('Improve Digital Adapter Tests', function () { }); getConfigStub.restore(); }); + + it('should set pagecat and genre âžž fpd:ortb2.site', function() { + config.setConfig(JSON.parse('{"ortb2":{"site":{"cat":["IAB2"],"pagecat":["IAB2-2"],"content":{"genre":"Adventure"}}}}')); + const bidRequest = Object.assign({}, simpleBidRequest); + const request = spec.buildRequests([bidRequest], bidderRequestReferrer)[0]; + const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); + expect(params.bid_request.pagecat).to.be.an('array'); + expect(params.bid_request.pagecat).to.deep.equal(['IAB2-2']); + expect(params.bid_request.genre).to.be.a('string'); + expect(params.bid_request.genre).be.equal('Adventure'); + }); + + it('should not set pagecat and genre when malformed data provided âžž fpd:ortb2.site', function() { + config.setConfig(JSON.parse('{"ortb2":{"site":{"pagecat":"IAB2-2","content":{"genre":["Adventure"]}}}}')); + const bidRequest = Object.assign({}, simpleBidRequest); + const request = spec.buildRequests([bidRequest], bidderRequestReferrer)[0]; + const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); + expect(params.bid_request.pagecat).does.not.exist; + expect(params.bid_request.genre).does.not.exist; + }); + + it('should use cat when pagecat not available âžž fpd:ortb2.site', function() { + config.setConfig(JSON.parse('{"ortb2":{"site":{"cat":["IAB2"]}}}')); + const bidRequest = Object.assign({}, simpleBidRequest); + const request = spec.buildRequests([bidRequest], bidderRequestReferrer)[0]; + const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); + expect(params.bid_request.pagecat).to.be.an('array'); + expect(params.bid_request.pagecat).to.deep.equal(['IAB2']); + }); + + it('should format pagecat correctly âžž fpd:ortb2.site', function() { + config.setConfig(JSON.parse('{"ortb2":{"site":{"cat":["IAB2", ["IAB-1"], "IAB3", 123, ""]}}}')); + const bidRequest = Object.assign({}, simpleBidRequest); + const request = spec.buildRequests([bidRequest], bidderRequestReferrer)[0]; + const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); + expect(params.bid_request.pagecat).to.be.an('array'); + expect(params.bid_request.pagecat).to.deep.equal([ + 'IAB2', + 'IAB3' + ] + ); + }); + + it('should set coppa', function() { + sinon.stub(config, 'getConfig') + .withArgs('coppa') + .returns(true); + const bidRequest = Object.assign({}, simpleBidRequest); + const request = spec.buildRequests([bidRequest], bidderRequestReferrer)[0]; + const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); + expect(params.bid_request.coppa).to.equal(1); + + config.getConfig.restore(); + }); + + it('should undefined coppa', function() { + const bidRequest = Object.assign({}, simpleBidRequest); + const request = spec.buildRequests([bidRequest], bidderRequestReferrer)[0]; + const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); + expect(params.bid_request.coppa).to.equal(undefined); + }); }); const serverResponse = { @@ -542,6 +605,33 @@ describe('Improve Digital Adapter Tests', function () { } }; + const serverResponseRazr = { + 'body': { + 'id': '687a06c541d8d1', + 'site_id': 191642, + 'bid': [ + { + 'isNet': false, + 'id': '33e9500b21129f', + 'advid': '5279', + 'price': 1.45888594164456, + 'nurl': 'https://ice.360yield.com/imp_pixel?ic=wVmhKI07hCVyGC1sNdFp.6buOSiGYOw8jPyZLlcMY2RCwD4ek3Fy6.xUI7U002skGBs3objMBoNU-Frpvmb9js3NKIG0YZJgWaNdcpXY9gOXE9hY4-wxybCjVSNzhOQB-zic73hzcnJnKeoGgcfvt8fMy18-yD0aVdYWt4zbqdoITOkKNCPBEgbPFu1rcje-o7a64yZ7H3dKvtnIixXQYc1Ep86xGSBGXY6xW2KfUOMT6vnkemxO72divMkMdhR8cAuqIubbx-ZID8-xf5c9k7p6DseeBW0I8ionrlTHx.rGosgxhiFaMqtr7HiA7PBzKvPdeEYN0hQ8RYo8JzYL82hA91A3V2m9Ij6y0DfIJnnrKN8YORffhxmJ6DzwEl1zjrVFbD01bqB3Vdww8w8PQJSkKQkd313tr-atU8LS26fnBmOngEkVHwAr2WCKxuUvxHmuVBTA-Lgz7wKwMoOJCA3hFxMavVb0ZFB7CK0BUTVU6z0De92Q.FJKNCHLMbjX3vcAQ90=', + 'h': 290, + 'pid': 1053688, + 'sync': [ + 'https://link1', + 'https://link2' + ], + 'crid': '422031', + 'w': 600, + 'cid': '99006', + 'adm': 'document.writeln("' + }, + 'rtb': { + 'banner': { + 'content': '', + 'width': 300, + 'height': 250 + }, + 'trackers': [ + { + 'impression_urls': [ + 'https://lax1-ib.adnxs.com/impression', + 'https://www.test.com/tracker' + ], + 'video_events': {} + } + ] + } + } + ] + } + ] + }; + + it('should get correct bid response', function () { + let expectedResponse = [ + { + 'requestId': '3db3773286ee59', + 'cpm': 0.5, + 'creativeId': 29681110, + 'dealId': undefined, + 'width': 300, + 'height': 250, + 'ad': '', + 'mediaType': 'banner', + 'currency': 'USD', + 'ttl': 300, + 'netRevenue': true, + 'adUnitCode': 'code', + 'mediafuse': { + 'buyerMemberId': 958 + }, + 'meta': { + 'dchain': { + 'ver': '1.0', + 'complete': 0, + 'nodes': [{ + 'bsid': '958' + }] + } + } + } + ]; + let bidderRequest = { + bids: [{ + bidId: '3db3773286ee59', + adUnitCode: 'code' + }] + }; + let result = spec.interpretResponse({ body: response }, {bidderRequest}); + expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); + }); + + it('should reject 0 cpm bids', function () { + let zeroCpmResponse = deepClone(response); + zeroCpmResponse.tags[0].ads[0].cpm = 0; + + let bidderRequest = { + bidderCode: 'mediafuse' + }; + + let result = spec.interpretResponse({ body: zeroCpmResponse }, { bidderRequest }); + expect(result.length).to.equal(0); + }); + + it('should allow 0 cpm bids if allowZeroCpmBids setConfig is true', function () { + $$PREBID_GLOBAL$$.bidderSettings = { + mediafuse: { + allowZeroCpmBids: true + } + }; + + let zeroCpmResponse = deepClone(response); + zeroCpmResponse.tags[0].ads[0].cpm = 0; + + let bidderRequest = { + bidderCode: 'mediafuse', + bids: [{ + bidId: '3db3773286ee59', + adUnitCode: 'code' + }] + }; + + let result = spec.interpretResponse({ body: zeroCpmResponse }, { bidderRequest }); + expect(result.length).to.equal(1); + expect(result[0].cpm).to.equal(0); + }); + + it('handles nobid responses', function () { + let response = { + 'version': '0.0.1', + 'tags': [{ + 'uuid': '84ab500420319d', + 'tag_id': 5976557, + 'auction_id': '297492697822162468', + 'nobid': true + }] + }; + let bidderRequest; + + let result = spec.interpretResponse({ body: response }, {bidderRequest}); + expect(result.length).to.equal(0); + }); + + it('handles outstream video responses', function () { + let response = { + 'tags': [{ + 'uuid': '84ab500420319d', + 'ads': [{ + 'ad_type': 'video', + 'cpm': 0.500000, + 'notify_url': 'imptracker.com', + 'rtb': { + 'video': { + 'content': '' + } + }, + 'javascriptTrackers': '' + }] + }] + }; + let bidderRequest = { + bids: [{ + bidId: '84ab500420319d', + adUnitCode: 'code', + mediaTypes: { + video: { + context: 'outstream' + } + } + }] + } + + let result = spec.interpretResponse({ body: response }, {bidderRequest}); + expect(result[0]).to.have.property('vastXml'); + expect(result[0]).to.have.property('vastImpUrl'); + expect(result[0]).to.have.property('mediaType', 'video'); + }); + + it('handles instream video responses', function () { + let response = { + 'tags': [{ + 'uuid': '84ab500420319d', + 'ads': [{ + 'ad_type': 'video', + 'cpm': 0.500000, + 'notify_url': 'imptracker.com', + 'rtb': { + 'video': { + 'asset_url': 'https://sample.vastURL.com/here/vid' + } + }, + 'javascriptTrackers': '' + }] + }] + }; + let bidderRequest = { + bids: [{ + bidId: '84ab500420319d', + adUnitCode: 'code', + mediaTypes: { + video: { + context: 'instream' + } + } + }] + } + + let result = spec.interpretResponse({ body: response }, {bidderRequest}); + expect(result[0]).to.have.property('vastUrl'); + expect(result[0]).to.have.property('vastImpUrl'); + expect(result[0]).to.have.property('mediaType', 'video'); + }); + + it('handles adpod responses', function () { + let response = { + 'tags': [{ + 'uuid': '84ab500420319d', + 'ads': [{ + 'ad_type': 'video', + 'brand_category_id': 10, + 'cpm': 0.500000, + 'notify_url': 'imptracker.com', + 'rtb': { + 'video': { + 'asset_url': 'https://sample.vastURL.com/here/adpod', + 'duration_ms': 30000, + } + }, + 'viewability': { + 'config': '' + } + }] + }] + }; + + let bidderRequest = { + bids: [{ + bidId: '84ab500420319d', + adUnitCode: 'code', + mediaTypes: { + video: { + context: 'adpod' + } + } + }] + }; + bfStub.returns('1'); + + let result = spec.interpretResponse({ body: response }, {bidderRequest}); + expect(result[0]).to.have.property('vastUrl'); + expect(result[0].video.context).to.equal('adpod'); + expect(result[0].video.durationSeconds).to.equal(30); + }); + + it('handles native responses', function () { + let response1 = deepClone(response); + response1.tags[0].ads[0].ad_type = 'native'; + response1.tags[0].ads[0].rtb.native = { + 'title': 'Native Creative', + 'desc': 'Cool description great stuff', + 'desc2': 'Additional body text', + 'ctatext': 'Do it', + 'sponsored': 'MediaFuse', + 'icon': { + 'width': 0, + 'height': 0, + 'url': 'https://cdn.adnxs.com/icon.png' + }, + 'main_img': { + 'width': 2352, + 'height': 1516, + 'url': 'https://cdn.adnxs.com/img.png' + }, + 'link': { + 'url': 'https://www.mediafuse.com', + 'fallback_url': '', + 'click_trackers': ['https://nym1-ib.adnxs.com/click'] + }, + 'impression_trackers': ['https://example.com'], + 'rating': '5', + 'displayurl': 'https://mediafuse.com/?url=display_url', + 'likes': '38908320', + 'downloads': '874983', + 'price': '9.99', + 'saleprice': 'FREE', + 'phone': '1234567890', + 'address': '28 W 23rd St, New York, NY 10010', + 'privacy_link': 'https://www.mediafuse.com/privacy-policy-agreement/', + 'javascriptTrackers': '' + }; + let bidderRequest = { + bids: [{ + bidId: '3db3773286ee59', + adUnitCode: 'code' + }] + } + + let result = spec.interpretResponse({ body: response1 }, {bidderRequest}); + expect(result[0].native.title).to.equal('Native Creative'); + expect(result[0].native.body).to.equal('Cool description great stuff'); + expect(result[0].native.cta).to.equal('Do it'); + expect(result[0].native.image.url).to.equal('https://cdn.adnxs.com/img.png'); + }); + + it('supports configuring outstream renderers', function () { + const outstreamResponse = deepClone(response); + outstreamResponse.tags[0].ads[0].rtb.video = {}; + outstreamResponse.tags[0].ads[0].renderer_url = 'renderer.js'; + + const bidderRequest = { + bids: [{ + bidId: '3db3773286ee59', + renderer: { + options: { + adText: 'configured' + } + }, + mediaTypes: { + video: { + context: 'outstream' + } + } + }] + }; + + const result = spec.interpretResponse({ body: outstreamResponse }, {bidderRequest}); + expect(result[0].renderer.config).to.deep.equal( + bidderRequest.bids[0].renderer.options + ); + }); + + it('should add deal_priority and deal_code', function() { + let responseWithDeal = deepClone(response); + responseWithDeal.tags[0].ads[0].ad_type = 'video'; + responseWithDeal.tags[0].ads[0].deal_priority = 5; + responseWithDeal.tags[0].ads[0].deal_code = '123'; + responseWithDeal.tags[0].ads[0].rtb.video = { + duration_ms: 1500, + player_width: 640, + player_height: 340, + }; + + let bidderRequest = { + bids: [{ + bidId: '3db3773286ee59', + adUnitCode: 'code', + mediaTypes: { + video: { + context: 'adpod' + } + } + }] + } + let result = spec.interpretResponse({ body: responseWithDeal }, {bidderRequest}); + expect(Object.keys(result[0].mediafuse)).to.include.members(['buyerMemberId', 'dealPriority', 'dealCode']); + expect(result[0].video.dealTier).to.equal(5); + }); + + it('should add advertiser id', function() { + let responseAdvertiserId = deepClone(response); + responseAdvertiserId.tags[0].ads[0].advertiser_id = '123'; + + let bidderRequest = { + bids: [{ + bidId: '3db3773286ee59', + adUnitCode: 'code' + }] + } + let result = spec.interpretResponse({ body: responseAdvertiserId }, {bidderRequest}); + expect(Object.keys(result[0].meta)).to.include.members(['advertiserId']); + }); + + it('should add brand id', function() { + let responseBrandId = deepClone(response); + responseBrandId.tags[0].ads[0].brand_id = 123; + + let bidderRequest = { + bids: [{ + bidId: '3db3773286ee59', + adUnitCode: 'code' + }] + } + let result = spec.interpretResponse({ body: responseBrandId }, {bidderRequest}); + expect(Object.keys(result[0].meta)).to.include.members(['brandId']); + }); + + it('should add advertiserDomains', function() { + let responseAdvertiserId = deepClone(response); + responseAdvertiserId.tags[0].ads[0].adomain = ['123']; + + let bidderRequest = { + bids: [{ + bidId: '3db3773286ee59', + adUnitCode: 'code' + }] + } + let result = spec.interpretResponse({ body: responseAdvertiserId }, {bidderRequest}); + expect(Object.keys(result[0].meta)).to.include.members(['advertiserDomains']); + expect(Object.keys(result[0].meta.advertiserDomains)).to.deep.equal([]); + }); + }); +}); diff --git a/test/spec/modules/mediakeysBidAdapter_spec.js b/test/spec/modules/mediakeysBidAdapter_spec.js index 602524e6eb3..38f2a3f9de3 100644 --- a/test/spec/modules/mediakeysBidAdapter_spec.js +++ b/test/spec/modules/mediakeysBidAdapter_spec.js @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import find from 'core-js-pure/features/array/find.js'; +import {find} from 'src/polyfill.js'; import { spec } from 'modules/mediakeysBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; import * as utils from 'src/utils.js'; @@ -575,12 +575,15 @@ describe('mediakeysBidAdapter', function () { }, user: { yob: 1985, - gender: 'm' - }, - device: { + gender: 'm', geo: { country: 'FR', city: 'Marseille' + }, + ext: { + data: { + registered: true + } } } } @@ -596,8 +599,9 @@ describe('mediakeysBidAdapter', function () { expect(data.site.ext.data.category).to.equal('sport'); expect(data.user.yob).to.equal(1985); expect(data.user.gender).to.equal('m'); - expect(data.device.geo.country).to.equal('FR'); - expect(data.device.geo.city).to.equal('Marseille'); + expect(data.user.geo.country).to.equal('FR'); + expect(data.user.geo.city).to.equal('Marseille'); + expect(data.user.ext.data.registered).to.be.true; }); }); diff --git a/test/spec/modules/medianetAnalyticsAdapter_spec.js b/test/spec/modules/medianetAnalyticsAdapter_spec.js index a0a62710a56..0ce26ab4863 100644 --- a/test/spec/modules/medianetAnalyticsAdapter_spec.js +++ b/test/spec/modules/medianetAnalyticsAdapter_spec.js @@ -2,7 +2,7 @@ import { expect } from 'chai'; import medianetAnalytics from 'modules/medianetAnalyticsAdapter.js'; import * as utils from 'src/utils.js'; import CONSTANTS from 'src/constants.json'; -import events from 'src/events.js'; +import * as events from 'src/events.js'; const { EVENTS: { AUCTION_INIT, BID_REQUESTED, BID_RESPONSE, NO_BID, BID_TIMEOUT, AUCTION_END, SET_TARGETING, BID_WON } @@ -23,7 +23,9 @@ const MOCK = { NO_BID_SET_TARGETING: {'div-gpt-ad-1460505748561-0': {}}, BID_WON: {'bidderCode': 'medianet', 'width': 300, 'height': 250, 'statusMessage': 'Bid available', 'adId': '3e6e4bce5c8fb3', 'requestId': '28248b0e6aece2', 'mediaType': 'banner', 'source': 'client', 'no_bid': false, 'cpm': 2.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 1.1495, 'originalCurrency': 'USD', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 266, 'pbLg': '2.00', 'pbMg': '2.20', 'pbHg': '2.29', 'pbAg': '2.25', 'pbDg': '2.29', 'pbCg': '2.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'medianet', 'hb_adid': '3e6e4bce5c8fb3', 'hb_pb': '2.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'status': 'rendered', 'params': [{'cid': 'test123', 'crid': '451466393'}]}, NO_BID: {'bidder': 'medianet', 'params': {'cid': 'test123', 'crid': '451466393', 'site': {}}, 'mediaTypes': {'banner': {'sizes': [[300, 250]], 'ext': ['asdads']}}, 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'transactionId': '303fa0c6-682f-4aea-8e4a-dc68f0d5c7d5', 'sizes': [[300, 250], [300, 600]], 'bidId': '28248b0e6aece2', 'bidderRequestId': '13fccf3809fe43', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'src': 'client'}, - BID_TIMEOUT: [{'bidId': '28248b0e6aece2', 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'params': [{'cid': 'test123', 'crid': '451466393', 'site': {}}, {'cid': '8CUX0H51P', 'crid': '451466393', 'site': {}}], 'timeout': 6}] + BID_TIMEOUT: [{'bidId': '28248b0e6aece2', 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'params': [{'cid': 'test123', 'crid': '451466393', 'site': {}}, {'cid': '8CUX0H51P', 'crid': '451466393', 'site': {}}], 'timeout': 6}], + BIDS_SAME_REQ_DIFF_CPM: [{'bidderCode': 'medianet', 'width': 300, 'height': 250, 'adId': '3e6e4bce5c8fgg', 'requestId': '28248b0e6aece2', 'mediaType': 'banner', 'source': 'client', 'ext': {'pvid': 123, 'crid': '321'}, 'no_bid': false, 'cpm': 2.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 1.1495, 'originalCurrency': 'USD', 'floorData': {'floorValue': 1.10, 'floorRule': 'banner'}, 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 266, 'pbLg': '2.00', 'pbMg': '2.20', 'pbHg': '2.29', 'pbAg': '2.25', 'pbDg': '2.29', 'pbCg': '2.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'medianet', 'hb_adid': '3e6e4bce5c8fb3', 'hb_pb': '2.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'status': 'rendered', 'params': [{'cid': 'test123', 'crid': '451466393'}]}, {'bidderCode': 'medianet', 'width': 300, 'height': 250, 'adId': '3e6e4bce5c8fb4', 'requestId': '28248b0e6aece2', 'mediaType': 'banner', 'source': 'client', 'ext': {'pvid': 123, 'crid': '321'}, 'no_bid': false, 'cpm': 1.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 1.1495, 'originalCurrency': 'USD', 'floorData': {'floorValue': 1.10, 'floorRule': 'banner'}, 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 278, 'pbLg': '1.00', 'pbMg': '1.20', 'pbHg': '1.29', 'pbAg': '1.25', 'pbDg': '1.29', 'pbCg': '1.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'medianet', 'hb_adid': '3e6e4bce5c8fb3', 'hb_pb': '1.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'status': 'rendered', 'params': [{'cid': 'test123', 'crid': '451466393'}]}], + BIDS_SAME_REQ_EQUAL_CPM: [{'bidderCode': 'medianet', 'width': 300, 'height': 250, 'adId': '3e6e4bce5c8fgg', 'requestId': '28248b0e6aece2', 'mediaType': 'banner', 'source': 'client', 'ext': {'pvid': 123, 'crid': '321'}, 'no_bid': false, 'cpm': 2.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 1.1495, 'originalCurrency': 'USD', 'floorData': {'floorValue': 1.1, 'floorRule': 'banner'}, 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 266, 'pbLg': '2.00', 'pbMg': '2.20', 'pbHg': '2.29', 'pbAg': '2.25', 'pbDg': '2.29', 'pbCg': '2.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'medianet', 'hb_adid': '3e6e4bce5c8fb3', 'hb_pb': '2.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'status': 'rendered', 'params': [{'cid': 'test123', 'crid': '451466393'}]}, {'bidderCode': 'medianet', 'width': 300, 'height': 250, 'adId': '3e6e4bce5c8fb4', 'requestId': '28248b0e6aece2', 'mediaType': 'banner', 'source': 'client', 'ext': {'pvid': 123, 'crid': '321'}, 'no_bid': false, 'cpm': 2.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 1.1495, 'originalCurrency': 'USD', 'floorData': {'floorValue': 1.1, 'floorRule': 'banner'}, 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 286, 'pbLg': '2.00', 'pbMg': '2.20', 'pbHg': '2.29', 'pbAg': '2.25', 'pbDg': '2.29', 'pbCg': '2.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'medianet', 'hb_adid': '3e6e4bce5c8fb3', 'hb_pb': '2.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'status': 'rendered', 'params': [{'cid': 'test123', 'crid': '451466393'}]}] } function performAuctionWithFloorConfig() { @@ -76,6 +78,15 @@ function performStandardAuctionWithTimeout() { events.emit(SET_TARGETING, MOCK.NO_BID_SET_TARGETING); } +function performStandardAuctionMultiBidWithSameRequestId(bidRespArray) { + events.emit(AUCTION_INIT, Object.assign({}, MOCK.AUCTION_INIT, {adUnits: MOCK.Ad_Units})); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + bidRespArray.forEach(bidResp => events.emit(BID_RESPONSE, bidResp)); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON); +} + function getQueryData(url, decode = false) { const queryArgs = url.split('?')[1].split('&'); return queryArgs.reduce((data, arg) => { @@ -265,5 +276,29 @@ describe('Media.net Analytics Adapter', function() { expect(timeoutLog.mpvid).to.have.ordered.members(['', '']); expect(timeoutLog.crid).to.have.ordered.members(['', '451466393']); }); + + it('should pick winning bid if multibids with same request id', function() { + performStandardAuctionMultiBidWithSameRequestId(MOCK.BIDS_SAME_REQ_DIFF_CPM); + let winningBid = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log)).filter(log => log.winner)[0]; + expect(winningBid.adid).equals('3e6e4bce5c8fgg'); + medianetAnalytics.clearlogsQueue(); + + const reversedResponseArray = [].concat(MOCK.BIDS_SAME_REQ_DIFF_CPM).reverse(); + performStandardAuctionMultiBidWithSameRequestId(reversedResponseArray); + winningBid = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log)).filter(log => log.winner)[0]; + expect(winningBid.adid).equals('3e6e4bce5c8fgg'); + }); + + it('should pick winning bid if multibids with same request id and equal cpm', function() { + performStandardAuctionMultiBidWithSameRequestId(MOCK.BIDS_SAME_REQ_EQUAL_CPM); + let winningBid = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log)).filter(log => log.winner)[0]; + expect(winningBid.adid).equals('3e6e4bce5c8fgg'); + medianetAnalytics.clearlogsQueue(); + + const reversedResponseArray = [].concat(MOCK.BIDS_SAME_REQ_EQUAL_CPM).reverse(); + performStandardAuctionMultiBidWithSameRequestId(reversedResponseArray); + winningBid = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log)).filter(log => log.winner)[0]; + expect(winningBid.adid).equals('3e6e4bce5c8fgg'); + }); }); }); diff --git a/test/spec/modules/mediasquareBidAdapter_spec.js b/test/spec/modules/mediasquareBidAdapter_spec.js index f3f09a8ddf8..40d0c44e4f9 100644 --- a/test/spec/modules/mediasquareBidAdapter_spec.js +++ b/test/spec/modules/mediasquareBidAdapter_spec.js @@ -61,7 +61,25 @@ describe('MediaSquare bid adapter tests', function () { code: 'publishername_atf_desktop_rg_pave' }, }]; - + var FLOORS_PARAMS = [{ + adUnitCode: 'banner-div', + bidId: 'aaaa1234', + auctionId: 'bbbb1234', + transactionId: 'cccc1234', + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ] + } + }, + bidder: 'mediasquare', + params: { + owner: 'test', + code: 'publishername_atf_desktop_rg_pave' + }, + getFloor: function (a) { return { currency: 'EUR', floor: 1.0 }; }, + }]; var BID_RESPONSE = {'body': { 'responses': [{ 'transaction_id': 'cccc1234', @@ -117,6 +135,12 @@ describe('MediaSquare bid adapter tests', function () { expect(requestContent.codes[0]).to.have.property('auctionId').and.to.equal('bbbb1234'); expect(requestContent.codes[0]).to.have.property('transactionId').and.to.equal('cccc1234'); expect(requestContent.codes[0]).to.have.property('mediatypes').exist; + expect(requestContent.codes[0]).to.have.property('floor').exist; + expect(requestContent.codes[0].floor).to.deep.equal({}); + const requestfloor = spec.buildRequests(FLOORS_PARAMS, DEFAULT_OPTIONS); + const responsefloor = JSON.parse(requestfloor.data); + expect(responsefloor.codes[0]).to.have.property('floor').exist; + expect(responsefloor.codes[0].floor).to.have.property('floor').and.to.equal(1.0); }); it('Verify parse response', function () { @@ -148,6 +172,14 @@ describe('MediaSquare bid adapter tests', function () { expect(bid.mediasquare.match).to.exist; expect(bid.mediasquare.match).to.equal(true); }); + it('Verifies hasConsent', function () { + const request = spec.buildRequests(DEFAULT_PARAMS, DEFAULT_OPTIONS); + BID_RESPONSE.body.responses[0].hasConsent = true; + const response = spec.interpretResponse(BID_RESPONSE, request); + const bid = response[0]; + expect(bid.mediasquare.hasConsent).to.exist; + expect(bid.mediasquare.hasConsent).to.equal(true); + }); it('Verifies bidder code', function () { expect(spec.code).to.equal('mediasquare'); }); @@ -161,13 +193,15 @@ describe('MediaSquare bid adapter tests', function () { }); it('Verifies bid won', function () { const request = spec.buildRequests(DEFAULT_PARAMS, DEFAULT_OPTIONS); + BID_RESPONSE.body.responses[0].match = true + BID_RESPONSE.body.responses[0].hasConsent = true; const response = spec.interpretResponse(BID_RESPONSE, request); const won = spec.onBidWon(response[0]); expect(won).to.equal(true); }); it('Verifies user sync without cookie in bid response', function () { var syncs = spec.getUserSyncs({}, [BID_RESPONSE], DEFAULT_OPTIONS.gdprConsent, DEFAULT_OPTIONS.uspConsent); - expect(syncs).to.have.property('type').and.to.equal('iframe'); + expect(syncs).to.have.lengthOf(0); }); it('Verifies user sync with cookies in bid response', function () { BID_RESPONSE.body.cookies = [{'type': 'image', 'url': 'http://www.cookie.sync.org/'}]; @@ -178,13 +212,13 @@ describe('MediaSquare bid adapter tests', function () { }); it('Verifies user sync with no bid response', function() { var syncs = spec.getUserSyncs({}, null, DEFAULT_OPTIONS.gdprConsent, DEFAULT_OPTIONS.uspConsent); - expect(syncs).to.have.property('type').and.to.equal('iframe'); + expect(syncs).to.have.lengthOf(0); }); it('Verifies user sync with no bid body response', function() { var syncs = spec.getUserSyncs({}, [], DEFAULT_OPTIONS.gdprConsent, DEFAULT_OPTIONS.uspConsent); - expect(syncs).to.have.property('type').and.to.equal('iframe'); + expect(syncs).to.have.lengthOf(0); var syncs = spec.getUserSyncs({}, [{}], DEFAULT_OPTIONS.gdprConsent, DEFAULT_OPTIONS.uspConsent); - expect(syncs).to.have.property('type').and.to.equal('iframe'); + expect(syncs).to.have.lengthOf(0); }); it('Verifies native in bid response', function () { const request = spec.buildRequests(NATIVE_PARAMS, DEFAULT_OPTIONS); diff --git a/test/spec/modules/minutemediaBidAdapter_spec.js b/test/spec/modules/minutemediaBidAdapter_spec.js new file mode 100644 index 00000000000..b1ad7f96bc4 --- /dev/null +++ b/test/spec/modules/minutemediaBidAdapter_spec.js @@ -0,0 +1,405 @@ +import { expect } from 'chai'; +import { spec } from 'modules/minutemediaBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import { config } from 'src/config.js'; +import { VIDEO } from '../../../src/mediaTypes.js'; +import { deepClone } from 'src/utils.js'; + +const ENDPOINT = 'https://hb.minutemedia-prebid.com/hb-mm'; +const TEST_ENDPOINT = 'https://hb.minutemedia-prebid.com/hb-mm-test'; +const TTL = 360; + +describe('minutemediaAdapter', function () { + const adapter = newBidder(spec); + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + const bid = { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [['640', '480']], + 'params': { + 'org': 'jdye8weeyirk00000001' + } + }; + + it('should return true when required params are passed', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not found', function () { + const newBid = Object.assign({}, bid); + delete newBid.params; + newBid.params = { + 'org': null + }; + expect(spec.isBidRequestValid(newBid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + const bidRequests = [ + { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [[640, 480]], + 'params': { + 'org': 'jdye8weeyirk00000001' + }, + 'bidId': '299ffc8cca0b87', + 'bidderRequestId': '1144f487e563f9', + 'auctionId': 'bfc420c3-8577-4568-9766-a8a935fb620d', + } + ]; + + const testModeBidRequests = [ + { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [[640, 480]], + 'params': { + 'org': 'jdye8weeyirk00000001', + 'testMode': true + }, + 'bidId': '299ffc8cca0b87', + 'bidderRequestId': '1144f487e563f9', + 'auctionId': 'bfc420c3-8577-4568-9766-a8a935fb620d', + } + ]; + + const bidderRequest = { + bidderCode: 'minutemedia', + } + const placementId = '12345678'; + + it('sends the placementId as a query param', function () { + bidRequests[0].params.placementId = placementId; + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data.placement_id).to.equal(placementId); + } + }); + + it('sends bid request to ENDPOINT via GET', function () { + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.url).to.equal(ENDPOINT); + expect(request.method).to.equal('GET'); + } + }); + + it('sends bid request to test ENDPOINT via GET', function () { + const requests = spec.buildRequests(testModeBidRequests, bidderRequest); + for (const request of requests) { + expect(request.url).to.equal(TEST_ENDPOINT); + expect(request.method).to.equal('GET'); + } + }); + + it('should send the correct bid Id', function () { + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data.bid_id).to.equal('299ffc8cca0b87'); + } + }); + + it('should send the correct width and height', function () { + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('width', 640); + expect(request.data).to.have.property('height', 480); + } + }); + + it('should respect syncEnabled option', function() { + config.setConfig({ + userSync: { + syncEnabled: false, + filterSettings: { + all: { + bidders: '*', + filter: 'include' + } + } + } + }); + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.not.have.property('cs_method'); + } + }); + + it('should respect "iframe" filter settings', function () { + config.setConfig({ + userSync: { + syncEnabled: true, + filterSettings: { + iframe: { + bidders: [spec.code], + filter: 'include' + } + } + } + }); + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('cs_method', 'iframe'); + } + }); + + it('should respect "all" filter settings', function () { + config.setConfig({ + userSync: { + syncEnabled: true, + filterSettings: { + all: { + bidders: [spec.code], + filter: 'include' + } + } + } + }); + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('cs_method', 'iframe'); + } + }); + + it('should send the pixel user sync param if userSync is enabled and no "iframe" or "all" configs are present', function () { + config.setConfig({ + userSync: { + syncEnabled: true + } + }); + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('cs_method', 'pixel'); + } + }); + + it('should respect total exclusion', function() { + config.setConfig({ + userSync: { + syncEnabled: true, + filterSettings: { + image: { + bidders: [spec.code], + filter: 'exclude' + }, + iframe: { + bidders: [spec.code], + filter: 'exclude' + } + } + } + }); + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.not.have.property('cs_method'); + } + }); + + it('should have us_privacy param if usPrivacy is available in the bidRequest', function () { + const bidderRequestWithUSP = Object.assign({uspConsent: '1YNN'}, bidderRequest); + const requests = spec.buildRequests(bidRequests, bidderRequestWithUSP); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('us_privacy', '1YNN'); + } + }); + + it('should have an empty us_privacy param if usPrivacy is missing in the bidRequest', function () { + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.not.have.property('us_privacy'); + } + }); + + it('should not send the gdpr param if gdprApplies is false in the bidRequest', function () { + const bidderRequestWithGDPR = Object.assign({gdprConsent: {gdprApplies: false}}, bidderRequest); + const requests = spec.buildRequests(bidRequests, bidderRequestWithGDPR); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.not.have.property('gdpr'); + expect(request.data).to.not.have.property('gdpr_consent'); + } + }); + + it('should send the gdpr param if gdprApplies is true in the bidRequest', function () { + const bidderRequestWithGDPR = Object.assign({gdprConsent: {gdprApplies: true, consentString: 'test-consent-string'}}, bidderRequest); + const requests = spec.buildRequests(bidRequests, bidderRequestWithGDPR); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('gdpr', true); + expect(request.data).to.have.property('gdpr_consent', 'test-consent-string'); + } + }); + + it('should have schain param if it is available in the bidRequest', () => { + const schain = { + ver: '1.0', + complete: 1, + nodes: [{ asi: 'indirectseller.com', sid: '00001', hp: 1 }], + }; + bidRequests[0].schain = schain; + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('schain', '1.0,1!indirectseller.com,00001,1,,,'); + } + }); + + it('should set floor_price to getFloor.floor value if it is greater than params.floorPrice', function() { + const bid = deepClone(bidRequests[0]); + bid.getFloor = () => { + return { + currency: 'USD', + floor: 3.32 + } + } + bid.params.floorPrice = 0.64; + const request = spec.buildRequests([bid], bidderRequest)[0]; + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('floor_price', 3.32); + }); + + it('should set floor_price to params.floorPrice value if it is greater than getFloor.floor', function() { + const bid = deepClone(bidRequests[0]); + bid.getFloor = () => { + return { + currency: 'USD', + floor: 0.8 + } + } + bid.params.floorPrice = 1.5; + const request = spec.buildRequests([bid], bidderRequest)[0]; + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('floor_price', 1.5); + }); + }); + + describe('interpretResponse', function () { + const response = { + cpm: 12.5, + vastXml: '', + width: 640, + height: 480, + requestId: '21e12606d47ba7', + netRevenue: true, + currency: 'USD', + adomain: ['abc.com'] + }; + + it('should get correct bid response', function () { + let expectedResponse = [ + { + requestId: '21e12606d47ba7', + cpm: 12.5, + width: 640, + height: 480, + creativeId: '21e12606d47ba7', + currency: 'USD', + netRevenue: true, + ttl: TTL, + vastXml: '', + mediaType: VIDEO, + meta: { + advertiserDomains: ['abc.com'] + } + } + ]; + const result = spec.interpretResponse({ body: response }); + expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); + }); + }) + + describe('getUserSyncs', function() { + const imageSyncResponse = { + body: { + userSyncPixels: [ + 'https://image-sync-url.test/1', + 'https://image-sync-url.test/2', + 'https://image-sync-url.test/3' + ] + } + }; + + const iframeSyncResponse = { + body: { + userSyncURL: 'https://iframe-sync-url.test' + } + }; + + it('should register all img urls from the response', function() { + const syncs = spec.getUserSyncs({ pixelEnabled: true }, [imageSyncResponse]); + expect(syncs).to.deep.equal([ + { + type: 'image', + url: 'https://image-sync-url.test/1' + }, + { + type: 'image', + url: 'https://image-sync-url.test/2' + }, + { + type: 'image', + url: 'https://image-sync-url.test/3' + } + ]); + }); + + it('should register the iframe url from the response', function() { + const syncs = spec.getUserSyncs({ iframeEnabled: true }, [iframeSyncResponse]); + expect(syncs).to.deep.equal([ + { + type: 'iframe', + url: 'https://iframe-sync-url.test' + } + ]); + }); + + it('should register both image and iframe urls from the responses', function() { + const syncs = spec.getUserSyncs({ pixelEnabled: true, iframeEnabled: true }, [iframeSyncResponse, imageSyncResponse]); + expect(syncs).to.deep.equal([ + { + type: 'iframe', + url: 'https://iframe-sync-url.test' + }, + { + type: 'image', + url: 'https://image-sync-url.test/1' + }, + { + type: 'image', + url: 'https://image-sync-url.test/2' + }, + { + type: 'image', + url: 'https://image-sync-url.test/3' + } + ]); + }); + + it('should handle an empty response', function() { + const syncs = spec.getUserSyncs({ iframeEnabled: true }, []); + expect(syncs).to.deep.equal([]); + }); + + it('should handle when user syncs are disabled', function() { + const syncs = spec.getUserSyncs({ pixelEnabled: false }, [imageSyncResponse]); + expect(syncs).to.deep.equal([]); + }); + }) +}); diff --git a/test/spec/modules/missenaBidAdapter_spec.js b/test/spec/modules/missenaBidAdapter_spec.js new file mode 100644 index 00000000000..86b967cca5b --- /dev/null +++ b/test/spec/modules/missenaBidAdapter_spec.js @@ -0,0 +1,134 @@ +import { expect } from 'chai'; +import { spec, _getPlatform } from 'modules/missenaBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; + +describe('Missena Adapter', function () { + const adapter = newBidder(spec); + + const bidId = 'abc'; + + const bid = { + bidder: 'missena', + bidId: bidId, + sizes: [[1, 1]], + params: { + apiKey: 'PA-34745704', + }, + }; + + describe('codes', function () { + it('should return a bidder code of missena', function () { + expect(spec.code).to.equal('missena'); + }); + }); + + describe('isBidRequestValid', function () { + it('should return true if the apiKey param is present', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false if the apiKey is missing', function () { + expect( + spec.isBidRequestValid(Object.assign(bid, { params: {} })) + ).to.equal(false); + }); + + it('should return false if the apiKey is an empty string', function () { + expect( + spec.isBidRequestValid(Object.assign(bid, { params: { apiKey: '' } })) + ).to.equal(false); + }); + }); + + describe('buildRequests', function () { + const consentString = 'AAAAAAAAA=='; + + const bidderRequest = { + gdprConsent: { + consentString: consentString, + gdprApplies: true, + }, + refererInfo: { + referer: 'https://referer', + canonicalUrl: 'https://canonical', + }, + }; + + const requests = spec.buildRequests([bid, bid], bidderRequest); + const request = requests[0]; + const payload = JSON.parse(request.data); + + it('should return as many server requests as bidder requests', function () { + expect(requests.length).to.equal(2); + }); + + it('should have a post method', function () { + expect(request.method).to.equal('POST'); + }); + + it('should send the bidder id', function () { + expect(payload.request_id).to.equal(bidId); + }); + + it('should send referer information to the request', function () { + expect(payload.referer).to.equal('https://referer'); + expect(payload.referer_canonical).to.equal('https://canonical'); + }); + + it('should send gdpr consent information to the request', function () { + expect(payload.consent_string).to.equal(consentString); + expect(payload.consent_required).to.equal(true); + }); + }); + + describe('interpretResponse', function () { + const serverResponse = { + requestId: bidId, + cpm: 0.5, + currency: 'USD', + ad: '', + meta: { + advertiserDomains: ['missena.com'] + }, + }; + + const serverTimeoutResponse = { + requestId: bidId, + timeout: true, + ad: '', + }; + + const serverEmptyAdResponse = { + requestId: bidId, + cpm: 0.5, + currency: 'USD', + ad: '', + }; + + it('should return a proper bid response', function () { + const result = spec.interpretResponse({ body: serverResponse }, bid); + + expect(result.length).to.equal(1); + + expect(Object.keys(result[0])).to.have.members( + Object.keys(serverResponse) + ); + }); + + it('should return an empty response when the server answers with a timeout', function () { + const result = spec.interpretResponse( + { body: serverTimeoutResponse }, + bid + ); + expect(result).to.deep.equal([]); + }); + + it('should return an empty response when the server answers with an empty ad', function () { + const result = spec.interpretResponse( + { body: serverEmptyAdResponse }, + bid + ); + expect(result).to.deep.equal([]); + }); + }); +}); diff --git a/test/spec/modules/multibid_spec.js b/test/spec/modules/multibid_spec.js index 86365eb520f..eaf8fa33a66 100644 --- a/test/spec/modules/multibid_spec.js +++ b/test/spec/modules/multibid_spec.js @@ -11,7 +11,6 @@ import { import {parse as parseQuery} from 'querystring'; import {config} from 'src/config.js'; import * as utils from 'src/utils.js'; -import find from 'core-js-pure/features/array/find.js'; describe('multibid adapter', function () { let bidArray = [{ diff --git a/test/spec/modules/nextMillenniumBidAdapter_spec.js b/test/spec/modules/nextMillenniumBidAdapter_spec.js index 1a24c6d0575..a8aa62f24d1 100644 --- a/test/spec/modules/nextMillenniumBidAdapter_spec.js +++ b/test/spec/modules/nextMillenniumBidAdapter_spec.js @@ -4,6 +4,7 @@ import { spec } from 'modules/nextMillenniumBidAdapter.js'; describe('nextMillenniumBidAdapterTests', function() { const bidRequestData = [ { + adUnitCode: 'test-div', bidId: 'bid1234', auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', bidder: 'nextMillennium', @@ -17,19 +18,94 @@ describe('nextMillenniumBidAdapterTests', function() { } ]; - it('Request params check with GDPR Consent', function () { + const bidRequestDataGI = [ + { + adUnitCode: 'test-banner-gi', + bidId: 'bid1234', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidder: 'nextMillennium', + params: { group_id: '1234' }, + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + + sizes: [[300, 250]], + uspConsent: '1---', + gdprConsent: { + consentString: 'kjfdniwjnifwenrif3', + gdprApplies: true + } + }, + + { + adUnitCode: 'test-video-gi', + bidId: 'bid1234', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidder: 'nextMillennium', + params: { group_id: '1234' }, + mediaTypes: { + video: { + playerSize: [640, 480], + } + }, + + uspConsent: '1---', + gdprConsent: { + consentString: 'kjfdniwjnifwenrif3', + gdprApplies: true + } + }, + ]; + + it('Request params check with GDPR and USP Consent', function () { const request = spec.buildRequests(bidRequestData, bidRequestData[0]); expect(JSON.parse(request[0].data).user.ext.consent).to.equal('kjfdniwjnifwenrif3'); expect(JSON.parse(request[0].data).regs.ext.us_privacy).to.equal('1---'); expect(JSON.parse(request[0].data).regs.ext.gdpr).to.equal(1); }); + it('Request params check without GDPR Consent', function () { + delete bidRequestData[0].gdprConsent + const request = spec.buildRequests(bidRequestData, bidRequestData[0]); + expect(JSON.parse(request[0].data).regs.ext.gdpr).to.be.undefined; + expect(JSON.parse(request[0].data).regs.ext.us_privacy).to.equal('1---'); + }); + it('validate_generated_params', function() { const request = spec.buildRequests(bidRequestData); expect(request[0].bidId).to.equal('bid1234'); expect(JSON.parse(request[0].data).id).to.equal('b06c5141-fe8f-4cdf-9d7d-54415490a917'); }); + it('use parameters group_id', function() { + for (let test of bidRequestDataGI) { + const request = spec.buildRequests([test]); + const requestData = JSON.parse(request[0].data); + const storeRequestId = requestData.ext.prebid.storedrequest.id; + const templateRE = /^g\d+;\d+x\d+;/; + expect(templateRE.test(storeRequestId)).to.be.true; + }; + }); + + it('Check if refresh_count param is incremented', function() { + const request = spec.buildRequests(bidRequestData); + expect(JSON.parse(request[0].data).ext.nextMillennium.refresh_count).to.equal(3); + }); + + it('Test getUserSyncs function', function () { + const syncOptions = { + 'iframeEnabled': true + } + const userSync = spec.getUserSyncs(syncOptions); + expect(userSync).to.be.an('array').with.lengthOf(1); + expect(userSync[0].type).to.exist; + expect(userSync[0].url).to.exist; + expect(userSync[0].type).to.be.equal('iframe'); + expect(userSync[0].url).to.be.equal('https://statics.nextmillmedia.com/load-cookie.html?v=4'); + }); + it('validate_response_params', function() { const serverResponse = { body: { diff --git a/test/spec/modules/nextrollBidAdapter_spec.js b/test/spec/modules/nextrollBidAdapter_spec.js index 4699fbc6e08..d4779120248 100644 --- a/test/spec/modules/nextrollBidAdapter_spec.js +++ b/test/spec/modules/nextrollBidAdapter_spec.js @@ -244,8 +244,8 @@ describe('nextrollBidAdapter', function() { let expectedResponse = { clickUrl: clickUrl, impressionTrackers: [impUrl], - privacyLink: 'https://info.evidon.com/pub_info/573', - privacyIcon: 'https://c.betrad.com/pub/icon1.png', + privacyLink: 'https://app.adroll.com/optout/personalized', + privacyIcon: 'https://s.adroll.com/j/ad-choices-small.png', title: titleText, image: {url: imgUrl, width: imgW, height: imgH}, sponsoredBy: brandText, @@ -274,8 +274,8 @@ describe('nextrollBidAdapter', function() { impressionTrackers: [impUrl], jstracker: [], clickTrackers: [], - privacyLink: 'https://info.evidon.com/pub_info/573', - privacyIcon: 'https://c.betrad.com/pub/icon1.png', + privacyLink: 'https://app.adroll.com/optout/personalized', + privacyIcon: 'https://s.adroll.com/j/ad-choices-small.png', title: titleText, image: {url: imgUrl, width: imgW, height: imgH}, icon: {url: iconUrl, width: iconW, height: iconH}, diff --git a/test/spec/modules/nexx360BidAdapter_spec.js b/test/spec/modules/nexx360BidAdapter_spec.js new file mode 100644 index 00000000000..7501391cbfc --- /dev/null +++ b/test/spec/modules/nexx360BidAdapter_spec.js @@ -0,0 +1,140 @@ +import {expect} from 'chai'; +import {spec} from 'modules/nexx360BidAdapter.js'; +import {newBidder} from 'src/adapters/bidderFactory.js'; +import {config} from 'src/config.js'; +import * as utils from 'src/utils.js'; +import { requestBidsHook } from 'modules/consentManagement.js'; + +describe('Nexx360 bid adapter tests', function () { + var DEFAULT_PARAMS = [{ + adUnitCode: 'banner-div', + bidId: 'aaaa1234', + auctionId: 'bbbb1234', + transactionId: 'cccc1234', + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ] + } + }, + bidder: 'nexx360', + params: { + account: '1067', + tagId: 'luvxjvgn' + }, + }]; + + var BID_RESPONSE = {'body': { + 'responses': [ + { + 'bidId': '49a705a42610a', + 'cpm': 0.437245, + 'width': 300, + 'height': 250, + 'creativeId': '98493581', + 'currency': 'EUR', + 'netRevenue': true, + 'ttl': 360, + 'uuid': 'ce6d1ee3-2a05-4d7c-b97a-9e62097798ec', + 'bidder': 'appnexus', + 'consent': 1, + 'tagId': 'luvxjvgn' + } + ], + }}; + + const DEFAULT_OPTIONS = { + gdprConsent: { + gdprApplies: true, + consentString: 'BOzZdA0OzZdA0AGABBENDJ-AAAAvh7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__79__3z3_9pxP78k89r7337Mw_v-_v-b7JCPN_Y3v-8Kg', + vendorData: {} + }, + refererInfo: { + referer: 'https://www.prebid.org', + canonicalUrl: 'https://www.prebid.org/the/link/to/the/page' + }, + uspConsent: '111222333', + userId: { 'id5id': { uid: '1111' } }, + schain: { + 'ver': '1.0', + 'complete': 1, + 'nodes': [{ + 'asi': 'exchange1.com', + 'sid': '1234', + 'hp': 1, + 'rid': 'bid-request-1', + 'name': 'publisher', + 'domain': 'publisher.com' + }] + }, + }; + it('Verify build request', function () { + const request = spec.buildRequests(DEFAULT_PARAMS, DEFAULT_OPTIONS); + expect(request).to.have.property('url').and.to.equal('https://fast.nexx360.io/prebid'); + expect(request).to.have.property('method').and.to.equal('POST'); + const requestContent = JSON.parse(request.data); + expect(requestContent.adUnits[0]).to.have.property('account').and.to.equal('1067'); + expect(requestContent.adUnits[0]).to.have.property('tagId').and.to.equal('luvxjvgn'); + expect(requestContent.adUnits[0]).to.have.property('label').and.to.equal('banner-div'); + expect(requestContent.adUnits[0]).to.have.property('bidId').and.to.equal('aaaa1234'); + expect(requestContent.adUnits[0]).to.have.property('auctionId').and.to.equal('bbbb1234'); + expect(requestContent.adUnits[0]).to.have.property('mediatypes').exist; + }); + + it('Verify parse response', function () { + const request = spec.buildRequests(DEFAULT_PARAMS, DEFAULT_OPTIONS); + const response = spec.interpretResponse(BID_RESPONSE, request); + expect(response).to.have.lengthOf(1); + const bid = response[0]; + expect(bid.cpm).to.equal(0.437245); + expect(bid.adUrl).to.equal('https://fast.nexx360.io/cache?uuid=ce6d1ee3-2a05-4d7c-b97a-9e62097798ec'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.creativeId).to.equal('98493581'); + expect(bid.currency).to.equal('EUR'); + expect(bid.netRevenue).to.equal(true); + expect(bid.ttl).to.equal(360); + expect(bid.requestId).to.equal('49a705a42610a'); + expect(bid.nexx360).to.exist; + expect(bid.nexx360.ssp).to.equal('appnexus'); + }); + it('Verifies bidder code', function () { + expect(spec.code).to.equal('nexx360'); + }); + + it('Verifies bidder aliases', function () { + expect(spec.aliases).to.have.lengthOf(1); + expect(spec.aliases[0]).to.equal('revenuemaker'); + }); + it('Verifies if bid request valid', function () { + expect(spec.isBidRequestValid(DEFAULT_PARAMS[0])).to.equal(true); + }); + it('Verifies bid won', function () { + const request = spec.buildRequests(DEFAULT_PARAMS, DEFAULT_OPTIONS); + const response = spec.interpretResponse(BID_RESPONSE, request); + const won = spec.onBidWon(response[0]); + expect(won).to.equal(true); + }); + it('Verifies user sync without cookie in bid response', function () { + var syncs = spec.getUserSyncs({}, [BID_RESPONSE], DEFAULT_OPTIONS.gdprConsent, DEFAULT_OPTIONS.uspConsent); + expect(syncs).to.have.lengthOf(0); + }); + it('Verifies user sync with cookies in bid response', function () { + BID_RESPONSE.body.cookies = [{'type': 'image', 'url': 'http://www.cookie.sync.org/'}]; + var syncs = spec.getUserSyncs({}, [BID_RESPONSE], DEFAULT_OPTIONS.gdprConsent); + expect(syncs).to.have.lengthOf(1); + expect(syncs[0]).to.have.property('type').and.to.equal('image'); + expect(syncs[0]).to.have.property('url').and.to.equal('http://www.cookie.sync.org/'); + }); + it('Verifies user sync with no bid response', function() { + var syncs = spec.getUserSyncs({}, null, DEFAULT_OPTIONS.gdprConsent, DEFAULT_OPTIONS.uspConsent); + expect(syncs).to.have.lengthOf(0); + }); + it('Verifies user sync with no bid body response', function() { + var syncs = spec.getUserSyncs({}, [], DEFAULT_OPTIONS.gdprConsent, DEFAULT_OPTIONS.uspConsent); + expect(syncs).to.have.lengthOf(0); + var syncs = spec.getUserSyncs({}, [{}], DEFAULT_OPTIONS.gdprConsent, DEFAULT_OPTIONS.uspConsent); + expect(syncs).to.have.lengthOf(0); + }); +}); diff --git a/test/spec/modules/novatiqIdSystem_spec.js b/test/spec/modules/novatiqIdSystem_spec.js index 60c82626450..b92fb0d219a 100644 --- a/test/spec/modules/novatiqIdSystem_spec.js +++ b/test/spec/modules/novatiqIdSystem_spec.js @@ -3,41 +3,41 @@ import * as utils from 'src/utils.js'; import { server } from 'test/mocks/xhr.js'; describe('novatiqIdSystem', function () { + let urlParams = { + novatiqId: 'snowflake', + useStandardUuid: false, + useSspId: true, + useSspHost: true + } + describe('getSrcId', function() { it('getSrcId should set srcId value to 000 due to undefined parameter in config section', function() { const config = { params: { } }; const configParams = config.params || {}; - const response = novatiqIdSubmodule.getSrcId(configParams); + const response = novatiqIdSubmodule.getSrcId(configParams, urlParams); expect(response).to.eq('000'); }); it('getSrcId should set srcId value to 000 due to missing value in config section', function() { const config = { params: { sourceid: '' } }; const configParams = config.params || {}; - const response = novatiqIdSubmodule.getSrcId(configParams); + const response = novatiqIdSubmodule.getSrcId(configParams, urlParams); expect(response).to.eq('000'); }); it('getSrcId should set value to 000 due to null value in config section', function() { const config = { params: { sourceid: null } }; const configParams = config.params || {}; - const response = novatiqIdSubmodule.getSrcId(configParams); + const response = novatiqIdSubmodule.getSrcId(configParams, urlParams); expect(response).to.eq('000'); }); it('getSrcId should set value to 001 due to wrong length in config section max 3 chars', function() { const config = { params: { sourceid: '1234' } }; const configParams = config.params || {}; - const response = novatiqIdSubmodule.getSrcId(configParams); + const response = novatiqIdSubmodule.getSrcId(configParams, urlParams); expect(response).to.eq('001'); }); - - it('getSrcId should set value to 002 due to wrong format in config section', function() { - const config = { params: { sourceid: '1xc' } }; - const configParams = config.params || {}; - const response = novatiqIdSubmodule.getSrcId(configParams); - expect(response).to.eq('002'); - }); }); describe('getId', function() { @@ -52,6 +52,89 @@ describe('novatiqIdSystem', function () { const response = novatiqIdSubmodule.getId(config); expect(response.id).should.be.not.empty; }); + + it('should set sharedStatus if sharedID is configured but returned null', function() { + const config = { params: { sourceid: '123', useSharedId: true } }; + const response = novatiqIdSubmodule.getId(config); + expect(response.sharedStatus).to.equal('Not Found'); + }); + + it('should set sharedStatus if sharedID is configured and is valid', function() { + const config = { params: { sourceid: '123', useSharedId: true } }; + + let stub = sinon.stub(novatiqIdSubmodule, 'getSharedId').returns('fakeId'); + + const response = novatiqIdSubmodule.getId(config); + + stub.restore(); + + expect(response.sharedStatus).to.equal('Found'); + }); + + it('should set sharedStatus if sharedID is configured and is valid when making an async call', function() { + const config = { params: { sourceid: '123', useSharedId: true, useCallbacks: true } }; + + let stub = sinon.stub(novatiqIdSubmodule, 'getSharedId').returns('fakeId'); + + const response = novatiqIdSubmodule.getId(config); + + stub.restore(); + + expect(response.sharedStatus).to.equal('Found'); + }); + }); + + describe('getUrlParams', function() { + it('should return default url parameters when none set', function() { + const defaultUrlParams = { + novatiqId: 'snowflake', + useStandardUuid: false, + useSspId: true, + useSspHost: true + } + + const config = { params: { sourceid: '123' } }; + const response = novatiqIdSubmodule.getUrlParams(config); + + expect(response).to.deep.equal(defaultUrlParams); + }); + + it('should return custom url parameters when set', function() { + let customUrlParams = { + novatiqId: 'hyperid', + useStandardUuid: true, + useSspId: false, + useSspHost: false + } + + const config = { + sourceid: '123', + urlParams: { + novatiqId: 'hyperid', + useStandardUuid: true, + useSspId: false, + useSspHost: false + } + }; + const response = novatiqIdSubmodule.getUrlParams(config); + + expect(response).to.deep.equal(customUrlParams); + }); + }); + + describe('sendAsyncSyncRequest', function() { + it('should return an async function when called asynchronously', function() { + const defaultUrlParams = { + novatiqId: 'snowflake', + useStandardUuid: false, + useSspId: true, + useSspHost: true + } + + const url = novatiqIdSubmodule.getSyncUrl(false, '', defaultUrlParams); + const response = novatiqIdSubmodule.sendAsyncSyncRequest('testuuid', url); + expect(response.callback).should.not.be.empty; + }); }); describe('decode', function() { diff --git a/test/spec/modules/oguryBidAdapter_spec.js b/test/spec/modules/oguryBidAdapter_spec.js index 13bffc8075c..acf62bf5a7b 100644 --- a/test/spec/modules/oguryBidAdapter_spec.js +++ b/test/spec/modules/oguryBidAdapter_spec.js @@ -229,7 +229,7 @@ describe('OguryBidAdapter', function () { const defaultTimeout = 1000; const expectedRequestObject = { id: bidRequests[0].auctionId, - at: 2, + at: 1, tmax: defaultTimeout, imp: [{ id: bidRequests[0].bidId, @@ -268,6 +268,10 @@ describe('OguryBidAdapter', function () { ext: { consent: bidderRequest.gdprConsent.consentString }, + }, + ext: { + prebidversion: '$prebid.version$', + adapterversion: '1.2.10' } }; @@ -296,7 +300,59 @@ describe('OguryBidAdapter', function () { ...expectedRequestObject, regs: { ext: { - gdpr: 1 + gdpr: 0 + }, + }, + user: { + ext: { + consent: '' + }, + } + }; + + const validBidRequests = bidRequests + + const request = spec.buildRequests(validBidRequests, bidderRequestWithoutGdpr); + expect(request.data).to.deep.equal(expectedRequestObjectWithoutGdpr); + expect(request.data.regs.ext.gdpr).to.be.a('number'); + }); + + it('should not add gdpr infos if gdprConsent is undefined', () => { + const bidderRequestWithoutGdpr = { + ...bidderRequest, + gdprConsent: undefined, + } + const expectedRequestObjectWithoutGdpr = { + ...expectedRequestObject, + regs: { + ext: { + gdpr: 0 + }, + }, + user: { + ext: { + consent: '' + }, + } + }; + + const validBidRequests = bidRequests + + const request = spec.buildRequests(validBidRequests, bidderRequestWithoutGdpr); + expect(request.data).to.deep.equal(expectedRequestObjectWithoutGdpr); + expect(request.data.regs.ext.gdpr).to.be.a('number'); + }); + + it('should not add tcString and turn off gdpr-applies if consentString and gdprApplies are undefined', () => { + const bidderRequestWithoutGdpr = { + ...bidderRequest, + gdprConsent: { consentString: undefined, gdprApplies: undefined }, + } + const expectedRequestObjectWithoutGdpr = { + ...expectedRequestObject, + regs: { + ext: { + gdpr: 0 }, }, user: { @@ -425,7 +481,9 @@ describe('OguryBidAdapter', function () { meta: { advertiserDomains: openRtbBidResponse.body.seatbid[0].bid[0].adomain }, - nurl: openRtbBidResponse.body.seatbid[0].bid[0].nurl + nurl: openRtbBidResponse.body.seatbid[0].bid[0].nurl, + adapterVersion: '1.2.10', + prebidVersion: '$prebid.version$' }, { requestId: openRtbBidResponse.body.seatbid[0].bid[1].impid, cpm: openRtbBidResponse.body.seatbid[0].bid[1].price, @@ -440,7 +498,9 @@ describe('OguryBidAdapter', function () { meta: { advertiserDomains: openRtbBidResponse.body.seatbid[0].bid[1].adomain }, - nurl: openRtbBidResponse.body.seatbid[0].bid[1].nurl + nurl: openRtbBidResponse.body.seatbid[0].bid[1].nurl, + adapterVersion: '1.2.10', + prebidVersion: '$prebid.version$' }] let request = spec.buildRequests(bidRequests, bidderRequest); @@ -486,6 +546,11 @@ describe('OguryBidAdapter', function () { expect(requests.length).to.equal(0); }) + it('Should not create nurl request if bid contains undefined nurl', function() { + spec.onBidWon({ nurl: undefined }) + expect(requests.length).to.equal(0); + }) + it('Should create nurl request if bid nurl', function() { spec.onBidWon({ nurl }) expect(requests.length).to.equal(1); @@ -561,6 +626,7 @@ describe('OguryBidAdapter', function () { expect(requests.length).to.equal(1); expect(requests[0].url).to.equal(TIMEOUT_URL); expect(requests[0].method).to.equal('POST'); + expect(JSON.parse(requests[0].requestBody).location).to.equal(window.location.href); }) }); }); diff --git a/test/spec/modules/onetagBidAdapter_spec.js b/test/spec/modules/onetagBidAdapter_spec.js index e873597ca15..f335f2ec62a 100644 --- a/test/spec/modules/onetagBidAdapter_spec.js +++ b/test/spec/modules/onetagBidAdapter_spec.js @@ -1,6 +1,6 @@ import { spec, isValid, hasTypeVideo } from 'modules/onetagBidAdapter.js'; import { expect } from 'chai'; -import find from 'core-js-pure/features/array/find.js'; +import {find} from 'src/polyfill.js'; import { BANNER, VIDEO } from 'src/mediaTypes.js'; import {INSTREAM, OUTSTREAM} from 'src/video.js'; diff --git a/test/spec/modules/ooloAnalyticsAdapter_spec.js b/test/spec/modules/ooloAnalyticsAdapter_spec.js index f82f7856fb2..a6030e972ce 100644 --- a/test/spec/modules/ooloAnalyticsAdapter_spec.js +++ b/test/spec/modules/ooloAnalyticsAdapter_spec.js @@ -2,7 +2,7 @@ import ooloAnalytics, { PAGEVIEW_ID } from 'modules/ooloAnalyticsAdapter.js'; import {expect} from 'chai'; import {server} from 'test/mocks/xhr.js'; import constants from 'src/constants.json' -import events from 'src/events' +import * as events from 'src/events' import { config } from 'src/config'; import { buildAuctionData, generatePageViewId } from 'modules/ooloAnalyticsAdapter'; diff --git a/test/spec/modules/open8BidAdapter_spec.js b/test/spec/modules/open8BidAdapter_spec.js new file mode 100644 index 00000000000..27e460bad9d --- /dev/null +++ b/test/spec/modules/open8BidAdapter_spec.js @@ -0,0 +1,258 @@ +import { spec } from 'modules/open8BidAdapter'; +import { newBidder } from 'src/adapters/bidderFactory'; + +const ENDPOINT = 'https://as.vt.open8.com/v1/control/prebid'; + +describe('Open8Adapter', function() { + const adapter = newBidder(spec); + + describe('isBidRequestValid', function() { + let bid = { + 'bidder': 'open8', + 'params': { + 'slotKey': 'slotkey1234' + }, + 'adUnitCode': 'adunit', + 'sizes': [[300, 250]], + 'bidId': 'bidid1234', + 'bidderRequestId': 'requestid1234', + 'auctionId': 'auctionid1234', + }; + + it('should return true when required params found', function() { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not passed', function() { + bid.params = { + ' slotKey': 0 + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function() { + let bidRequests = [ + { + 'bidder': 'open8', + 'params': { + 'slotKey': 'slotkey1234' + }, + 'adUnitCode': 'adunit', + 'sizes': [[300, 250]], + 'bidId': 'bidid1234', + 'bidderRequestId': 'requestid1234', + 'auctionId': 'auctionid1234', + } + ]; + + it('sends bid request to ENDPOINT via GET', function() { + const requests = spec.buildRequests(bidRequests); + expect(requests[0].url).to.equal(ENDPOINT); + expect(requests[0].method).to.equal('GET'); + }); + }); + describe('interpretResponse', function() { + const adomin = ['example.com'] + const bannerResponse = { + slotKey: 'slotkey1234', + userId: 'userid1234', + impId: 'impid1234', + media: 'TEST_MEDIA', + nurl: '//example/win', + isAdReturn: true, + syncPixels: ['//example/sync/pixel.gif'], + syncIFs: [], + ad: { + bidId: 'TEST_BID_ID', + price: 1234.56, + creativeId: 'creativeid1234', + dealId: 'TEST_DEAL_ID', + currency: 'JPY', + ds: 876, + spd: 1234, + fa: 5678, + pr: 'pr1234', + mr: 'mr1234', + nurl: '//example/win', + adType: 2, + banner: { + w: 300, + h: 250, + adm: '
', + imps: ['//example.com/imp'] + }, + adomain: adomin + } + }; + const videoResponse = { + slotKey: 'slotkey1234', + userId: 'userid1234', + impId: 'impid1234', + media: 'TEST_MEDIA', + isAdReturn: true, + syncPixels: ['//example/sync/pixel.gif'], + syncIFs: [], + ad: { + bidId: 'TEST_BID_ID', + price: 1234.56, + creativeId: 'creativeid1234', + dealId: 'TEST_DEAL_ID', + currency: 'JPY', + ds: 876, + spd: 1234, + fa: 5678, + pr: 'pr1234', + mr: 'mr1234', + nurl: '//example/win', + adType: 1, + video: { + purl: '//playerexample.js', + vastXml: '', + w: 320, + h: 180 + }, + adomain: adomin + } + }; + + it('should get correct banner bid response', function() { + let expectedResponse = [{ + 'slotKey': 'slotkey1234', + 'userId': 'userid1234', + 'impId': 'impid1234', + 'media': 'TEST_MEDIA', + 'ds': 876, + 'spd': 1234, + 'fa': 5678, + 'pr': 'pr1234', + 'mr': 'mr1234', + 'nurl': '//example/win', + 'requestId': 'requestid1234', + 'cpm': 1234.56, + 'creativeId': 'creativeid1234', + 'dealId': 'TEST_DEAL_ID', + 'width': 300, + 'height': 250, + 'ad': "
", + 'mediaType': 'banner', + 'currency': 'JPY', + 'ttl': 360, + 'netRevenue': true, + 'meta': { } + }]; + + let bidderRequest; + let result = spec.interpretResponse({ body: bannerResponse }, { bidderRequest }); + expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); + expect(result[0]).to.nested.contain.property('meta.advertiserDomains', adomin); + }); + + it('handles video responses', function() { + let expectedResponse = [{ + 'slotKey': 'slotkey1234', + 'userId': 'userid1234', + 'impId': 'impid1234', + 'media': 'TEST_MEDIA', + 'ds': 876, + 'spd': 1234, + 'fa': 5678, + 'pr': 'pr1234', + 'mr': 'mr1234', + 'nurl': '//example/win', + 'requestId': 'requestid1234', + 'cpm': 1234.56, + 'creativeId': 'creativeid1234', + 'dealId': 'TEST_DEAL_ID', + 'width': 320, + 'height': 180, + 'vastXml': '', + 'mediaType': 'video', + 'renderer': {}, + 'adResponse': {}, + 'currency': 'JPY', + 'ttl': 360, + 'netRevenue': true, + 'meta': { } + }]; + + let bidderRequest; + let result = spec.interpretResponse({ body: videoResponse }, { bidderRequest }); + expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); + expect(result[0]).to.nested.contain.property('meta.advertiserDomains', adomin); + }); + + it('handles nobid responses', function() { + let response = { + isAdReturn: false, + 'ad': {} + }; + + let bidderRequest; + let result = spec.interpretResponse({ body: response }, { bidderRequest }); + expect(result.length).to.equal(0); + }); + }); + + describe('getUserSyncs', function() { + const imgResponse1 = { + body: { + 'isAdReturn': true, + 'ad': { /* ad body */ }, + 'syncPixels': [ + 'https://example.test/1' + ] + } + }; + + const imgResponse2 = { + body: { + 'isAdReturn': true, + 'ad': { /* ad body */ }, + 'syncPixels': [ + 'https://example.test/2' + ] + } + }; + + const ifResponse = { + body: { + 'isAdReturn': true, + 'ad': { /* ad body */ }, + 'syncIFs': [ + 'https://example.test/3' + ] + } + }; + + it('should use a sync img url from first response', function() { + const syncs = spec.getUserSyncs({ pixelEnabled: true }, [imgResponse1, imgResponse2, ifResponse]); + expect(syncs).to.deep.equal([ + { + type: 'image', + url: 'https://example.test/1' + } + ]); + }); + + it('handle ifs response', function() { + const syncs = spec.getUserSyncs({ iframeEnabled: true }, [ifResponse]); + expect(syncs).to.deep.equal([ + { + type: 'iframe', + url: 'https://example.test/3' + } + ]); + }); + + it('handle empty response (e.g. timeout)', function() { + const syncs = spec.getUserSyncs({ pixelEnabled: true }, []); + expect(syncs).to.deep.equal([]); + }); + + it('returns empty syncs when not enabled', function() { + const syncs = spec.getUserSyncs({ pixelEnabled: false }, [imgResponse1]); + expect(syncs).to.deep.equal([]); + }); + }); +}); diff --git a/test/spec/modules/openxAnalyticsAdapter_spec.js b/test/spec/modules/openxAnalyticsAdapter_spec.js index d7d2d31669c..47663a41f47 100644 --- a/test/spec/modules/openxAnalyticsAdapter_spec.js +++ b/test/spec/modules/openxAnalyticsAdapter_spec.js @@ -1,10 +1,10 @@ import { expect } from 'chai'; import openxAdapter, {AUCTION_STATES} from 'modules/openxAnalyticsAdapter.js'; -import events from 'src/events.js'; +import * as events from 'src/events.js'; import CONSTANTS from 'src/constants.json'; import * as utils from 'src/utils.js'; import { server } from 'test/mocks/xhr.js'; -import find from 'core-js-pure/features/array/find.js'; +import {find} from 'src/polyfill.js'; const { EVENTS: { AUCTION_INIT, BID_REQUESTED, BID_RESPONSE, BID_TIMEOUT, BID_WON, AUCTION_END } diff --git a/test/spec/modules/optimeraRtdProvider_spec.js b/test/spec/modules/optimeraRtdProvider_spec.js index 8b1866d044a..aec8b79045e 100644 --- a/test/spec/modules/optimeraRtdProvider_spec.js +++ b/test/spec/modules/optimeraRtdProvider_spec.js @@ -1,5 +1,6 @@ import * as optimeraRTD from '../../../modules/optimeraRtdProvider.js'; -let utils = require('src/utils.js'); + +const utils = require('src/utils.js'); describe('Optimera RTD sub module', () => { it('should init, return true, and set the params', () => { @@ -31,40 +32,86 @@ describe('Optimera RTD score file properly sets targeting values', () => { const scores = { 'div-0': ['A1', 'A2'], 'div-1': ['A3', 'A4'], - 'device': { - 'de': { + device: { + de: { 'div-0': ['A5', 'A6'], 'div-1': ['A7', 'A8'], + insights: { + ilv: ['div-0'], + miv: ['div-4'], + } }, - 'mo': { + mo: { 'div-0': ['A9', 'B0'], 'div-1': ['B1', 'B2'], + insights: { + ilv: ['div-1'], + miv: ['div-2'], + } } + }, + insights: { + ilv: ['div-5'], + miv: ['div-6'], } }; - it('Properly set the score file url', () => { + it('Properly set the score file url and scores', () => { optimeraRTD.setScores(JSON.stringify(scores)); expect(optimeraRTD.optimeraTargeting['div-0']).to.include.ordered.members(['A5', 'A6']); expect(optimeraRTD.optimeraTargeting['div-1']).to.include.ordered.members(['A7', 'A8']); }); }); +describe('Optimera RTD propery sets the window.optimera object', () => { + const scores = { + 'div-0': ['A1', 'A2'], + 'div-1': ['A3', 'A4'], + device: { + de: { + 'div-0': ['A5', 'A6'], + 'div-1': ['A7', 'A8'], + insights: { + ilv: ['div-0'], + miv: ['div-4'], + } + }, + mo: { + 'div-0': ['A9', 'B0'], + 'div-1': ['B1', 'B2'], + insights: { + ilv: ['div-1'], + miv: ['div-2'], + } + } + }, + insights: { + ilv: ['div-5'], + miv: ['div-6'], + } + }; + it('Properly set the score file url and scores', () => { + optimeraRTD.setScores(JSON.stringify(scores)); + expect(window.optimera.data['div-1']).to.include.ordered.members(['A7', 'A8']); + expect(window.optimera.insights.ilv).to.include.ordered.members(['div-0']); + }); +}); + describe('Optimera RTD targeting object is properly formed', () => { const adDivs = ['div-0', 'div-1']; it('applyTargeting properly created the targeting object', () => { const targeting = optimeraRTD.returnTargetingData(adDivs); - expect(targeting).to.deep.include({'div-0': {'optimera': [['A5', 'A6']]}}); - expect(targeting).to.deep.include({'div-1': {'optimera': [['A7', 'A8']]}}); + expect(targeting).to.deep.include({ 'div-0': { optimera: [['A5', 'A6']] } }); + expect(targeting).to.deep.include({ 'div-1': { optimera: [['A7', 'A8']] } }); }); }); describe('Optimera RTD error logging', () => { let utilsLogErrorStub; - beforeEach(function () { + beforeEach(() => { utilsLogErrorStub = sinon.stub(utils, 'logError'); }); - afterEach(function () { + afterEach(() => { utilsLogErrorStub.restore(); }); diff --git a/test/spec/modules/optimonAnalyticsAdapter_spec.js b/test/spec/modules/optimonAnalyticsAdapter_spec.js index b5b76ce3fde..c50bfcb170f 100644 --- a/test/spec/modules/optimonAnalyticsAdapter_spec.js +++ b/test/spec/modules/optimonAnalyticsAdapter_spec.js @@ -2,7 +2,7 @@ import * as utils from 'src/utils.js'; import { expect } from 'chai'; import optimonAnalyticsAdapter from '../../../modules/optimonAnalyticsAdapter.js'; import adapterManager from 'src/adapterManager'; -import events from 'src/events'; +import * as events from 'src/events'; import constants from 'src/constants.json' const AD_UNIT_CODE = 'demo-adunit-1'; diff --git a/test/spec/modules/orbidderBidAdapter_spec.js b/test/spec/modules/orbidderBidAdapter_spec.js index 0a18799ad4b..750524cf47f 100644 --- a/test/spec/modules/orbidderBidAdapter_spec.js +++ b/test/spec/modules/orbidderBidAdapter_spec.js @@ -283,6 +283,27 @@ describe('orbidderBidAdapter', () => { }); }); + describe('buildRequests with price floor module', () => { + const bidRequest = deepClone(defaultBidRequestBanner); + bidRequest.params.bidfloor = 1; + bidRequest.getFloor = (floorObj) => { + return { + floor: bidRequest.floors.values['banner|640x480'], + currency: floorObj.currency, + mediaType: floorObj.mediaType + } + }; + + bidRequest.floors = { + currency: 'EUR', + values: { + 'banner|640x480': 15.07 + } + }; + const request = buildRequest(bidRequest); + expect(request.data.params.bidfloor).to.equal(15.07); + }); + describe('interpretResponse', () => { it('banner: should get correct bid response', () => { const serverResponse = [ diff --git a/test/spec/modules/ozoneBidAdapter_spec.js b/test/spec/modules/ozoneBidAdapter_spec.js index f9941b41189..658af310ea5 100644 --- a/test/spec/modules/ozoneBidAdapter_spec.js +++ b/test/spec/modules/ozoneBidAdapter_spec.js @@ -69,8 +69,6 @@ var validBidRequestsMulti = [ transactionId: '2e63c0ed-b10c-4008-aed5-84582cecfe87' } ]; -// use 'pubcid', 'tdid', 'id5id', 'parrableId', 'idl_env', 'criteoId' -// see http://prebid.org/dev-docs/modules/userId.html var validBidRequestsWithUserIdData = [ { adUnitCode: 'div-gpt-ad-1460505748561-0', @@ -291,7 +289,6 @@ var validBidRequests1OutstreamVideo2020 = [ } ]; -// WHEN sent as bidderRequest to buildRequests you should send the child: .bidderRequest var validBidderRequest1OutstreamVideo2020 = { bidderRequest: { auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', @@ -394,7 +391,6 @@ var validBidderRequest1OutstreamVideo2020 = { timeout: 3000 } }; -// WHEN sent as bidderRequest to buildRequests you should send the child: .bidderRequest var validBidderRequest = { bidderRequest: { auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', @@ -419,11 +415,6 @@ var validBidderRequest = { } }; -// bidder request with GDPR - change the values for testing: -// gdprConsent.gdprApplies (true/false) -// gdprConsent.vendorData.purposeConsents (make empty, make null, remove it) -// gdprConsent.vendorData.vendorConsents (remove 524, remove all, make the element null, remove it) -// WHEN sent as bidderRequest to buildRequests you should send the child: .bidderRequest var bidderRequestWithFullGdpr = { bidderRequest: { auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', @@ -512,7 +503,6 @@ var gdpr1 = { 'gdprApplies': true }; -// simulating the Mirror var bidderRequestWithPartialGdpr = { bidderRequest: { auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', @@ -558,7 +548,6 @@ var bidderRequestWithPartialGdpr = { } }; -// make sure the impid matches the request bidId var validResponse = { 'body': { 'id': 'd6198807-7a53-4141-b2db-d2cb754d68ba', @@ -1113,7 +1102,6 @@ var multiRequest1 = [ } ]; -// WHEN sent as bidderRequest to buildRequests you should send the child: .bidderRequest var multiBidderRequest1 = { bidderRequest: { 'bidderCode': 'ozone', @@ -1507,7 +1495,6 @@ var multiResponse1 = { describe('ozone Adapter', function () { describe('isBidRequestValid', function () { - // A test ad unit that will consistently return test creatives let validBidReq = { bidder: BIDDER_CODE, params: { @@ -1941,7 +1928,6 @@ describe('ozone Adapter', function () { const request = spec.buildRequests(validBidRequestsNoSizes, validBidderRequest.bidderRequest); expect(request).to.be.a('array'); expect(request[0]).to.have.all.keys(['bidderRequest', 'data', 'method', 'url']); - // need to reset the singleRequest config flag: config.setConfig({'ozone': {'singleRequest': true}}); }); @@ -1965,7 +1951,6 @@ describe('ozone Adapter', function () { expect(payload.user.ext.consent).to.equal(consentString); }); - // mirror it('should add gdpr consent information to the request when vendorData is missing vendorConsents (Mirror)', function () { let consentString = 'BOcocyaOcocyaAfEYDENCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NphLgA=='; let bidderRequest = validBidderRequest.bidderRequest; @@ -2018,7 +2003,6 @@ describe('ozone Adapter', function () { }; let bidRequests = validBidRequests; - // values from http://prebid.org/dev-docs/modules/userId.html#pubcommon-id bidRequests[0]['userId'] = { 'digitrustid': {data: {id: 'DTID', keyv: 4, privacy: {optout: false}, producer: 'ABC', version: 2}}, 'id5id': { uid: '1111', ext: { linkType: 2, abTestingControlGroup: false } }, @@ -2038,13 +2022,11 @@ describe('ozone Adapter', function () { it('should pick up the value of pubcid when built using the pubCommonId module (not userId)', function () { let bidRequests = validBidRequests; - // values from http://prebid.org/dev-docs/modules/userId.html#pubcommon-id bidRequests[0]['userId'] = { 'digitrustid': {data: {id: 'DTID', keyv: 4, privacy: {optout: false}, producer: 'ABC', version: 2}}, 'id5id': { uid: '1111', ext: { linkType: 2, abTestingControlGroup: false } }, 'idl_env': '3333', 'parrableid': 'eidVersion.encryptionKeyReference.encryptedValue', - // 'pubcid': '5555', // remove pubcid from here to emulate the OLD module & cause the failover code to kick in 'tdid': '6666', 'sharedid': {'id': '01EAJWWNEPN3CYMM5N8M5VXY22', 'third': '01EAJWWNEPN3CYMM5N8M5VXY22'} }; @@ -2170,7 +2152,6 @@ describe('ozone Adapter', function () { }); it('should use oztestmode GET value if set', function() { var specMock = utils.deepClone(spec); - // mock the getGetParametersAsObject function to simulate GET parameters for oztestmode: specMock.getGetParametersAsObject = function() { return {'oztestmode': 'mytestvalue_123'}; }; @@ -2181,7 +2162,6 @@ describe('ozone Adapter', function () { }); it('should pass through GET params if present: ozf, ozpf, ozrp, ozip', function() { var specMock = utils.deepClone(spec); - // mock the getGetParametersAsObject function to simulate GET parameters : specMock.getGetParametersAsObject = function() { return {ozf: '1', ozpf: '0', ozrp: '2', ozip: '123'}; }; @@ -2194,7 +2174,6 @@ describe('ozone Adapter', function () { }); it('should pass through GET params if present: ozf, ozpf, ozrp, ozip with alternative values', function() { var specMock = utils.deepClone(spec); - // mock the getGetParametersAsObject function to simulate GET parameters : specMock.getGetParametersAsObject = function() { return {ozf: 'false', ozpf: 'true', ozrp: 'xyz', ozip: 'hello'}; }; @@ -2207,7 +2186,6 @@ describe('ozone Adapter', function () { }); it('should use oztestmode GET value if set, even if there is no customdata in config', function() { var specMock = utils.deepClone(spec); - // mock the getGetParametersAsObject function to simulate GET parameters for oztestmode: specMock.getGetParametersAsObject = function() { return {'oztestmode': 'mytestvalue_123'}; }; @@ -2217,7 +2195,6 @@ describe('ozone Adapter', function () { expect(data.imp[0].ext.ozone.customData[0].targeting.oztestmode).to.equal('mytestvalue_123'); }); it('should use GET values auction=dev & cookiesync=dev if set', function() { - // mock the getGetParametersAsObject function to simulate GET parameters for oztestmode: var specMock = utils.deepClone(spec); specMock.getGetParametersAsObject = function() { return {}; @@ -2228,8 +2205,6 @@ describe('ozone Adapter', function () { let cookieUrl = specMock.getCookieSyncUrl(); expect(cookieUrl).to.equal('https://elb.the-ozone-project.com/static/load-cookie.html'); - // now mock the response from getGetParametersAsObject & do the request again - specMock = utils.deepClone(spec); specMock.getGetParametersAsObject = function() { return {'auction': 'dev', 'cookiesync': 'dev'}; @@ -2241,7 +2216,6 @@ describe('ozone Adapter', function () { expect(cookieUrl).to.equal('https://test.ozpr.net/static/load-cookie.html'); }); it('should use a valid ozstoredrequest GET value if set to override the placementId values, and set oz_rw if we find it', function() { - // mock the getGetParametersAsObject function to simulate GET parameters for ozstoredrequest: var specMock = utils.deepClone(spec); specMock.getGetParametersAsObject = function() { return {'ozstoredrequest': '1122334455'}; // 10 digits are valid @@ -2252,7 +2226,6 @@ describe('ozone Adapter', function () { expect(data.imp[0].ext.prebid.storedrequest.id).to.equal('1122334455'); }); it('should NOT use an invalid ozstoredrequest GET value if set to override the placementId values, and set oz_rw to 0', function() { - // mock the getGetParametersAsObject function to simulate GET parameters for ozstoredrequest: var specMock = utils.deepClone(spec); specMock.getGetParametersAsObject = function() { return {'ozstoredrequest': 'BADVAL'}; // 10 digits are valid @@ -2424,6 +2397,26 @@ describe('ozone Adapter', function () { expect(utils.deepAccess(payload, 'imp.0.floor.banner.floor')).to.equal(0.8); config.resetConfig(); }); + + it('handles schain object in each bidrequest (will be the same in each br)', function () { + let br = JSON.parse(JSON.stringify(validBidRequests)); + let schainConfigObject = { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'bidderA.com', + 'sid': '00001', + 'hp': 1 + } + ] + }; + br[0]['schain'] = schainConfigObject; + const request = spec.buildRequests(br, validBidderRequest.bidderRequest); + const data = JSON.parse(request.data); + expect(data.source.ext).to.haveOwnProperty('schain'); + expect(data.source.ext.schain).to.deep.equal(schainConfigObject); // .deep.equal() : Target object deeply (but not strictly) equals `{a: 1}` + }); }); describe('interpretResponse', function () { @@ -2624,7 +2617,6 @@ describe('ozone Adapter', function () { expect(result[1]['impid']).to.equal('3025f169863b7f8'); expect(result[1]['id']).to.equal('18552976939844999'); expect(result[1]['adserverTargeting']['oz_ozappnexus_adId']).to.equal('3025f169863b7f8-0-oz-2'); - // change the bid values so a different second bid for an impid by the same bidder gets dropped validres = JSON.parse(JSON.stringify(multiResponse1)); validres.body.seatbid[0].bid[1].price = 1.1; validres.body.seatbid[0].bid[1].cpm = 1.1; @@ -2647,7 +2639,6 @@ describe('ozone Adapter', function () { expect(result).to.be.empty; }); it('should append the various values if they exist', function() { - // get the cookie bag populated spec.buildRequests(validBidRequests, validBidderRequest.bidderRequest); const result = spec.getUserSyncs({iframeEnabled: true}, 'good server response', gdpr1); expect(result).to.be.an('array'); @@ -2657,14 +2648,12 @@ describe('ozone Adapter', function () { expect(result[0].url).to.include('gdpr_consent=BOh7mtYOh7mtYAcABBENCU-AAAAncgPIXJiiAoao0PxBFkgCAC8ACIAAQAQQAAIAAAIAAAhBGAAAQAQAEQgAAAAAAABAAAAAAAAAAAAAAACAAAAAAAACgAAAAABAAAAQAAAAAAA'); }); it('should append ccpa (usp data)', function() { - // get the cookie bag populated spec.buildRequests(validBidRequests, validBidderRequest.bidderRequest); const result = spec.getUserSyncs({iframeEnabled: true}, 'good server response', gdpr1, '1YYN'); expect(result).to.be.an('array'); expect(result[0].url).to.include('usp_consent=1YYN'); }); it('should use "" if no usp is sent to cookieSync', function() { - // get the cookie bag populated spec.buildRequests(validBidRequests, validBidderRequest.bidderRequest); const result = spec.getUserSyncs({iframeEnabled: true}, 'good server response', gdpr1); expect(result).to.be.an('array'); @@ -2887,4 +2876,27 @@ describe('ozone Adapter', function () { expect(response[1].bid.length).to.equal(2); }); }); + /** + * spec.getWhitelabelConfigItem test - get a config value for a whitelabelled bidder, + * from a standard ozone.oz_xxxx_yyy string + */ + describe('getWhitelabelConfigItem', function() { + it('should fetch the whitelabelled equivalent config value correctly', function () { + var specMock = utils.deepClone(spec); + config.setConfig({'ozone': {'oz_omp_floor': 'ozone-floor-value'}}); + config.setConfig({'markbidder': {'mb_omp_floor': 'markbidder-floor-value'}}); + specMock.propertyBag.whitelabel = {bidder: 'ozone', keyPrefix: 'oz'}; + let testKey = 'ozone.oz_omp_floor'; + let ozone_value = specMock.getWhitelabelConfigItem(testKey); + expect(ozone_value).to.equal('ozone-floor-value'); + specMock.propertyBag.whitelabel = {bidder: 'markbidder', keyPrefix: 'mb'}; + let markbidder_config = specMock.getWhitelabelConfigItem(testKey); + expect(markbidder_config).to.equal('markbidder-floor-value'); + config.setConfig({'markbidder': {'singleRequest': 'markbidder-singlerequest-value'}}); + let testKey2 = 'ozone.singleRequest'; + let markbidder_config2 = specMock.getWhitelabelConfigItem(testKey2); + expect(markbidder_config2).to.equal('markbidder-singlerequest-value'); + config.resetConfig(); + }); + }); }); diff --git a/test/spec/modules/parrableIdSystem_spec.js b/test/spec/modules/parrableIdSystem_spec.js index 44118fb50de..176ba70fbcb 100644 --- a/test/spec/modules/parrableIdSystem_spec.js +++ b/test/spec/modules/parrableIdSystem_spec.js @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import find from 'core-js-pure/features/array/find.js'; +import {find} from 'src/polyfill.js'; import { config } from 'src/config.js'; import * as utils from 'src/utils.js'; import { newStorageManager } from 'src/storageManager.js'; diff --git a/test/spec/modules/pilotxBidAdapter_spec.js b/test/spec/modules/pilotxBidAdapter_spec.js new file mode 100644 index 00000000000..2ef31c0a8f5 --- /dev/null +++ b/test/spec/modules/pilotxBidAdapter_spec.js @@ -0,0 +1,244 @@ +// import or require modules necessary for the test, e.g.: +import { expect } from 'chai'; // may prefer 'assert' in place of 'expect' +import { spec } from '../../../modules/pilotxBidAdapter.js'; + +describe('pilotxAdapter', function () { + describe('isBidRequestValid', function () { + let banner; + beforeEach(function () { + banner = { + bidder: 'pilotx', + adUnitCode: 'adunit-test', + mediaTypes: { banner: {} }, + sizes: [[300, 250], [468, 60]], + bidId: '2de8c82e30665a', + params: { + placementId: '1' + } + }; + }); + + it('should return false if sizes is empty', function () { + banner.sizes = [] + expect(spec.isBidRequestValid(banner)).to.equal(false); + }); + it('should return true if all is valid/ is not empty', function () { + expect(spec.isBidRequestValid(banner)).to.equal(true); + }); + it('should return false if there is no placement id found', function () { + banner.params = {} + expect(spec.isBidRequestValid(banner)).to.equal(false); + }); + it('should return false if sizes is empty', function () { + banner.sizes = [] + expect(spec.isBidRequestValid(banner)).to.equal(false); + }); + it('should return false for no size and empty params', function() { + const emptySizes = { + bidder: 'pilotx', + adUnitCode: 'adunit-test', + mediaTypes: { banner: {} }, + bidId: '2de8c82e30665a', + params: { + placementId: '1', + sizes: [] + } + }; + expect(spec.isBidRequestValid(emptySizes)).to.equal(false); + }) + it('should return true for no size and valid size params', function() { + const emptySizes = { + bidder: 'pilotx', + adUnitCode: 'adunit-test', + mediaTypes: { banner: {} }, + bidId: '2de8c82e30665a', + params: { + placementId: '1', + sizes: [[300, 250], [468, 60]] + } + }; + expect(spec.isBidRequestValid(emptySizes)).to.equal(true); + }) + it('should return false for no size items', function() { + const emptySizes = { + bidder: 'pilotx', + adUnitCode: 'adunit-test', + mediaTypes: { banner: {} }, + bidId: '2de8c82e30665a', + params: { + placementId: '1' + } + }; + expect(spec.isBidRequestValid(emptySizes)).to.equal(false); + }) + }); + + describe('buildRequests', function () { + const mockRequest = { refererInfo: {} }; + const mockRequestGDPR = { + refererInfo: {}, + gdprConsent: { + consentString: 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==', + gdprApplies: true + } + + } + const mockVideo1 = [{ + adUnitCode: 'video1', + auctionId: '01618029-7ae9-4e98-a73a-1ed0c817f414', + bidId: '2a59588c0114fa', + bidRequestsCount: 1, + bidder: 'pilotx', + bidderRequestId: '1f6b4ba2039726', + bidderRequestsCount: 1, + bidderWinsCount: 0, + crumbs: { pubcid: 'de5240ef-ff80-4b55-8837-26a11cfbf64c' }, + mediaTypes: { + video: { + context: 'instream', + mimes: ['video/mp4'], + playbackmethod: [2], + playerSize: [[640, 480]], + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + skip: 1 + } + }, + ortb2Imp: { + ext: { + data: { + pbadslot: 'video1' + } + } + }, + params: { placementId: '379' }, + sizes: [[640, 480]], + src: 'client', + transactionId: 'fec9f2ff-da13-4921-8437-8d679c2be7fe', + }]; + const mockVideo2 = [{ + adUnitCode: 'video1', + auctionId: '01618029-7ae9-4e98-a73a-1ed0c817f414', + bidId: '2a59588c0114fa', + bidRequestsCount: 1, + bidder: 'pilotx', + bidderRequestId: '1f6b4ba2039726', + bidderRequestsCount: 1, + bidderWinsCount: 0, + crumbs: { pubcid: 'de5240ef-ff80-4b55-8837-26a11cfbf64c' }, + mediaTypes: { + video: { + context: 'instream', + mimes: ['video/mp4'], + playbackmethod: [2], + playerSize: [[640, 480]], + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + skip: 1 + } + }, + ortb2Imp: { + ext: { + data: { + pbadslot: 'video1' + } + } + }, + params: { placementId: '379' }, + sizes: [640, 480], + src: 'client', + transactionId: 'fec9f2ff-da13-4921-8437-8d679c2be7fe', + }]; + it('should return correct response', function () { + const builtRequest = spec.buildRequests(mockVideo1, mockRequest) + let builtRequestData = builtRequest.data + let data = JSON.parse(builtRequestData) + expect(data['379'].bidId).to.equal(mockVideo1[0].bidId) + }); + it('should return correct response for only array of size', function () { + const builtRequest = spec.buildRequests(mockVideo2, mockRequest) + let builtRequestData = builtRequest.data + let data = JSON.parse(builtRequestData) + expect(data['379'].sizes[0][0]).to.equal(mockVideo2[0].sizes[0]) + expect(data['379'].sizes[0][1]).to.equal(mockVideo2[0].sizes[1]) + }); + it('should be valid and pass gdpr items correctly', function () { + const builtRequest = spec.buildRequests(mockVideo2, mockRequestGDPR) + let builtRequestData = builtRequest.data + let data = JSON.parse(builtRequestData) + expect(data['379'].gdprConsentString).to.equal(mockRequestGDPR.gdprConsent.consentString) + expect(data['379'].gdprConsentRequired).to.equal(mockRequestGDPR.gdprConsent.gdprApplies) + }); + }); + describe('interpretResponse', function () { + const bidRequest = {} + const serverResponse = { + cpm: 2.5, + creativeId: 'V9060', + currency: 'US', + height: 480, + mediaType: 'video', + netRevenue: false, + requestId: '273b39c74069cb', + ttl: 3000, + vastUrl: 'http://testadserver.com/ads?&k=60cd901ad8ab70c9cedf373cb17b93b8&pid=379&tid=91342717', + width: 640 + } + const serverResponseVideo = { + body: serverResponse + } + const serverResponse2 = { + cpm: 2.5, + creativeId: 'V9060', + currency: 'US', + height: 480, + mediaType: 'banner', + netRevenue: false, + requestId: '273b39c74069cb', + ttl: 3000, + vastUrl: 'http://testadserver.com/ads?&k=60cd901ad8ab70c9cedf373cb17b93b8&pid=379&tid=91342717', + width: 640 + } + const serverResponseBanner = { + body: serverResponse2 + } + it('should be valid from bidRequest for video', function () { + const bidResponses = spec.interpretResponse(serverResponseVideo, bidRequest) + expect(bidResponses[0].requestId).to.equal(serverResponse.requestId) + expect(bidResponses[0].cpm).to.equal(serverResponse.cpm) + expect(bidResponses[0].width).to.equal(serverResponse.width) + expect(bidResponses[0].height).to.equal(serverResponse.height) + expect(bidResponses[0].creativeId).to.equal(serverResponse.creativeId) + expect(bidResponses[0].currency).to.equal(serverResponse.currency) + expect(bidResponses[0].netRevenue).to.equal(serverResponse.netRevenue) + expect(bidResponses[0].ttl).to.equal(serverResponse.ttl) + expect(bidResponses[0].vastUrl).to.equal(serverResponse.vastUrl) + expect(bidResponses[0].mediaType).to.equal(serverResponse.mediaType) + expect(bidResponses[0].meta.mediaType).to.equal(serverResponse.mediaType) + }); + it('should be valid from bidRequest for banner', function () { + const bidResponses = spec.interpretResponse(serverResponseBanner, bidRequest) + expect(bidResponses[0].requestId).to.equal(serverResponse2.requestId) + expect(bidResponses[0].cpm).to.equal(serverResponse2.cpm) + expect(bidResponses[0].width).to.equal(serverResponse2.width) + expect(bidResponses[0].height).to.equal(serverResponse2.height) + expect(bidResponses[0].creativeId).to.equal(serverResponse2.creativeId) + expect(bidResponses[0].currency).to.equal(serverResponse2.currency) + expect(bidResponses[0].netRevenue).to.equal(serverResponse2.netRevenue) + expect(bidResponses[0].ttl).to.equal(serverResponse2.ttl) + expect(bidResponses[0].ad).to.equal(serverResponse2.ad) + expect(bidResponses[0].mediaType).to.equal(serverResponse2.mediaType) + expect(bidResponses[0].meta.mediaType).to.equal(serverResponse2.mediaType) + }); + }); + describe('setPlacementID', function () { + const multiplePlacementIds = ['380', '381'] + it('should be valid with an array of placement ids passed', function () { + const placementID = spec.setPlacementID(multiplePlacementIds) + expect(placementID).to.equal('380#381') + }); + it('should be valid with single placement ID passed', function () { + const placementID = spec.setPlacementID('381') + expect(placementID).to.equal('381') + }); + }); + // Add other `describe` or `it` blocks as necessary +}); diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index be07e1dcc93..7b297aa4c5a 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -4,10 +4,19 @@ import adapterManager from 'src/adapterManager.js'; import * as utils from 'src/utils.js'; import { ajax } from 'src/ajax.js'; import { config } from 'src/config.js'; -import events from 'src/events.js'; +import * as events from 'src/events.js'; import CONSTANTS from 'src/constants.json'; import { server } from 'test/mocks/xhr.js'; import { createEidsArray } from 'modules/userId/eids.js'; +import {deepAccess, deepClone} from 'src/utils.js'; +import 'modules/appnexusBidAdapter.js' // appnexus alias test +import 'modules/rubiconBidAdapter.js' // rubicon alias test +import 'src/prebid.js' // $$PREBID_GLOBAL$$.aliasBidder test +import 'modules/currency.js' // adServerCurrency test +import {hook} from '../../../src/hook.js'; +import {decorateAdUnitsWithNativeParams} from '../../../src/native.js'; +import {auctionManager} from '../../../src/auctionManager.js'; +import {stubAuctionIndex} from '../../helpers/indexStub.js'; let CONFIG = { accountId: '1', @@ -447,6 +456,16 @@ describe('S2S Adapter', function () { addBidResponse = sinon.spy(), done = sinon.spy(); + function prepRequest(req) { + req.ad_units.forEach((adUnit) => { delete adUnit.nativeParams }); + decorateAdUnitsWithNativeParams(req.ad_units); + } + + before(() => { + hook.ready(); + prepRequest(REQUEST); + }); + beforeEach(function () { config.resetConfig(); adapter = new Adapter(); @@ -1092,6 +1111,19 @@ describe('S2S Adapter', function () { }); }); + it('should not include ext.aspectratios if adunit\'s aspect_ratios do not define radio_width and ratio_height', () => { + const req = deepClone(REQUEST); + req.ad_units[0].mediaTypes.native.icon.aspect_ratios[0] = {'min_width': 1, 'min_height': 2}; + prepRequest(req); + adapter.callBids(req, BID_REQUESTS, addBidResponse, done, ajax); + const nativeReq = JSON.parse(JSON.parse(server.requests[0].requestBody).imp[0].native.request); + const icons = nativeReq.assets.map((a) => a.img).filter((img) => img && img.type === 1); + expect(icons).to.have.length(1); + expect(icons[0].hmin).to.equal(2); + expect(icons[0].wmin).to.equal(1); + expect(deepAccess(icons[0], 'ext.aspectratios')).to.be.undefined; + }) + it('adds site if app is not present', function () { const _config = { s2sConfig: CONFIG, @@ -2391,11 +2423,8 @@ describe('S2S Adapter', function () { }); it('handles OpenRTB native responses', function () { - sinon.stub(utils, 'getBidRequest').returns({ - adUnitCode: 'div-gpt-ad-1460505748561-0', - bidder: 'appnexus', - bidId: '123' - }); + const stub = sinon.stub(auctionManager, 'index'); + stub.get(() => stubAuctionIndex({adUnits: REQUEST.ad_units})); const s2sConfig = Object.assign({}, CONFIG, { endpoint: { p1Consent: 'https://prebidserverurl/openrtb2/auction?querystring=param' @@ -2418,7 +2447,81 @@ describe('S2S Adapter', function () { expect(response).to.have.property('requestId', '123'); expect(response).to.have.property('cpm', 10); - utils.getBidRequest.restore(); + stub.restore(); + }); + + it('does not (by default) allow bids that were not requested', function () { + config.setConfig({ s2sConfig: CONFIG }); + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + const response = deepClone(RESPONSE_OPENRTB); + response.seatbid[0].seat = 'unknown'; + server.requests[0].respond(200, {}, JSON.stringify(response)); + + expect(addBidResponse.called).to.be.false; + }); + + it('allows unrequested bids if config.allowUnknownBidderCodes', function () { + const cfg = {...CONFIG, allowUnknownBidderCodes: true}; + config.setConfig({s2sConfig: cfg}); + adapter.callBids({...REQUEST, s2sConfig: cfg}, BID_REQUESTS, addBidResponse, done, ajax); + const response = deepClone(RESPONSE_OPENRTB); + response.seatbid[0].seat = 'unknown'; + server.requests[0].respond(200, {}, JSON.stringify(response)); + + expect(addBidResponse.calledWith(sinon.match.any, sinon.match({bidderCode: 'unknown'}))).to.be.true; + }); + + it('uses "null" request\'s ID for all responses, when a null request is present', function () { + const cfg = {...CONFIG, allowUnknownBidderCodes: true}; + config.setConfig({s2sConfig: cfg}); + const req = {...REQUEST, s2sConfig: cfg, ad_units: [{...REQUEST.ad_units[0], bids: [{bidder: null, bid_id: 'testId'}]}]}; + const bidReq = {...BID_REQUESTS[0], bidderCode: null, bids: [{...BID_REQUESTS[0].bids[0], bidder: null, bidId: 'testId'}]} + adapter.callBids(req, [bidReq], addBidResponse, done, ajax); + const response = deepClone(RESPONSE_OPENRTB); + response.seatbid[0].seat = 'storedImpression'; + server.requests[0].respond(200, {}, JSON.stringify(response)); + sinon.assert.calledWith(addBidResponse, sinon.match.any, sinon.match({bidderCode: 'storedImpression', requestId: 'testId'})) + }); + + it('copies ortb2Imp to response when there is only a null bid', () => { + const cfg = {...CONFIG}; + config.setConfig({s2sConfig: cfg}); + const ortb2Imp = {ext: {prebid: {storedrequest: 'value'}}}; + const req = {...REQUEST, s2sConfig: cfg, ad_units: [{...REQUEST.ad_units[0], bids: [{bidder: null, bid_id: 'testId'}], ortb2Imp}]}; + const bidReq = {...BID_REQUESTS[0], bidderCode: null, bids: [{...BID_REQUESTS[0].bids[0], bidder: null, bidId: 'testId'}]} + adapter.callBids(req, [bidReq], addBidResponse, done, ajax); + const actual = JSON.parse(server.requests[0].requestBody); + sinon.assert.match(actual.imp[0], sinon.match(ortb2Imp)); + }); + + describe('on sync requested with no cookie', () => { + let cfg, req, csRes; + + beforeEach(() => { + cfg = utils.deepClone(CONFIG); + req = utils.deepClone(REQUEST); + cfg.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; + req.s2sConfig = cfg; + config.setConfig({ s2sConfig: cfg }); + csRes = utils.deepClone(RESPONSE_NO_COOKIE); + }); + + afterEach(() => { + resetSyncedStatus(); + }) + + Object.entries({ + iframe: () => utils.insertUserSyncIframe, + image: () => utils.triggerPixel, + }).forEach(([type, syncer]) => { + it(`passes timeout to ${type} syncs`, () => { + cfg.syncTimeout = 123; + csRes.bidder_status[0].usersync.type = type; + adapter.callBids(req, BID_REQUESTS, addBidResponse, done, ajax); + server.requests[0].respond(200, {}, JSON.stringify(csRes)); + expect(syncer().args[0]).to.include.members([123]); + }); + }); }); }); @@ -2543,21 +2646,6 @@ describe('S2S Adapter', function () { sinon.assert.calledOnce(logErrorSpy); }); - it('should log an error when bidders is missing', function () { - const options = { - accountId: '1', - enabled: true, - timeout: 1000, - adapter: 's2s', - endpoint: { - p1Consent: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction' - } - }; - - config.setConfig({ s2sConfig: options }); - sinon.assert.calledOnce(logErrorSpy); - }); - it('should log an error when endpoint is missing', function () { const options = { accountId: '1', @@ -2830,6 +2918,24 @@ describe('S2S Adapter', function () { expect(requestBid.coopSync).to.be.undefined; }); + it('should set imp banner if ortb2Imp.banner is present', function() { + const consentConfig = { s2sConfig: CONFIG }; + config.setConfig(consentConfig); + const bidRequest = utils.deepClone(REQUEST); + bidRequest.ad_units[0].ortb2Imp = { + banner: { + api: 7 + }, + instl: 1 + }; + + adapter.callBids(bidRequest, BID_REQUESTS, addBidResponse, done, ajax); + const parsedRequestBody = JSON.parse(server.requests[0].requestBody); + + expect(parsedRequestBody.imp[0].banner.api).to.equal(7); + expect(parsedRequestBody.imp[0].instl).to.equal(1); + }); + it('adds debug flag', function () { config.setConfig({debug: true}); diff --git a/test/spec/modules/priceFloors_spec.js b/test/spec/modules/priceFloors_spec.js index b3105dafc39..40e2f796db2 100644 --- a/test/spec/modules/priceFloors_spec.js +++ b/test/spec/modules/priceFloors_spec.js @@ -14,8 +14,12 @@ import { fieldMatchingFunctions, allowedFields } from 'modules/priceFloors.js'; -import events from 'src/events.js'; +import * as events from 'src/events.js'; import * as mockGpt from '../integration/faker/googletag.js'; +import 'src/prebid.js'; +import {createBid} from '../../../src/bidfactory.js'; +import {auctionManager} from '../../../src/auctionManager.js'; +import {stubAuctionIndex} from '../../helpers/indexStub.js'; describe('the price floors module', function () { let logErrorSpy; @@ -109,6 +113,7 @@ describe('the price floors module', function () { bidder: 'rubicon', adUnitCode: 'test_div_1', auctionId: '1234-56-789', + transactionId: 'tr_test_div_1' }; function getAdUnitMock(code = 'adUnit-code') { @@ -401,6 +406,7 @@ describe('the price floors module', function () { }); describe('with gpt enabled', function () { let gptFloorData; + let indexStub, adUnits; beforeEach(function () { gptFloorData = { currency: 'USD', @@ -426,10 +432,13 @@ describe('the price floors module', function () { code: '/12345/sports/basketball', divId: 'test_div_2' }); + indexStub = sinon.stub(auctionManager, 'index'); + indexStub.get(() => stubAuctionIndex({adUnits})) }); afterEach(function () { // reset it so no lingering stuff from other test specs mockGpt.reset(); + indexStub.restore(); }); it('picks the right rule when looking for gptSlot', function () { expect(getFirstMatchingFloor(gptFloorData, basicBidRequest)).to.deep.equal({ @@ -449,9 +458,10 @@ describe('the price floors module', function () { matchingRule: '/12345/sports/basketball' }); }); - it('picks the gptSlot from the bidRequest and does not call the slotMatching', function () { - const newBidRequest1 = { ...basicBidRequest }; - utils.deepSetValue(newBidRequest1, 'ortb2Imp.ext.data.adserver', { + it('picks the gptSlot from the adUnit and does not call the slotMatching', function () { + const newBidRequest1 = { ...basicBidRequest, transactionId: 'au1' }; + adUnits = [{code: newBidRequest1.code, transactionId: 'au1'}]; + utils.deepSetValue(adUnits[0], 'ortb2Imp.ext.data.adserver', { name: 'gam', adslot: '/12345/news/politics' }) @@ -463,8 +473,9 @@ describe('the price floors module', function () { matchingRule: '/12345/news/politics' }); - const newBidRequest2 = { ...basicBidRequest, adUnitCode: 'test_div_2' }; - utils.deepSetValue(newBidRequest2, 'ortb2Imp.ext.data.adserver', { + const newBidRequest2 = { ...basicBidRequest, adUnitCode: 'test_div_2', transactionId: 'au2' }; + adUnits = [{code: newBidRequest2.adUnitCode, transactionId: newBidRequest2.transactionId}]; + utils.deepSetValue(adUnits[0], 'ortb2Imp.ext.data.adserver', { name: 'gam', adslot: '/12345/news/weather' }) @@ -1565,17 +1576,12 @@ describe('the price floors module', function () { }); }); describe('bidResponseHook tests', function () { - let returnedBidResponse; - let bidderRequest = { - bidderCode: 'appnexus', - auctionId: '123456', - bids: [{ - bidder: 'appnexus', - adUnitCode: 'test_div_1', - auctionId: '123456', - bidId: '1111' - }] - }; + const AUCTION_ID = '123456'; + let returnedBidResponse, indexStub; + let adUnit = { + transactionId: 'au', + code: 'test_div_1' + } let basicBidResponse = { bidderCode: 'appnexus', width: 300, @@ -1583,38 +1589,46 @@ describe('the price floors module', function () { cpm: 0.5, mediaType: 'banner', requestId: '1111', + transactionId: 'au', }; beforeEach(function () { returnedBidResponse = {}; + indexStub = sinon.stub(auctionManager, 'index'); + indexStub.get(() => stubAuctionIndex({adUnits: [adUnit]})); + }); + + afterEach(() => { + indexStub.restore(); }); + function runBidResponse(bidResp = basicBidResponse) { let next = (adUnitCode, bid) => { returnedBidResponse = bid; }; - addBidResponseHook.bind({ bidderRequest })(next, bidResp.adUnitCode, bidResp); + addBidResponseHook(next, bidResp.adUnitCode, Object.assign(createBid(CONSTANTS.STATUS.GOOD, {auctionId: AUCTION_ID}), bidResp)); }; it('continues with the auction if not floors data is present without any flooring', function () { runBidResponse(); expect(returnedBidResponse).to.not.haveOwnProperty('floorData'); }); it('if no matching rule it should not floor and should call log warn', function () { - _floorDataForAuction[bidderRequest.auctionId] = utils.deepClone(basicFloorConfig); - _floorDataForAuction[bidderRequest.auctionId].data.values = { 'video': 1.0 }; + _floorDataForAuction[AUCTION_ID] = utils.deepClone(basicFloorConfig); + _floorDataForAuction[AUCTION_ID].data.values = { 'video': 1.0 }; runBidResponse(); expect(returnedBidResponse).to.not.haveOwnProperty('floorData'); expect(logWarnSpy.calledOnce).to.equal(true); }); it('if it finds a rule and floors should update the bid accordingly', function () { - _floorDataForAuction[bidderRequest.auctionId] = utils.deepClone(basicFloorConfig); - _floorDataForAuction[bidderRequest.auctionId].data.values = { 'banner': 1.0 }; + _floorDataForAuction[AUCTION_ID] = utils.deepClone(basicFloorConfig); + _floorDataForAuction[AUCTION_ID].data.values = { 'banner': 1.0 }; runBidResponse(); expect(returnedBidResponse).to.haveOwnProperty('floorData'); expect(returnedBidResponse.status).to.equal(CONSTANTS.BID_STATUS.BID_REJECTED); expect(returnedBidResponse.cpm).to.equal(0); }); it('if it finds a rule and does not floor should update the bid accordingly', function () { - _floorDataForAuction[bidderRequest.auctionId] = utils.deepClone(basicFloorConfig); - _floorDataForAuction[bidderRequest.auctionId].data.values = { 'banner': 0.3 }; + _floorDataForAuction[AUCTION_ID] = utils.deepClone(basicFloorConfig); + _floorDataForAuction[AUCTION_ID].data.values = { 'banner': 0.3 }; runBidResponse(); expect(returnedBidResponse).to.haveOwnProperty('floorData'); expect(returnedBidResponse.floorData).to.deep.equal({ @@ -1636,7 +1650,7 @@ describe('the price floors module', function () { expect(returnedBidResponse.cpm).to.equal(0.5); }); it('if should work with more complex rules and update accordingly', function () { - _floorDataForAuction[bidderRequest.auctionId] = { + _floorDataForAuction[AUCTION_ID] = { ...basicFloorConfig, data: { currency: 'USD', @@ -1722,4 +1736,49 @@ describe('the price floors module', function () { expect(_floorDataForAuction[AUCTION_END_EVENT.auctionId]).to.be.undefined; }); }); + + describe('fieldMatchingFunctions', () => { + let sandbox; + + const req = { + ...basicBidRequest, + } + + const resp = { + transactionId: req.transactionId, + size: [100, 100], + mediaType: 'banner', + } + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + sandbox.stub(auctionManager, 'index').get(() => stubAuctionIndex({ + adUnits: [ + { + code: req.adUnitCode, + transactionId: req.transactionId, + ortb2Imp: {ext: {data: {adserver: {name: 'gam', adslot: 'slot'}}}} + } + ] + })); + }); + + afterEach(() => { + sandbox.restore(); + }) + + Object.entries({ + size: '100x100', + mediaType: resp.mediaType, + gptSlot: 'slot', + domain: 'localhost', + adUnitCode: req.adUnitCode, + }).forEach(([test, expected]) => { + describe(`${test}`, () => { + it('should work with only bidResponse', () => { + expect(fieldMatchingFunctions[test](undefined, resp)).to.eql(expected) + }) + }); + }) + }); }); diff --git a/test/spec/modules/pubgeniusBidAdapter_spec.js b/test/spec/modules/pubgeniusBidAdapter_spec.js index 6568f7aa782..4599eb2a6fa 100644 --- a/test/spec/modules/pubgeniusBidAdapter_spec.js +++ b/test/spec/modules/pubgeniusBidAdapter_spec.js @@ -173,7 +173,7 @@ describe('pubGENIUS adapter', () => { expectedRequest = { method: 'POST', - url: 'https://ortb.adpearl.io/prebid/auction', + url: 'https://auction.adpearl.io/prebid/auction', data: { id: 'fake-auction-id', imp: [ @@ -493,7 +493,7 @@ describe('pubGENIUS adapter', () => { }; expectedSync = { type: 'iframe', - url: 'https://ortb.adpearl.io/usersync/pixels.html?', + url: 'https://auction.adpearl.io/usersync/pixels.html?', }; }); @@ -551,7 +551,7 @@ describe('pubGENIUS adapter', () => { onTimeout(timeoutData); expect(server.requests[0].method).to.equal('POST'); - expect(server.requests[0].url).to.equal('https://ortb.adpearl.io/prebid/events?type=timeout'); + expect(server.requests[0].url).to.equal('https://auction.adpearl.io/prebid/events?type=timeout'); expect(JSON.parse(server.requests[0].requestBody)).to.deep.equal(timeoutData); }); }); diff --git a/test/spec/modules/publinkIdSystem_spec.js b/test/spec/modules/publinkIdSystem_spec.js index cfb5f8ed135..4656afe1585 100644 --- a/test/spec/modules/publinkIdSystem_spec.js +++ b/test/spec/modules/publinkIdSystem_spec.js @@ -5,7 +5,7 @@ import sinon from 'sinon'; import {uspDataHandler} from '../../../src/adapterManager'; import {parseUrl} from '../../../src/utils'; -export const storage = getStorageManager(24); +export const storage = getStorageManager({gvlid: 24}); const TEST_COOKIE_VALUE = 'cookievalue'; describe('PublinkIdSystem', () => { describe('decode', () => { diff --git a/test/spec/modules/pubmaticAnalyticsAdapter_spec.js b/test/spec/modules/pubmaticAnalyticsAdapter_spec.js index c6496ee7fe1..c60b08ae972 100755 --- a/test/spec/modules/pubmaticAnalyticsAdapter_spec.js +++ b/test/spec/modules/pubmaticAnalyticsAdapter_spec.js @@ -7,7 +7,6 @@ import { addBidResponseHook, } from 'modules/currency.js'; -// using es6 "import * as events from 'src/events'" causes the events.getEvents stub not to work... let events = require('src/events'); let ajax = require('src/ajax'); let utils = require('src/utils'); @@ -95,6 +94,9 @@ const BID2 = Object.assign({}, BID, { 'hb_pb': '1.500', 'hb_size': '728x90', 'hb_source': 'server' + }, + meta: { + advertiserDomains: ['example.com'] } }); @@ -382,6 +384,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].di).to.equal('the-deal-id'); expect(data.s[1].ps[0].dc).to.equal('PMP'); expect(data.s[1].ps[0].mi).to.equal('matched-impression'); + expect(data.s[1].ps[0].adv).to.equal('example.com'); expect(data.s[1].ps[0].l1).to.equal(3214); expect(data.s[1].ps[0].l2).to.equal(0); expect(data.s[1].ps[0].ss).to.equal(1); @@ -651,6 +654,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].di).to.equal('the-deal-id'); expect(data.s[1].ps[0].dc).to.equal('PMP'); expect(data.s[1].ps[0].mi).to.equal('matched-impression'); + expect(data.s[1].ps[0].adv).to.equal('example.com'); expect(data.s[1].ps[0].l1).to.equal(3214); expect(data.s[1].ps[0].l2).to.equal(0); expect(data.s[1].ps[0].ss).to.equal(1); @@ -708,6 +712,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].di).to.equal('the-deal-id'); expect(data.s[1].ps[0].dc).to.equal('PMP'); expect(data.s[1].ps[0].mi).to.equal('matched-impression'); + expect(data.s[1].ps[0].adv).to.equal('example.com'); expect(data.s[1].ps[0].l1).to.equal(3214); expect(data.s[1].ps[0].l2).to.equal(0); expect(data.s[1].ps[0].ss).to.equal(1); @@ -754,6 +759,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].di).to.equal('the-deal-id'); expect(data.s[1].ps[0].dc).to.equal('PMP'); expect(data.s[1].ps[0].mi).to.equal('matched-impression'); + expect(data.s[1].ps[0].adv).to.equal('example.com'); expect(data.s[1].ps[0].l1).to.equal(3214); expect(data.s[1].ps[0].l2).to.equal(0); expect(data.s[1].ps[0].ss).to.equal(1); @@ -771,9 +777,10 @@ describe('pubmatic analytics adapter', function () { expect(data.kgpv).to.equal('*'); }); - it('Logger: regexPattern in bid.bidResponse', function() { + it('Logger: regexPattern in bid.bidResponse and url in adomain', function() { const BID2_COPY = utils.deepClone(BID2); BID2_COPY.regexPattern = '*'; + BID2_COPY.meta.advertiserDomains = ['https://www.example.com/abc/223'] events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); @@ -808,6 +815,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].di).to.equal('the-deal-id'); expect(data.s[1].ps[0].dc).to.equal('PMP'); expect(data.s[1].ps[0].mi).to.equal('matched-impression'); + expect(data.s[1].ps[0].adv).to.equal('example.com'); expect(data.s[1].ps[0].l1).to.equal(3214); expect(data.s[1].ps[0].l2).to.equal(0); expect(data.s[1].ps[0].ss).to.equal(1); @@ -859,6 +867,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].di).to.equal('the-deal-id'); expect(data.s[1].ps[0].dc).to.equal('PMP'); expect(data.s[1].ps[0].mi).to.equal('matched-impression'); + expect(data.s[1].ps[0].adv).to.equal('example.com'); expect(data.s[1].ps[0].l1).to.equal(3214); expect(data.s[1].ps[0].l2).to.equal(0); expect(data.s[1].ps[0].ss).to.equal(1); @@ -912,6 +921,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].di).to.equal('the-deal-id'); expect(data.s[1].ps[0].dc).to.equal('PMP'); expect(data.s[1].ps[0].mi).to.equal('matched-impression'); + expect(data.s[1].ps[0].adv).to.equal('example.com'); expect(data.s[1].ps[0].l1).to.equal(3214); expect(data.s[1].ps[0].l2).to.equal(0); expect(data.s[1].ps[0].ss).to.equal(1); @@ -1011,6 +1021,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].di).to.equal('the-deal-id'); expect(data.s[1].ps[0].dc).to.equal('PMP'); expect(data.s[1].ps[0].mi).to.equal('matched-impression'); + expect(data.s[1].ps[0].adv).to.equal('example.com'); expect(data.s[1].ps[0].l1).to.equal(3214); expect(data.s[1].ps[0].l2).to.equal(0); expect(data.s[1].ps[0].ss).to.equal(1); diff --git a/test/spec/modules/pubmaticBidAdapter_spec.js b/test/spec/modules/pubmaticBidAdapter_spec.js index 9696501437b..64e95460321 100644 --- a/test/spec/modules/pubmaticBidAdapter_spec.js +++ b/test/spec/modules/pubmaticBidAdapter_spec.js @@ -3861,4 +3861,80 @@ describe('PubMatic adapter', function () { }) }); }); + + describe('Video request params', function() { + let sandbox, utilsMock, newVideoRequest; + beforeEach(() => { + utilsMock = sinon.mock(utils); + sandbox = sinon.sandbox.create(); + sandbox.spy(utils, 'logWarn'); + newVideoRequest = utils.deepClone(videoBidRequests) + }); + + afterEach(() => { + utilsMock.restore(); + sandbox.restore(); + }) + + it('Should log warning if video params from mediaTypes and params obj of bid are not present', function () { + delete newVideoRequest[0].mediaTypes.video; + delete newVideoRequest[0].params.video; + + let request = spec.buildRequests(newVideoRequest, { + auctionId: 'new-auction-id' + }); + + sinon.assert.calledOnce(utils.logWarn); + expect(request).to.equal(undefined); + }); + + it('Should consider video params from mediaType object of bid', function () { + delete newVideoRequest[0].params.video; + + let request = spec.buildRequests(newVideoRequest, { + auctionId: 'new-auction-id' + }); + let data = JSON.parse(request.data); + expect(data.imp[0].video).to.exist; + expect(data.imp[0]['video']['w']).to.equal(videoBidRequests[0].mediaTypes.video.playerSize[0]); + expect(data.imp[0]['video']['h']).to.equal(videoBidRequests[0].mediaTypes.video.playerSize[1]); + expect(data.imp[0]['video']['battr']).to.equal(undefined); + }); + }); + + describe('GroupM params', function() { + let sandbox, utilsMock, newBidRequests, newBidResponses; + beforeEach(() => { + utilsMock = sinon.mock(utils); + sandbox = sinon.sandbox.create(); + sandbox.spy(utils, 'logInfo'); + newBidRequests = utils.deepClone(bidRequests) + newBidRequests[0].bidder = 'groupm'; + newBidResponses = utils.deepClone(bidResponses); + newBidResponses.body.seatbid[0].bid[0].ext.marketplace = 'groupm' + }); + + afterEach(() => { + utilsMock.restore(); + sandbox.restore(); + }) + + it('Should log info when bidder is groupm and return', function () { + let request = spec.buildRequests(newBidRequests, {bidderCode: 'groupm', + auctionId: 'new-auction-id' + }); + sinon.assert.calledOnce(utils.logInfo); + expect(request).to.equal(undefined); + }); + + it('Should add bidder code & bidder as groupm for marketplace groupm response', function () { + let request = spec.buildRequests(newBidRequests, { + auctionId: 'new-auction-id' + }); + let response = spec.interpretResponse(newBidResponses, request); + expect(response).to.be.an('array').with.length.above(0); + expect(response[0].bidderCode).to.equal('groupm'); + expect(response[0].bidder).to.equal('groupm'); + }); + }); }); diff --git a/test/spec/modules/pubstackAnalyticsAdapter_spec.js b/test/spec/modules/pubstackAnalyticsAdapter_spec.js index e3db334c888..4df25f1665b 100644 --- a/test/spec/modules/pubstackAnalyticsAdapter_spec.js +++ b/test/spec/modules/pubstackAnalyticsAdapter_spec.js @@ -1,7 +1,7 @@ import * as utils from 'src/utils.js'; import pubstackAnalytics from '../../../modules/pubstackAnalyticsAdapter.js'; import adapterManager from 'src/adapterManager'; -import events from 'src/events'; +import * as events from 'src/events'; import constants from 'src/constants.json' describe('Pubstack Analytics Adapter', () => { diff --git a/test/spec/modules/pubxaiAnalyticsAdapter_spec.js b/test/spec/modules/pubxaiAnalyticsAdapter_spec.js index 3d9be082be3..63364b867be 100644 --- a/test/spec/modules/pubxaiAnalyticsAdapter_spec.js +++ b/test/spec/modules/pubxaiAnalyticsAdapter_spec.js @@ -28,6 +28,7 @@ describe('pubxai analytics adapter', function() { }; let location = utils.getWindowLocation(); + let storage = window.top['sessionStorage']; let prebidEvent = { 'auctionInit': { @@ -514,6 +515,11 @@ describe('pubxai analytics adapter', function() { 'path': location.pathname, 'search': location.search }, + 'pmcDetail': { + 'bidDensity': storage.getItem('pbx:dpbid'), + 'maxBid': storage.getItem('pbx:mxbid'), + 'auctionId': storage.getItem('pbx:aucid') + } }; let expectedAfterBid = { @@ -577,6 +583,11 @@ describe('pubxai analytics adapter', function() { 'deviceOS': getOS(), 'browser': getBrowser() }, + 'pmcDetail': { + 'bidDensity': storage.getItem('pbx:dpbid'), + 'maxBid': storage.getItem('pbx:mxbid'), + 'auctionId': storage.getItem('pbx:aucid') + }, 'initOptions': initOptions }; @@ -625,6 +636,11 @@ describe('pubxai analytics adapter', function() { 'statusMessage': 'Bid available', 'timeToRespond': 267 }, + 'pageDetail': { + 'host': location.host, + 'path': location.pathname, + 'search': location.search + }, 'deviceDetail': { 'platform': navigator.platform, 'deviceType': getDeviceType(), diff --git a/test/spec/modules/readpeakBidAdapter_spec.js b/test/spec/modules/readpeakBidAdapter_spec.js index eefd7792a7c..04358fad52b 100644 --- a/test/spec/modules/readpeakBidAdapter_spec.js +++ b/test/spec/modules/readpeakBidAdapter_spec.js @@ -4,9 +4,13 @@ import { config } from 'src/config.js'; import { parseUrl } from 'src/utils.js'; describe('ReadPeakAdapter', function() { - let bidRequest; - let serverResponse; - let serverRequest; + let baseBidRequest; + let bannerBidRequest; + let nativeBidRequest; + let nativeServerResponse; + let nativeServerRequest; + let bannerServerResponse; + let bannerServerRequest; let bidderRequest; beforeEach(function() { @@ -16,15 +20,8 @@ describe('ReadPeakAdapter', function() { } }; - bidRequest = { + baseBidRequest = { bidder: 'readpeak', - nativeParams: { - title: { required: true, len: 200 }, - image: { wmin: 100 }, - sponsoredBy: {}, - body: { required: false }, - cta: { required: false } - }, params: { bidfloor: 5.0, publisherId: '11bc5dd5-7421-4dd8-c926-40fa653bec76', @@ -34,17 +31,46 @@ describe('ReadPeakAdapter', function() { bidId: '2ffb201a808da7', bidderRequestId: '178e34bad3658f', auctionId: 'c45dd708-a418-42ec-b8a7-b70a6c6fab0a', - transactionId: 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b' + transactionId: 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b', + }; + + nativeBidRequest = { + ...baseBidRequest, + nativeParams: { + title: { required: true, len: 200 }, + image: { wmin: 100 }, + sponsoredBy: {}, + body: { required: false }, + cta: { required: false } + }, + mediaTypes: { + native: { + title: { required: true, len: 200 }, + image: { wmin: 100 }, + sponsoredBy: {}, + body: { required: false }, + cta: { required: false } + }, + } }; - serverResponse = { - id: bidRequest.bidderRequestId, + bannerBidRequest = { + ...baseBidRequest, + mediaTypes: { + banner: { + sizes: [[640, 320], [300, 600]], + } + }, + sizes: [[640, 320], [300, 600]], + } + nativeServerResponse = { + id: baseBidRequest.bidderRequestId, cur: 'USD', seatbid: [ { bid: [ { - id: 'bidRequest.bidId', - impid: bidRequest.bidId, + id: 'baseBidRequest.bidId', + impid: baseBidRequest.bidId, price: 0.12, cid: '12', crid: '123', @@ -91,7 +117,30 @@ describe('ReadPeakAdapter', function() { } ] }; - serverRequest = { + bannerServerResponse = { + id: baseBidRequest.bidderRequestId, + cur: 'USD', + seatbid: [ + { + bid: [ + { + id: 'baseBidRequest.bidId', + impid: baseBidRequest.bidId, + price: 0.12, + cid: '12', + crid: '123', + adomain: ['readpeak.com'], + adm: '', + burl: 'https://localhost:8081/url/b?d=0O95O4326I528Ie4d39f94-533d-4577-a579-585fd4c02b0aI0I352e303232363639333139393939393939&c=USD&p=${AUCTION_PRICE}&bad=0-0-95O0O0OdO640360&gc=0', + nurl: 'https://localhost:8081/url/n?d=0O95O4326I528Ie4d39f94-533d-4577-a579-585fd4c02b0aI0I352e303232363639333139393939393939&gc=0', + w: 640, + h: 360, + } + ] + } + ] + }; + nativeServerRequest = { method: 'POST', url: 'http://localhost:60080/header/prebid', data: JSON.stringify({ @@ -101,7 +150,7 @@ describe('ReadPeakAdapter', function() { id: '2ffb201a808da7', native: { request: - '{"assets":[{"id":1,"required":1,"title":{"len":200}},{"id":2,"required":0,"data":{"type":1,"len":50}},{"id":3,"required":0,"img":{"type":3,"wmin":100,"hmin":150}}]}', + '{\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":70}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":150,\"hmin\":150}},{\"id\":4,\"required\":1,\"data\":{\"type\":2,\"len\":120}}]}', ver: '1.1' }, bidfloor: 5, @@ -127,175 +176,362 @@ describe('ReadPeakAdapter', function() { isPrebid: true }) }; + bannerServerRequest = { + method: 'POST', + url: 'http://localhost:60080/header/prebid', + data: JSON.stringify({ + id: '178e34bad3658f', + imp: [ + { + id: '2ffb201a808da7', + bidfloor: 5, + bidfloorcur: 'USD', + tagId: 'test-tag-1', + banner: { + w: 640, + h: 360, + format: [ + { w: 640, h: 360 }, + { w: 320, h: 320 }, + ] + } + } + ], + site: { + publisher: { + id: '11bc5dd5-7421-4dd8-c926-40fa653bec76' + }, + id: '11bc5dd5-7421-4dd8-c926-40fa653bec77', + ref: '', + page: 'http://localhost', + domain: 'localhost' + }, + app: null, + device: { + ua: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/61.0.3163.100 Safari/537.36', + language: 'en-US' + }, + isPrebid: true + }) + }; }); - describe('spec.isBidRequestValid', function() { - it('should return true when the required params are passed', function() { - expect(spec.isBidRequestValid(bidRequest)).to.equal(true); - }); + describe('Native', function() { + describe('spec.isBidRequestValid', function() { + it('should return true when the required params are passed', function() { + expect(spec.isBidRequestValid(nativeBidRequest)).to.equal(true); + }); - it('should return false when the native params are missing', function() { - bidRequest.nativeParams = undefined; - expect(spec.isBidRequestValid(bidRequest)).to.equal(false); - }); + it('should return false when the "publisherId" param is missing', function() { + nativeBidRequest.params = { + bidfloor: 5.0 + }; + expect(spec.isBidRequestValid(nativeBidRequest)).to.equal(false); + }); + + it('should return false when no bid params are passed', function() { + nativeBidRequest.params = {}; + expect(spec.isBidRequestValid(nativeBidRequest)).to.equal(false); + }); - it('should return false when the "publisherId" param is missing', function() { - bidRequest.params = { - bidfloor: 5.0 - }; - expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + it('should return false when a bid request is not passed', function() { + expect(spec.isBidRequestValid()).to.equal(false); + expect(spec.isBidRequestValid({})).to.equal(false); + }); }); - it('should return false when no bid params are passed', function() { - bidRequest.params = {}; - expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + describe('spec.buildRequests', function() { + it('should create a POST request for every bid', function() { + const request = spec.buildRequests([nativeBidRequest], bidderRequest); + expect(request.method).to.equal('POST'); + expect(request.url).to.equal(ENDPOINT); + }); + + it('should attach request data', function() { + config.setConfig({ + currency: { + adServerCurrency: 'EUR' + } + }); + + const request = spec.buildRequests([nativeBidRequest], bidderRequest); + + const data = JSON.parse(request.data); + + expect(data.source.ext.prebid).to.equal('$prebid.version$'); + expect(data.id).to.equal(nativeBidRequest.bidderRequestId); + expect(data.imp[0].bidfloor).to.equal(nativeBidRequest.params.bidfloor); + expect(data.imp[0].bidfloorcur).to.equal('USD'); + expect(data.imp[0].tagId).to.equal('test-tag-1'); + expect(data.site.publisher.id).to.equal(nativeBidRequest.params.publisherId); + expect(data.site.id).to.equal(nativeBidRequest.params.siteId); + expect(data.site.page).to.equal(bidderRequest.refererInfo.referer); + expect(data.site.domain).to.equal(parseUrl(bidderRequest.refererInfo.referer).hostname); + expect(data.device).to.deep.contain({ + ua: navigator.userAgent, + language: navigator.language + }); + expect(data.cur).to.deep.equal(['EUR']); + expect(data.user).to.be.undefined; + expect(data.regs).to.be.undefined; + }); + + it('should get bid floor from module', function() { + const floorModuleData = { + currency: 'USD', + floor: 3.2, + } + nativeBidRequest.getFloor = function () { + return floorModuleData + } + const request = spec.buildRequests([nativeBidRequest], bidderRequest); + + const data = JSON.parse(request.data); + + expect(data.source.ext.prebid).to.equal('$prebid.version$'); + expect(data.id).to.equal(nativeBidRequest.bidderRequestId); + expect(data.imp[0].bidfloor).to.equal(floorModuleData.floor); + expect(data.imp[0].bidfloorcur).to.equal(floorModuleData.currency); + }); + + it('should send gdpr data when gdpr does not apply', function() { + const gdprData = { + gdprConsent: { + gdprApplies: false, + consentString: undefined, + } + } + const request = spec.buildRequests([nativeBidRequest], {...bidderRequest, ...gdprData}); + + const data = JSON.parse(request.data); + + expect(data.user).to.deep.equal({ + ext: { + consent: '' + } + }); + expect(data.regs).to.deep.equal({ + ext: { + gdpr: false + } + }); + }); + + it('should send gdpr data when gdpr applies', function() { + const tcString = 'sometcstring'; + const gdprData = { + gdprConsent: { + gdprApplies: true, + consentString: tcString + } + } + const request = spec.buildRequests([nativeBidRequest], {...bidderRequest, ...gdprData}); + + const data = JSON.parse(request.data); + + expect(data.user).to.deep.equal({ + ext: { + consent: tcString + } + }); + expect(data.regs).to.deep.equal({ + ext: { + gdpr: true + } + }); + }); }); - it('should return false when a bid request is not passed', function() { - expect(spec.isBidRequestValid()).to.equal(false); - expect(spec.isBidRequestValid({})).to.equal(false); + describe('spec.interpretResponse', function() { + it('should return no bids if the response is not valid', function() { + const bidResponse = spec.interpretResponse({ body: null }, nativeServerRequest); + expect(bidResponse.length).to.equal(0); + }); + + it('should return a valid bid response', function() { + const bidResponse = spec.interpretResponse( + { body: nativeServerResponse }, + nativeServerRequest + )[0]; + expect(bidResponse).to.contain({ + requestId: nativeBidRequest.bidId, + cpm: nativeServerResponse.seatbid[0].bid[0].price, + creativeId: nativeServerResponse.seatbid[0].bid[0].crid, + ttl: 300, + netRevenue: true, + mediaType: 'native', + currency: nativeServerResponse.cur + }); + + expect(bidResponse.meta).to.deep.equal({ + advertiserDomains: ['readpeak.com'], + }) + expect(bidResponse.native.title).to.equal('Title'); + expect(bidResponse.native.body).to.equal('Description'); + expect(bidResponse.native.image).to.deep.equal({ + url: 'http://url.to/image', + width: 750, + height: 500 + }); + expect(bidResponse.native.clickUrl).to.equal( + 'http%3A%2F%2Furl.to%2Ftarget' + ); + expect(bidResponse.native.impressionTrackers).to.contain( + 'http://url.to/pixeltracker' + ); + }); }); }); - describe('spec.buildRequests', function() { - it('should create a POST request for every bid', function() { - const request = spec.buildRequests([bidRequest], bidderRequest); - expect(request.method).to.equal('POST'); - expect(request.url).to.equal(ENDPOINT); - }); + describe('Banner', function() { + describe('spec.isBidRequestValid', function() { + it('should return true when the required params are passed', function() { + expect(spec.isBidRequestValid(bannerBidRequest)).to.equal(true); + }); - it('should attach request data', function() { - config.setConfig({ - currency: { - adServerCurrency: 'EUR' - } + it('should return false when the "publisherId" param is missing', function() { + bannerBidRequest.params = { + bidfloor: 5.0 + }; + expect(spec.isBidRequestValid(bannerBidRequest)).to.equal(false); }); - const request = spec.buildRequests([bidRequest], bidderRequest); - - const data = JSON.parse(request.data); - - expect(data.source.ext.prebid).to.equal('$prebid.version$'); - expect(data.id).to.equal(bidRequest.bidderRequestId); - expect(data.imp[0].bidfloor).to.equal(bidRequest.params.bidfloor); - expect(data.imp[0].bidfloorcur).to.equal('USD'); - expect(data.imp[0].tagId).to.equal('test-tag-1'); - expect(data.site.publisher.id).to.equal(bidRequest.params.publisherId); - expect(data.site.id).to.equal(bidRequest.params.siteId); - expect(data.site.page).to.equal(bidderRequest.refererInfo.referer); - expect(data.site.domain).to.equal(parseUrl(bidderRequest.refererInfo.referer).hostname); - expect(data.device).to.deep.contain({ - ua: navigator.userAgent, - language: navigator.language + it('should return false when no bid params are passed', function() { + bannerBidRequest.params = {}; + expect(spec.isBidRequestValid(bannerBidRequest)).to.equal(false); }); - expect(data.cur).to.deep.equal(['EUR']); - expect(data.user).to.be.undefined; - expect(data.regs).to.be.undefined; }); - it('should get bid floor from module', function() { - const floorModuleData = { - currency: 'USD', - floor: 3.2, - } - bidRequest.getFloor = function () { - return floorModuleData - } - const request = spec.buildRequests([bidRequest], bidderRequest); + describe('spec.buildRequests', function() { + it('should create a POST request for every bid', function() { + const request = spec.buildRequests([bannerBidRequest], bidderRequest); + expect(request.method).to.equal('POST'); + expect(request.url).to.equal(ENDPOINT); + }); - const data = JSON.parse(request.data); + it('should attach request data', function() { + config.setConfig({ + currency: { + adServerCurrency: 'EUR' + } + }); - expect(data.source.ext.prebid).to.equal('$prebid.version$'); - expect(data.id).to.equal(bidRequest.bidderRequestId); - expect(data.imp[0].bidfloor).to.equal(floorModuleData.floor); - expect(data.imp[0].bidfloorcur).to.equal(floorModuleData.currency); - }); + const request = spec.buildRequests([bannerBidRequest], bidderRequest); - it('should send gdpr data when gdpr does not apply', function() { - const gdprData = { - gdprConsent: { - gdprApplies: false, - consentString: undefined, - } - } - const request = spec.buildRequests([bidRequest], {...bidderRequest, ...gdprData}); + const data = JSON.parse(request.data); - const data = JSON.parse(request.data); + expect(data.source.ext.prebid).to.equal('$prebid.version$'); + expect(data.id).to.equal(bannerBidRequest.bidderRequestId); + expect(data.imp[0].bidfloor).to.equal(bannerBidRequest.params.bidfloor); + expect(data.imp[0].bidfloorcur).to.equal('USD'); + expect(data.imp[0].tagId).to.equal('test-tag-1'); + expect(data.site.publisher.id).to.equal(bannerBidRequest.params.publisherId); + expect(data.site.id).to.equal(bannerBidRequest.params.siteId); + expect(data.site.page).to.equal(bidderRequest.refererInfo.referer); + expect(data.site.domain).to.equal(parseUrl(bidderRequest.refererInfo.referer).hostname); + expect(data.device).to.deep.contain({ + ua: navigator.userAgent, + language: navigator.language + }); + expect(data.cur).to.deep.equal(['EUR']); + expect(data.user).to.be.undefined; + expect(data.regs).to.be.undefined; + }); - expect(data.user).to.deep.equal({ - ext: { - consent: '' + it('should get bid floor from module', function() { + const floorModuleData = { + currency: 'USD', + floor: 3.2, } - }); - expect(data.regs).to.deep.equal({ - ext: { - gdpr: false + bannerBidRequest.getFloor = function () { + return floorModuleData } + const request = spec.buildRequests([bannerBidRequest], bidderRequest); + + const data = JSON.parse(request.data); + + expect(data.source.ext.prebid).to.equal('$prebid.version$'); + expect(data.id).to.equal(bannerBidRequest.bidderRequestId); + expect(data.imp[0].bidfloor).to.equal(floorModuleData.floor); + expect(data.imp[0].bidfloorcur).to.equal(floorModuleData.currency); }); - }); - it('should send gdpr data when gdpr applies', function() { - const tcString = 'sometcstring'; - const gdprData = { - gdprConsent: { - gdprApplies: true, - consentString: tcString + it('should send gdpr data when gdpr does not apply', function() { + const gdprData = { + gdprConsent: { + gdprApplies: false, + consentString: undefined, + } } - } - const request = spec.buildRequests([bidRequest], {...bidderRequest, ...gdprData}); + const request = spec.buildRequests([bannerBidRequest], {...bidderRequest, ...gdprData}); - const data = JSON.parse(request.data); + const data = JSON.parse(request.data); - expect(data.user).to.deep.equal({ - ext: { - consent: tcString - } + expect(data.user).to.deep.equal({ + ext: { + consent: '' + } + }); + expect(data.regs).to.deep.equal({ + ext: { + gdpr: false + } + }); }); - expect(data.regs).to.deep.equal({ - ext: { - gdpr: true + + it('should send gdpr data when gdpr applies', function() { + const tcString = 'sometcstring'; + const gdprData = { + gdprConsent: { + gdprApplies: true, + consentString: tcString + } } - }); - }); - }); + const request = spec.buildRequests([bannerBidRequest], {...bidderRequest, ...gdprData}); + + const data = JSON.parse(request.data); - describe('spec.interpretResponse', function() { - it('should return no bids if the response is not valid', function() { - const bidResponse = spec.interpretResponse({ body: null }, serverRequest); - expect(bidResponse.length).to.equal(0); + expect(data.user).to.deep.equal({ + ext: { + consent: tcString + } + }); + expect(data.regs).to.deep.equal({ + ext: { + gdpr: true + } + }); + }); }); - it('should return a valid bid response', function() { - const bidResponse = spec.interpretResponse( - { body: serverResponse }, - serverRequest - )[0]; - expect(bidResponse).to.contain({ - requestId: bidRequest.bidId, - cpm: serverResponse.seatbid[0].bid[0].price, - creativeId: serverResponse.seatbid[0].bid[0].crid, - ttl: 300, - netRevenue: true, - mediaType: 'native', - currency: serverResponse.cur + describe('spec.interpretResponse', function() { + it('should return no bids if the response is not valid', function() { + const bidResponse = spec.interpretResponse({ body: null }, bannerServerRequest); + expect(bidResponse.length).to.equal(0); }); - expect(bidResponse.meta).to.deep.equal({ - advertiserDomains: ['readpeak.com'], - }) - expect(bidResponse.native.title).to.equal('Title'); - expect(bidResponse.native.body).to.equal('Description'); - expect(bidResponse.native.image).to.deep.equal({ - url: 'http://url.to/image', - width: 750, - height: 500 + it('should return a valid bid response', function() { + const bidResponse = spec.interpretResponse( + { body: bannerServerResponse }, + bannerServerRequest + )[0]; + expect(bidResponse).to.contain({ + requestId: bannerBidRequest.bidId, + cpm: bannerServerResponse.seatbid[0].bid[0].price, + creativeId: bannerServerResponse.seatbid[0].bid[0].crid, + ttl: 300, + netRevenue: true, + mediaType: 'banner', + currency: bannerServerResponse.cur, + ad: bannerServerResponse.seatbid[0].bid[0].adm, + width: bannerServerResponse.seatbid[0].bid[0].w, + height: bannerServerResponse.seatbid[0].bid[0].h, + }); + expect(bidResponse.meta).to.deep.equal({ + advertiserDomains: ['readpeak.com'], + }); }); - expect(bidResponse.native.clickUrl).to.equal( - 'http%3A%2F%2Furl.to%2Ftarget' - ); - expect(bidResponse.native.impressionTrackers).to.contain( - 'http://url.to/pixeltracker' - ); }); }); }); diff --git a/test/spec/modules/realTimeDataModule_spec.js b/test/spec/modules/realTimeDataModule_spec.js index b84aef15feb..daeeb9bc47c 100644 --- a/test/spec/modules/realTimeDataModule_spec.js +++ b/test/spec/modules/realTimeDataModule_spec.js @@ -1,6 +1,9 @@ import * as rtdModule from 'modules/rtdModule/index.js'; -import { config } from 'src/config.js'; +import {config} from 'src/config.js'; import * as sinon from 'sinon'; +import {default as CONSTANTS} from '../../../src/constants.json'; +import * as events from '../../../src/events.js'; +import 'src/prebid.js'; const getBidRequestDataSpy = sinon.spy(); @@ -58,33 +61,136 @@ const conf = { }; describe('Real time module', function () { - before(function () { - rtdModule.attachRealTimeDataProvider(validSM); - rtdModule.attachRealTimeDataProvider(invalidSM); - rtdModule.attachRealTimeDataProvider(failureSM); - rtdModule.attachRealTimeDataProvider(nonConfSM); - rtdModule.attachRealTimeDataProvider(validSMWait); - }); + let eventHandlers; + let sandbox; - after(function () { - config.resetConfig(); - }); + function mockEmitEvent(event, ...args) { + (eventHandlers[event] || []).forEach((h) => h(...args)); + } - beforeEach(function () { - config.setConfig(conf); + before(() => { + eventHandlers = {}; + sandbox = sinon.sandbox.create(); + sandbox.stub(events, 'on').callsFake((event, handler) => { + if (!eventHandlers.hasOwnProperty(event)) { + eventHandlers[event] = []; + } + eventHandlers[event].push(handler); + }); }); - it('should use only valid modules', function () { - rtdModule.init(config); - expect(rtdModule.subModules).to.eql([validSMWait, validSM]); + after(() => { + sandbox.restore(); }); - it('should be able to modify bid request', function (done) { - rtdModule.setBidRequestsData(() => { - assert(getBidRequestDataSpy.calledTwice); - assert(getBidRequestDataSpy.calledWith({bidRequest: {}})); + describe('', () => { + const PROVIDERS = [validSM, invalidSM, failureSM, nonConfSM, validSMWait]; + let _detachers; + + beforeEach(function () { + _detachers = PROVIDERS.map(rtdModule.attachRealTimeDataProvider); + rtdModule.init(config); + config.setConfig(conf); + }); + + afterEach(function () { + _detachers.forEach((f) => f()); + config.resetConfig(); + }); + + it('should use only valid modules', function () { + expect(rtdModule.subModules).to.eql([validSMWait, validSM]); + }); + + it('should be able to modify bid request', function (done) { + rtdModule.setBidRequestsData(() => { + assert(getBidRequestDataSpy.calledTwice); + assert(getBidRequestDataSpy.calledWith({bidRequest: {}})); + done(); + }, {bidRequest: {}}) + }); + + it('sould place targeting on adUnits', function (done) { + const auction = { + adUnitCodes: ['ad1', 'ad2'], + adUnits: [ + { + code: 'ad1' + }, + { + code: 'ad2', + adserverTargeting: {preKey: 'preValue'} + } + ] + }; + + const expectedAdUnits = [ + { + code: 'ad1', + adserverTargeting: {key: 'validSMWait'} + }, + { + code: 'ad2', + adserverTargeting: { + preKey: 'preValue', + key: 'validSM' + } + } + ]; + + const adUnits = rtdModule.getAdUnitTargeting(auction); + assert.deepEqual(expectedAdUnits, adUnits) done(); - }, {bidRequest: {}}) + }); + + describe('setBidRequestData', () => { + let withWait, withoutWait; + + function runSetBidRequestData() { + return new Promise((resolve) => { + rtdModule.setBidRequestsData(resolve, {bidRequest: {}}); + }); + } + + beforeEach(() => { + withWait = { + submod: validSMWait, + cbTime: 0, + cbRan: false + }; + withoutWait = { + submod: validSM, + cbTime: 0, + cbRan: false + }; + + [withWait, withoutWait].forEach((c) => { + c.submod.getBidRequestData = sinon.stub().callsFake((_, cb) => { + setTimeout(() => { + c.cbRan = true; + cb(); + }, c.cbTime); + }); + }); + }); + + it('should allow non-priority submodules to run synchronously', () => { + withWait.cbTime = withoutWait.cbTime = 0; + return runSetBidRequestData().then(() => { + expect(withWait.cbRan).to.be.true; + expect(withoutWait.cbRan).to.be.true; + }) + }); + + it('should not wait for non-priority submodules if priority ones complete first', () => { + withWait.cbTime = 10; + withoutWait.cbTime = 100; + return runSetBidRequestData().then(() => { + expect(withWait.cbRan).to.be.true; + expect(withoutWait.cbRan).to.be.false; + }); + }); + }); }); it('deep merge object', function () { @@ -125,36 +231,78 @@ describe('Real time module', function () { assert.deepEqual(expected, merged); }); - it('sould place targeting on adUnits', function (done) { - const auction = { - adUnitCodes: ['ad1', 'ad2'], - adUnits: [ - { - code: 'ad1' - }, - { - code: 'ad2', - adserverTargeting: {preKey: 'preValue'} - } - ] + describe('event', () => { + const EVENTS = { + [CONSTANTS.EVENTS.AUCTION_INIT]: 'onAuctionInitEvent', + [CONSTANTS.EVENTS.AUCTION_END]: 'onAuctionEndEvent', + [CONSTANTS.EVENTS.BID_RESPONSE]: 'onBidResponseEvent', + [CONSTANTS.EVENTS.BID_REQUESTED]: 'onBidRequestEvent' + } + const conf = { + 'realTimeData': { + dataProviders: [ + { + 'name': 'tp1', + }, + { + 'name': 'tp2', + } + ] + } }; + let providers; + let _detachers; - const expectedAdUnits = [ - { - code: 'ad1', - adserverTargeting: {key: 'validSMWait'} - }, - { - code: 'ad2', - adserverTargeting: { - preKey: 'preValue', - key: 'validSM' - } + function eventHandlingProvider(name) { + const provider = { + name: name, + init: () => true, } - ]; + Object.values(EVENTS).forEach((ev) => provider[ev] = sinon.spy()); + return provider; + } + + beforeEach(() => { + providers = [eventHandlingProvider('tp1'), eventHandlingProvider('tp2')]; + _detachers = providers.map(rtdModule.attachRealTimeDataProvider); + rtdModule.init(config); + config.setConfig(conf); + }); - const adUnits = rtdModule.getAdUnitTargeting(auction); - assert.deepEqual(expectedAdUnits, adUnits) - done(); - }) + afterEach(() => { + _detachers.forEach((d) => d()) + config.resetConfig(); + }); + + it('should set targeting for auctionEnd', () => { + providers.forEach(p => p.getTargetingData = sinon.spy()); + const auction = { + adUnitCodes: ['a1'], + adUnits: [{code: 'a1'}] + }; + mockEmitEvent(CONSTANTS.EVENTS.AUCTION_END, auction); + providers.forEach(p => { + expect(p.getTargetingData.calledWith(auction.adUnitCodes)).to.be.true; + }); + }); + + Object.entries(EVENTS).forEach(([event, hook]) => { + it(`'${event}' should be propagated to providers through '${hook}'`, () => { + const eventArg = {}; + mockEmitEvent(event, eventArg); + providers.forEach((provider) => { + const providerConf = conf.realTimeData.dataProviders.find((cfg) => cfg.name === provider.name); + expect(provider[hook].called).to.be.true; + expect(provider[hook].args).to.have.length(1); + expect(provider[hook].args[0]).to.include.members([eventArg, providerConf]) + }) + }); + + it(`${event} should not fail to propagate elsewhere if a provider throws in its event handler`, () => { + providers[0][hook] = function () { throw new Error() }; + mockEmitEvent(event); + expect(providers[1][hook].called).to.be.true; + }); + }); + }); }); diff --git a/test/spec/modules/relaidoBidAdapter_spec.js b/test/spec/modules/relaidoBidAdapter_spec.js index 868b1856c34..f0d381ee3ed 100644 --- a/test/spec/modules/relaidoBidAdapter_spec.js +++ b/test/spec/modules/relaidoBidAdapter_spec.js @@ -57,23 +57,34 @@ describe('RelaidoAdapter', function () { serverResponse = { body: { status: 'ok', - price: 500, - model: 'vcpm', - currency: 'JPY', - creativeId: 1000, - uuid: relaido_uuid, - vast: '', + ads: [{ + placementId: 100000, + width: 640, + height: 360, + bidId: '2ed93003f7bb99', + price: 500, + model: 'vcpm', + currency: 'JPY', + creativeId: 1000, + vast: '', + syncUrl: 'https://relaido/sync.html', + adomain: ['relaido.co.jp', 'www.cmertv.co.jp'], + mediaType: 'video' + }], playerUrl: 'https://relaido/player.js', - syncUrl: 'https://relaido/sync.html', - adomain: ['relaido.co.jp', 'www.cmertv.co.jp'] + syncUrl: 'https://api-dev.ulizaex.com/tr/v1/prebid/sync.html', + uuid: relaido_uuid, } }; serverRequest = { - method: 'GET', - bidId: bidRequest.bidId, - width: bidRequest.mediaTypes.video.playerSize[0][0], - height: bidRequest.mediaTypes.video.playerSize[0][1], - mediaType: 'video', + method: 'POST', + data: { + bids: [{ + bidId: bidRequest.bidId, + width: bidRequest.mediaTypes.video.playerSize[0][0], + height: bidRequest.mediaTypes.video.playerSize[0][1], + mediaType: 'video'}] + } }; }); @@ -195,28 +206,23 @@ describe('RelaidoAdapter', function () { describe('spec.buildRequests', function () { it('should build bid requests by video', function () { const bidRequests = spec.buildRequests([bidRequest], bidderRequest); - expect(bidRequests).to.have.lengthOf(1); - const request = bidRequests[0]; - expect(request.method).to.equal('GET'); - expect(request.url).to.equal('https://api.relaido.jp/bid/v1/prebid/100000'); - expect(request.bidId).to.equal(bidRequest.bidId); - expect(request.width).to.equal(bidRequest.mediaTypes.video.playerSize[0][0]); - expect(request.height).to.equal(bidRequest.mediaTypes.video.playerSize[0][1]); - expect(request.mediaType).to.equal('video'); - expect(request.data.ref).to.equal(bidderRequest.refererInfo.referer); - expect(request.data.timeout_ms).to.equal(bidderRequest.timeout); - expect(request.data.ad_unit_code).to.equal(bidRequest.adUnitCode); - expect(request.data.auction_id).to.equal(bidRequest.auctionId); - expect(request.data.bidder).to.equal(bidRequest.bidder); - expect(request.data.bidder_request_id).to.equal(bidRequest.bidderRequestId); - expect(request.data.bid_requests_count).to.equal(bidRequest.bidRequestsCount); - expect(request.data.bid_id).to.equal(bidRequest.bidId); - expect(request.data.transaction_id).to.equal(bidRequest.transactionId); - expect(request.data.media_type).to.equal('video'); - expect(request.data.uuid).to.equal(relaido_uuid); - expect(request.data.width).to.equal(bidRequest.mediaTypes.video.playerSize[0][0]); - expect(request.data.height).to.equal(bidRequest.mediaTypes.video.playerSize[0][1]); - expect(request.data.pv).to.equal('$prebid.version$'); + const data = JSON.parse(bidRequests.data); + expect(data.bids).to.have.lengthOf(1); + const request = data.bids[0]; + expect(bidRequests.method).to.equal('POST'); + expect(bidRequests.url).to.equal('https://api.relaido.jp/bid/v1/sprebid'); + expect(data.ref).to.equal(bidderRequest.refererInfo.referer); + expect(data.timeout_ms).to.equal(bidderRequest.timeout); + expect(request.ad_unit_code).to.equal(bidRequest.adUnitCode); + expect(request.auction_id).to.equal(bidRequest.auctionId); + expect(data.bidder).to.equal(bidRequest.bidder); + expect(request.bidder_request_id).to.equal(bidRequest.bidderRequestId); + expect(data.bid_requests_count).to.equal(bidRequest.bidRequestsCount); + expect(request.bid_id).to.equal(bidRequest.bidId); + expect(request.transaction_id).to.equal(bidRequest.transactionId); + expect(request.media_type).to.equal('video'); + expect(data.uuid).to.equal(relaido_uuid); + expect(data.pv).to.equal('$prebid.version$'); }); it('should build bid requests by banner', function () { @@ -236,9 +242,10 @@ describe('RelaidoAdapter', function () { } }; const bidRequests = spec.buildRequests([bidRequest], bidderRequest); - expect(bidRequests).to.have.lengthOf(1); - const request = bidRequests[0]; - expect(request.mediaType).to.equal('banner'); + const data = JSON.parse(bidRequests.data); + expect(data.bids).to.have.lengthOf(1); + const request = data.bids[0]; + expect(request.media_type).to.equal('banner'); }); it('should take 1x1 size', function () { @@ -258,17 +265,18 @@ describe('RelaidoAdapter', function () { } }; const bidRequests = spec.buildRequests([bidRequest], bidderRequest); - expect(bidRequests).to.have.lengthOf(1); - const request = bidRequests[0]; + const data = JSON.parse(bidRequests.data); + expect(data.bids).to.have.lengthOf(1); + const request = data.bids[0]; expect(request.width).to.equal(1); }); it('The referrer should be the last', function () { const bidRequests = spec.buildRequests([bidRequest], bidderRequest); - expect(bidRequests).to.have.lengthOf(1); - const request = bidRequests[0]; - const keys = Object.keys(request.data); + const data = JSON.parse(bidRequests.data); + expect(data.bids).to.have.lengthOf(1); + const keys = Object.keys(data); expect(keys[0]).to.equal('version'); expect(keys[keys.length - 1]).to.equal('ref'); }); @@ -277,9 +285,9 @@ describe('RelaidoAdapter', function () { bidRequest.userId = {} bidRequest.userId.imuid = 'i.tjHcK_7fTcqnbrS_YA2vaw'; const bidRequests = spec.buildRequests([bidRequest], bidderRequest); - expect(bidRequests).to.have.lengthOf(1); - const request = bidRequests[0]; - expect(request.data.imuid).to.equal('i.tjHcK_7fTcqnbrS_YA2vaw'); + const data = JSON.parse(bidRequests.data); + expect(data.bids).to.have.lengthOf(1); + expect(data.imuid).to.equal('i.tjHcK_7fTcqnbrS_YA2vaw'); }); }); @@ -288,35 +296,54 @@ describe('RelaidoAdapter', function () { const bidResponses = spec.interpretResponse(serverResponse, serverRequest); expect(bidResponses).to.have.lengthOf(1); const response = bidResponses[0]; - expect(response.requestId).to.equal(serverRequest.bidId); - expect(response.width).to.equal(serverRequest.width); - expect(response.height).to.equal(serverRequest.height); - expect(response.cpm).to.equal(serverResponse.body.price); - expect(response.currency).to.equal(serverResponse.body.currency); - expect(response.creativeId).to.equal(serverResponse.body.creativeId); - expect(response.vastXml).to.equal(serverResponse.body.vast); - expect(response.meta.advertiserDomains).to.equal(serverResponse.body.adomain); + expect(response.requestId).to.equal(serverRequest.data.bids[0].bidId); + expect(response.width).to.equal(serverRequest.data.bids[0].width); + expect(response.height).to.equal(serverRequest.data.bids[0].height); + expect(response.cpm).to.equal(serverResponse.body.ads[0].price); + expect(response.currency).to.equal(serverResponse.body.ads[0].currency); + expect(response.creativeId).to.equal(serverResponse.body.ads[0].creativeId); + expect(response.vastXml).to.equal(serverResponse.body.ads[0].vast); + expect(response.playerUrl).to.equal(serverResponse.body.playerUrl); + expect(response.meta.advertiserDomains).to.equal(serverResponse.body.ads[0].adomain); expect(response.meta.mediaType).to.equal(VIDEO); expect(response.ad).to.be.undefined; }); it('should build bid response by banner', function () { - serverRequest.mediaType = 'banner'; + serverResponse.body.ads[0].mediaType = 'banner'; const bidResponses = spec.interpretResponse(serverResponse, serverRequest); expect(bidResponses).to.have.lengthOf(1); const response = bidResponses[0]; - expect(response.requestId).to.equal(serverRequest.bidId); - expect(response.width).to.equal(serverRequest.width); - expect(response.height).to.equal(serverRequest.height); - expect(response.cpm).to.equal(serverResponse.body.price); - expect(response.currency).to.equal(serverResponse.body.currency); - expect(response.creativeId).to.equal(serverResponse.body.creativeId); + expect(response.requestId).to.equal(serverRequest.data.bids[0].bidId); + expect(response.width).to.equal(serverRequest.data.bids[0].width); + expect(response.height).to.equal(serverRequest.data.bids[0].height); + expect(response.cpm).to.equal(serverResponse.body.ads[0].price); + expect(response.currency).to.equal(serverResponse.body.ads[0].currency); + expect(response.creativeId).to.equal(serverResponse.body.ads[0].creativeId); expect(response.vastXml).to.be.undefined; + expect(response.playerUrl).to.equal(serverResponse.body.playerUrl); expect(response.ad).to.include(`
`); expect(response.ad).to.include(``); expect(response.ad).to.include(`window.RelaidoPlayer.renderAd`); }); + it('should build bid response by video and playerUrl in ads', function () { + serverResponse.body.ads[0].playerUrl = 'https://relaido/player-customized.js'; + const bidResponses = spec.interpretResponse(serverResponse, serverRequest); + expect(bidResponses).to.have.lengthOf(1); + const response = bidResponses[0]; + expect(response.playerUrl).to.equal(serverResponse.body.ads[0].playerUrl); + }); + + it('should build bid response by banner and playerUrl in ads', function () { + serverResponse.body.ads[0].playerUrl = 'https://relaido/player-customized.js'; + serverResponse.body.ads[0].mediaType = 'banner'; + const bidResponses = spec.interpretResponse(serverResponse, serverRequest); + expect(bidResponses).to.have.lengthOf(1); + const response = bidResponses[0]; + expect(response.playerUrl).to.equal(serverResponse.body.ads[0].playerUrl); + }); + it('should not build bid response', function () { serverResponse = {}; const bidResponses = spec.interpretResponse(serverResponse, serverRequest); @@ -370,8 +397,8 @@ describe('RelaidoAdapter', function () { it('Should create nurl pixel if bid nurl', function () { let bid = { bidder: bidRequest.bidder, - creativeId: serverResponse.body.creativeId, - cpm: serverResponse.body.price, + creativeId: serverResponse.body.ads[0].creativeId, + cpm: serverResponse.body.ads[0].price, params: [bidRequest.params], auctionId: bidRequest.auctionId, requestId: bidRequest.bidId, diff --git a/test/spec/modules/relevantAnalyticsAdapter_spec.js b/test/spec/modules/relevantAnalyticsAdapter_spec.js index 3e31db2d7dc..5c818fe01d4 100644 --- a/test/spec/modules/relevantAnalyticsAdapter_spec.js +++ b/test/spec/modules/relevantAnalyticsAdapter_spec.js @@ -1,6 +1,6 @@ import relevantAnalytics from '../../../modules/relevantAnalyticsAdapter.js'; import adapterManager from 'src/adapterManager'; -import events from 'src/events'; +import * as events from 'src/events'; import constants from 'src/constants.json' import { expect } from 'chai'; diff --git a/test/spec/modules/richaudienceBidAdapter_spec.js b/test/spec/modules/richaudienceBidAdapter_spec.js index 00ae55823b0..9e9366072be 100644 --- a/test/spec/modules/richaudienceBidAdapter_spec.js +++ b/test/spec/modules/richaudienceBidAdapter_spec.js @@ -791,6 +791,46 @@ describe('Richaudience adapter tests', function () { })).to.equal(true); }); + it('should pass schain', function() { + let schain = { + 'ver': '1.0', + 'complete': 1, + 'nodes': [{ + 'asi': 'richaudience.com', + 'sid': '00001', + 'hp': 1 + }, { + 'asi': 'richaudience-2.com', + 'sid': '00002', + 'hp': 1 + }] + } + + DEFAULT_PARAMS_NEW_SIZES[0].schain = { + 'ver': '1.0', + 'complete': 1, + 'nodes': [{ + 'asi': 'richaudience.com', + 'sid': '00001', + 'hp': 1 + }, { + 'asi': 'richaudience-2.com', + 'sid': '00002', + 'hp': 1 + }] + } + + const request = spec.buildRequests(DEFAULT_PARAMS_NEW_SIZES, { + gdprConsent: { + consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', + gdprApplies: true + }, + refererInfo: {} + }) + const requestContent = JSON.parse(request[0].data); + expect(requestContent).to.have.property('schain').to.deep.equal(schain); + }) + describe('userSync', function () { it('Verifies user syncs iframe include', function () { config.setConfig({ @@ -905,7 +945,7 @@ describe('Richaudience adapter tests', function () { }, [], { consentString: null, referer: 'http://domain.com', - gdprApplies: true + gdprApplies: false }) expect(syncs).to.have.lengthOf(1); expect(syncs[0].type).to.equal('image'); @@ -942,7 +982,7 @@ describe('Richaudience adapter tests', function () { }, [], { consentString: null, referer: 'http://domain.com', - gdprApplies: true + gdprApplies: false }) expect(syncs).to.have.lengthOf(0); }); diff --git a/test/spec/modules/riseBidAdapter_spec.js b/test/spec/modules/riseBidAdapter_spec.js index 61b307eef21..596c4d0f58c 100644 --- a/test/spec/modules/riseBidAdapter_spec.js +++ b/test/spec/modules/riseBidAdapter_spec.js @@ -2,12 +2,13 @@ import { expect } from 'chai'; import { spec } from 'modules/riseBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; import { config } from 'src/config.js'; -import { VIDEO } from '../../../src/mediaTypes.js'; -import { deepClone } from 'src/utils.js'; +import { BANNER, VIDEO } from '../../../src/mediaTypes.js'; +import * as utils from 'src/utils.js'; -const ENDPOINT = 'https://hb.yellowblue.io/hb'; -const TEST_ENDPOINT = 'https://hb.yellowblue.io/hb-test'; +const ENDPOINT = 'https://hb.yellowblue.io/hb-multi'; +const TEST_ENDPOINT = 'https://hb.yellowblue.io/hb-multi-test'; const TTL = 360; +/* eslint no-console: ["error", { allow: ["log", "warn", "error"] }] */ describe('riseAdapter', function () { const adapter = newBidder(spec); @@ -54,6 +55,29 @@ describe('riseAdapter', function () { 'bidId': '299ffc8cca0b87', 'bidderRequestId': '1144f487e563f9', 'auctionId': 'bfc420c3-8577-4568-9766-a8a935fb620d', + 'mediaTypes': { + 'video': { + 'playerSize': [[640, 480]], + 'context': 'instream' + } + }, + 'vastXml': '"..."' + }, + { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250]], + 'params': { + 'org': 'jdye8weeyirk00000001' + }, + 'bidId': '299ffc8cca0b87', + 'bidderRequestId': '1144f487e563f9', + 'auctionId': 'bfc420c3-8577-4568-9766-a8a935fb620d', + 'mediaTypes': { + 'banner': { + } + }, + 'ad': '""' } ]; @@ -77,44 +101,41 @@ describe('riseAdapter', function () { } const placementId = '12345678'; - it('sends the placementId as a query param', function () { + it('sends the placementId to ENDPOINT via POST', function () { bidRequests[0].params.placementId = placementId; - const requests = spec.buildRequests(bidRequests, bidderRequest); - for (const request of requests) { - expect(request.data.placement_id).to.equal(placementId); - } + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[0].placementId).to.equal(placementId); }); - it('sends bid request to ENDPOINT via GET', function () { - const requests = spec.buildRequests(bidRequests, bidderRequest); - for (const request of requests) { - expect(request.url).to.equal(ENDPOINT); - expect(request.method).to.equal('GET'); - } + it('sends bid request to ENDPOINT via POST', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.url).to.equal(ENDPOINT); + expect(request.method).to.equal('POST'); }); - it('sends bid request to test ENDPOINT via GET', function () { - const requests = spec.buildRequests(testModeBidRequests, bidderRequest); - for (const request of requests) { - expect(request.url).to.equal(TEST_ENDPOINT); - expect(request.method).to.equal('GET'); - } + it('sends bid request to TEST ENDPOINT via POST', function () { + const request = spec.buildRequests(testModeBidRequests, bidderRequest); + expect(request.url).to.equal(TEST_ENDPOINT); + expect(request.method).to.equal('POST'); }); it('should send the correct bid Id', function () { - const requests = spec.buildRequests(bidRequests, bidderRequest); - for (const request of requests) { - expect(request.data.bid_id).to.equal('299ffc8cca0b87'); - } + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[0].bidId).to.equal('299ffc8cca0b87'); }); - it('should send the correct width and height', function () { - const requests = spec.buildRequests(bidRequests, bidderRequest); - for (const request of requests) { - expect(request.data).to.be.an('object'); - expect(request.data).to.have.property('width', 640); - expect(request.data).to.have.property('height', 480); - } + it('should send the correct sizes array', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[0].sizes).to.be.an('array'); + expect(request.data.bids[0].sizes).to.equal(bidRequests[0].sizes) + expect(request.data.bids[1].sizes).to.be.an('array'); + expect(request.data.bids[1].sizes).to.equal(bidRequests[1].sizes) + }); + + it('should send the correct media type', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[0].mediaType).to.equal(VIDEO) + expect(request.data.bids[1].mediaType).to.equal(BANNER) }); it('should respect syncEnabled option', function() { @@ -129,11 +150,9 @@ describe('riseAdapter', function () { } } }); - const requests = spec.buildRequests(bidRequests, bidderRequest); - for (const request of requests) { - expect(request.data).to.be.an('object'); - expect(request.data).to.not.have.property('cs_method'); - } + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.not.have.property('cs_method'); }); it('should respect "iframe" filter settings', function () { @@ -148,11 +167,9 @@ describe('riseAdapter', function () { } } }); - const requests = spec.buildRequests(bidRequests, bidderRequest); - for (const request of requests) { - expect(request.data).to.be.an('object'); - expect(request.data).to.have.property('cs_method', 'iframe'); - } + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('cs_method', 'iframe'); }); it('should respect "all" filter settings', function () { @@ -167,24 +184,21 @@ describe('riseAdapter', function () { } } }); - const requests = spec.buildRequests(bidRequests, bidderRequest); - for (const request of requests) { - expect(request.data).to.be.an('object'); - expect(request.data).to.have.property('cs_method', 'iframe'); - } + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('cs_method', 'iframe'); }); it('should send the pixel user sync param if userSync is enabled and no "iframe" or "all" configs are present', function () { + config.resetConfig(); config.setConfig({ userSync: { - syncEnabled: true + syncEnabled: true, } }); - const requests = spec.buildRequests(bidRequests, bidderRequest); - for (const request of requests) { - expect(request.data).to.be.an('object'); - expect(request.data).to.have.property('cs_method', 'pixel'); - } + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('cs_method', 'pixel'); }); it('should respect total exclusion', function() { @@ -203,48 +217,38 @@ describe('riseAdapter', function () { } } }); - const requests = spec.buildRequests(bidRequests, bidderRequest); - for (const request of requests) { - expect(request.data).to.be.an('object'); - expect(request.data).to.not.have.property('cs_method'); - } + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.not.have.property('cs_method'); }); it('should have us_privacy param if usPrivacy is available in the bidRequest', function () { const bidderRequestWithUSP = Object.assign({uspConsent: '1YNN'}, bidderRequest); - const requests = spec.buildRequests(bidRequests, bidderRequestWithUSP); - for (const request of requests) { - expect(request.data).to.be.an('object'); - expect(request.data).to.have.property('us_privacy', '1YNN'); - } + const request = spec.buildRequests(bidRequests, bidderRequestWithUSP); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('us_privacy', '1YNN'); }); it('should have an empty us_privacy param if usPrivacy is missing in the bidRequest', function () { - const requests = spec.buildRequests(bidRequests, bidderRequest); - for (const request of requests) { - expect(request.data).to.be.an('object'); - expect(request.data).to.not.have.property('us_privacy'); - } + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.not.have.property('us_privacy'); }); it('should not send the gdpr param if gdprApplies is false in the bidRequest', function () { const bidderRequestWithGDPR = Object.assign({gdprConsent: {gdprApplies: false}}, bidderRequest); - const requests = spec.buildRequests(bidRequests, bidderRequestWithGDPR); - for (const request of requests) { - expect(request.data).to.be.an('object'); - expect(request.data).to.not.have.property('gdpr'); - expect(request.data).to.not.have.property('gdpr_consent'); - } + const request = spec.buildRequests(bidRequests, bidderRequestWithGDPR); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.not.have.property('gdpr'); + expect(request.data.params).to.not.have.property('gdpr_consent'); }); it('should send the gdpr param if gdprApplies is true in the bidRequest', function () { const bidderRequestWithGDPR = Object.assign({gdprConsent: {gdprApplies: true, consentString: 'test-consent-string'}}, bidderRequest); - const requests = spec.buildRequests(bidRequests, bidderRequestWithGDPR); - for (const request of requests) { - expect(request.data).to.be.an('object'); - expect(request.data).to.have.property('gdpr', true); - expect(request.data).to.have.property('gdpr_consent', 'test-consent-string'); - } + const request = spec.buildRequests(bidRequests, bidderRequestWithGDPR); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('gdpr', true); + expect(request.data.params).to.have.property('gdpr_consent', 'test-consent-string'); }); it('should have schain param if it is available in the bidRequest', () => { @@ -254,15 +258,13 @@ describe('riseAdapter', function () { nodes: [{ asi: 'indirectseller.com', sid: '00001', hp: 1 }], }; bidRequests[0].schain = schain; - const requests = spec.buildRequests(bidRequests, bidderRequest); - for (const request of requests) { - expect(request.data).to.be.an('object'); - expect(request.data).to.have.property('schain', '1.0,1!indirectseller.com,00001,,,,'); - } + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('schain', '1.0,1!indirectseller.com,00001,1,,,'); }); - it('should set floor_price to getFloor.floor value if it is greater than params.floorPrice', function() { - const bid = deepClone(bidRequests[0]); + it('should set flooPrice to getFloor.floor value if it is greater than params.floorPrice', function() { + const bid = utils.deepClone(bidRequests[0]); bid.getFloor = () => { return { currency: 'USD', @@ -270,13 +272,13 @@ describe('riseAdapter', function () { } } bid.params.floorPrice = 0.64; - const request = spec.buildRequests([bid], bidderRequest)[0]; - expect(request.data).to.be.an('object'); - expect(request.data).to.have.property('floor_price', 3.32); + const request = spec.buildRequests([bid], bidderRequest); + expect(request.data.bids[0]).to.be.an('object'); + expect(request.data.bids[0]).to.have.property('floorPrice', 3.32); }); - it('should set floor_price to params.floorPrice value if it is greater than getFloor.floor', function() { - const bid = deepClone(bidRequests[0]); + it('should set floorPrice to params.floorPrice value if it is greater than getFloor.floor', function() { + const bid = utils.deepClone(bidRequests[0]); bid.getFloor = () => { return { currency: 'USD', @@ -284,61 +286,109 @@ describe('riseAdapter', function () { } } bid.params.floorPrice = 1.5; - const request = spec.buildRequests([bid], bidderRequest)[0]; - expect(request.data).to.be.an('object'); - expect(request.data).to.have.property('floor_price', 1.5); + const request = spec.buildRequests([bid], bidderRequest); + expect(request.data.bids[0]).to.be.an('object'); + expect(request.data.bids[0]).to.have.property('floorPrice', 1.5); }); }); describe('interpretResponse', function () { const response = { + params: { + currency: 'USD', + netRevenue: true, + }, + bids: [{ + cpm: 12.5, + vastXml: '', + width: 640, + height: 480, + requestId: '21e12606d47ba7', + adomain: ['abc.com'], + mediaType: VIDEO + }, + { + cpm: 12.5, + ad: '""', + width: 300, + height: 250, + requestId: '21e12606d47ba7', + adomain: ['abc.com'], + mediaType: BANNER + }] + }; + + const expectedVideoResponse = { + requestId: '21e12606d47ba7', cpm: 12.5, - vastXml: '', + currency: 'USD', width: 640, height: 480, - requestId: '21e12606d47ba7', + ttl: TTL, + creativeId: '21e12606d47ba7', netRevenue: true, + nurl: 'http://example.com/win/1234', + mediaType: VIDEO, + meta: { + mediaType: VIDEO, + advertiserDomains: ['abc.com'] + }, + vastXml: '', + }; + + const expectedBannerResponse = { + requestId: '21e12606d47ba7', + cpm: 12.5, currency: 'USD', - adomain: ['abc.com'] + width: 640, + height: 480, + ttl: TTL, + creativeId: '21e12606d47ba7', + netRevenue: true, + nurl: 'http://example.com/win/1234', + mediaType: BANNER, + meta: { + mediaType: BANNER, + advertiserDomains: ['abc.com'] + }, + ad: '""' }; it('should get correct bid response', function () { - let expectedResponse = [ - { - requestId: '21e12606d47ba7', - cpm: 12.5, - width: 640, - height: 480, - creativeId: '21e12606d47ba7', - currency: 'USD', - netRevenue: true, - ttl: TTL, - vastXml: '', - mediaType: VIDEO, - meta: { - advertiserDomains: ['abc.com'] - } - } - ]; const result = spec.interpretResponse({ body: response }); - expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); + expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedVideoResponse)); + expect(Object.keys(result[1])).to.deep.equal(Object.keys(expectedBannerResponse)); + }); + + it('video type should have vastXml key', function () { + const result = spec.interpretResponse({ body: response }); + expect(result[0].vastXml).to.equal(expectedVideoResponse.vastXml) + }); + + it('banner type should have ad key', function () { + const result = spec.interpretResponse({ body: response }); + expect(result[1].ad).to.equal(expectedBannerResponse.ad) }); }) describe('getUserSyncs', function() { const imageSyncResponse = { body: { - userSyncPixels: [ - 'https://image-sync-url.test/1', - 'https://image-sync-url.test/2', - 'https://image-sync-url.test/3' - ] + params: { + userSyncPixels: [ + 'https://image-sync-url.test/1', + 'https://image-sync-url.test/2', + 'https://image-sync-url.test/3' + ] + } } }; const iframeSyncResponse = { body: { - userSyncURL: 'https://iframe-sync-url.test' + params: { + userSyncURL: 'https://iframe-sync-url.test' + } } }; @@ -402,4 +452,28 @@ describe('riseAdapter', function () { expect(syncs).to.deep.equal([]); }); }) + + describe('onBidWon', function() { + beforeEach(function() { + sinon.stub(utils, 'triggerPixel'); + }); + afterEach(function() { + utils.triggerPixel.restore(); + }); + + it('Should trigger pixel if bid nurl', function() { + const bid = { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [['640', '480']], + 'nurl': 'http://example.com/win/1234', + 'params': { + 'org': 'jdye8weeyirk00000001' + } + }; + + spec.onBidWon(bid); + expect(utils.triggerPixel.callCount).to.equal(1) + }) + }) }); diff --git a/test/spec/modules/rtbhouseBidAdapter_spec.js b/test/spec/modules/rtbhouseBidAdapter_spec.js index d6bee26d73b..f4bcb48474a 100644 --- a/test/spec/modules/rtbhouseBidAdapter_spec.js +++ b/test/spec/modules/rtbhouseBidAdapter_spec.js @@ -68,6 +68,7 @@ describe('RTBHouseAdapter', () => { 'params': { 'publisherId': 'PREBID_TEST', 'region': 'prebid-eu', + 'channel': 'Partner_Site - news', 'test': 1 }, 'adUnitCode': 'adunit-code', @@ -101,6 +102,25 @@ describe('RTBHouseAdapter', () => { expect(JSON.parse(builtTestRequest).test).to.equal(1); }); + it('should build channel param into request.site', () => { + let builtTestRequest = spec.buildRequests(bidRequests, bidderRequest).data; + expect(JSON.parse(builtTestRequest).site.channel).to.equal('Partner_Site - news'); + }) + + it('should not build channel param into request.site if no value is passed', () => { + let bidRequest = Object.assign([], bidRequests); + bidRequest[0].params.channel = undefined; + let builtTestRequest = spec.buildRequests(bidRequest, bidderRequest).data; + expect(JSON.parse(builtTestRequest).site.channel).to.be.undefined + }) + + it('should cap the request.site.channel length to 50', () => { + let bidRequest = Object.assign([], bidRequests); + bidRequest[0].params.channel = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent scelerisque ipsum eu purus lobortis iaculis.'; + let builtTestRequest = spec.buildRequests(bidRequest, bidderRequest).data; + expect(JSON.parse(builtTestRequest).site.channel.length).to.equal(50) + }) + it('should build valid OpenRTB banner object', () => { const request = JSON.parse(spec.buildRequests(bidRequests, bidderRequest).data); const imp = request.imp[0]; diff --git a/test/spec/modules/rubiconAnalyticsAdapter_spec.js b/test/spec/modules/rubiconAnalyticsAdapter_spec.js index 798e98bb97d..f365d416876 100644 --- a/test/spec/modules/rubiconAnalyticsAdapter_spec.js +++ b/test/spec/modules/rubiconAnalyticsAdapter_spec.js @@ -13,6 +13,7 @@ import { setConfig, addBidResponseHook, } from 'modules/currency.js'; +import truncate from 'lodash.truncate'; let Ajv = require('ajv'); let schema = require('./rubiconAnalyticsSchema.json'); let ajv = new Ajv({ @@ -26,7 +27,6 @@ function validate(message) { expect(validator.errors).to.deep.equal(null); } -// using es6 "import * as events from 'src/events.js'" causes the events.getEvents stub not to work... let events = require('src/events.js'); let utils = require('src/utils.js'); @@ -39,7 +39,8 @@ const { BIDDER_DONE, BID_WON, BID_TIMEOUT, - SET_TARGETING + SET_TARGETING, + BILLABLE_EVENT } } = CONSTANTS; @@ -246,7 +247,7 @@ const MOCK = { } }, BID_REQUESTED: { - 'bidder': 'rubicon', + 'bidderCode': 'rubicon', 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', 'bidderRequestId': '1be65d7958826a', 'bids': [ @@ -384,6 +385,10 @@ const ANALYTICS_MESSAGE = { 'referrerHostname': 'www.test.com', 'auctions': [ { + + 'auctionEnd': 1519767013781, + 'auctionStart': 1519767010567, + 'bidderOrder': ['rubicon'], 'requestId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', 'clientTimeoutMillis': 3000, 'serverTimeoutMillis': 1000, @@ -573,21 +578,21 @@ const ANALYTICS_MESSAGE = { } }; -function performStandardAuction(gptEvents) { - events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); - events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); - events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); - events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[1]); - events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); - events.emit(AUCTION_END, MOCK.AUCTION_END); +function performStandardAuction(gptEvents, auctionId = MOCK.AUCTION_INIT.auctionId) { + events.emit(AUCTION_INIT, { ...MOCK.AUCTION_INIT, auctionId }); + events.emit(BID_REQUESTED, { ...MOCK.BID_REQUESTED, auctionId }); + events.emit(BID_RESPONSE, { ...MOCK.BID_RESPONSE[0], auctionId }); + events.emit(BID_RESPONSE, { ...MOCK.BID_RESPONSE[1], auctionId }); + events.emit(BIDDER_DONE, { ...MOCK.BIDDER_DONE, auctionId }); + events.emit(AUCTION_END, { ...MOCK.AUCTION_END, auctionId }); if (gptEvents && gptEvents.length) { gptEvents.forEach(gptEvent => mockGpt.emitEvent(gptEvent.eventName, gptEvent.params)); } - events.emit(SET_TARGETING, MOCK.SET_TARGETING); - events.emit(BID_WON, MOCK.BID_WON[0]); - events.emit(BID_WON, MOCK.BID_WON[1]); + events.emit(SET_TARGETING, { ...MOCK.SET_TARGETING, auctionId }); + events.emit(BID_WON, { ...MOCK.BID_WON[0], auctionId }); + events.emit(BID_WON, { ...MOCK.BID_WON[1], auctionId }); } describe('rubicon analytics adapter', function () { @@ -673,6 +678,11 @@ describe('rubicon analytics adapter', function () { }); expect(rubiConf).to.deep.equal({ analyticsEventDelay: 0, + dmBilling: { + enabled: false, + vendors: [], + waitForAuction: true + }, pvid: '12345678', wrapperName: '1001_general', int_type: 'dmpbjs', @@ -692,6 +702,11 @@ describe('rubicon analytics adapter', function () { }); expect(rubiConf).to.deep.equal({ analyticsEventDelay: 0, + dmBilling: { + enabled: false, + vendors: [], + waitForAuction: true + }, pvid: '12345678', wrapperName: '1001_general', int_type: 'dmpbjs', @@ -713,6 +728,11 @@ describe('rubicon analytics adapter', function () { }); expect(rubiConf).to.deep.equal({ analyticsEventDelay: 0, + dmBilling: { + enabled: false, + vendors: [], + waitForAuction: true + }, pvid: '12345678', wrapperName: '1001_general', int_type: 'dmpbjs', @@ -853,6 +873,32 @@ describe('rubicon analytics adapter', function () { expect(message).to.deep.equal(ANALYTICS_MESSAGE); }); + it('should pass along bidderOrder correctly', function () { + const appnexusBid = utils.deepClone(MOCK.BID_REQUESTED); + appnexusBid.bidderCode = 'appnexus'; + const pubmaticBid = utils.deepClone(MOCK.BID_REQUESTED); + pubmaticBid.bidderCode = 'pubmatic'; + const indexBid = utils.deepClone(MOCK.BID_REQUESTED); + indexBid.bidderCode = 'ix'; + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, pubmaticBid); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_REQUESTED, indexBid); + events.emit(BID_REQUESTED, appnexusBid); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + clock.tick(SEND_TIMEOUT + 1000); + + let message = JSON.parse(server.requests[0].requestBody); + expect(message.auctions[0].bidderOrder).to.deep.equal([ + 'pubmatic', + 'rubicon', + 'ix', + 'appnexus' + ]); + }); + it('should pass along user ids', function () { let auctionInit = utils.deepClone(MOCK.AUCTION_INIT); auctionInit.bidderRequests[0].bids[0].userId = { @@ -987,6 +1033,36 @@ describe('rubicon analytics adapter', function () { expect(message.auctions[0].adUnits[1].bids[0].bidResponse.adomains).to.be.undefined; }); + it('should NOT pass along adomians with other edge cases', function () { + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + + // should filter out non string values and pass valid ones + let bidResponse1 = utils.deepClone(MOCK.BID_RESPONSE[0]); + bidResponse1.meta = { + advertiserDomains: [123, 'prebid.org', false, true, [], 'magnite.com', {}] + } + + // array of arrays (as seen when passed by kargo bid adapter) + let bidResponse2 = utils.deepClone(MOCK.BID_RESPONSE[1]); + bidResponse2.meta = { + advertiserDomains: [['prebid.org']] + } + + events.emit(BID_RESPONSE, bidResponse1); + events.emit(BID_RESPONSE, bidResponse2); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON[0]); + events.emit(BID_WON, MOCK.BID_WON[1]); + + let message = JSON.parse(server.requests[0].requestBody); + validate(message); + expect(message.auctions[0].adUnits[0].bids[0].bidResponse.adomains).to.deep.equal(['prebid.org', 'magnite.com']); + expect(message.auctions[0].adUnits[1].bids[0].bidResponse.adomains).to.be.undefined; + }); + it('should not pass empty adServerTargeting values', function () { events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); @@ -1703,6 +1779,50 @@ describe('rubicon analytics adapter', function () { expect(message).to.deep.equal(expectedMessage); }); + it('should only mark the first gam data not all matches', function () { + config.setConfig({ + rubicon: { + waitForGamSlots: true + } + }); + performStandardAuction(); + performStandardAuction([gptSlotRenderEnded0, gptSlotRenderEnded1], '32d332de-123a-32dg-2345-cefef3423324'); + + // tick the clock and both should fire + clock.tick(3000); + + expect(server.requests.length).to.equal(2); + + // first one should have GAM data + let request = server.requests[0]; + let message = JSON.parse(request.requestBody); + + // trigger should be gam since all adunits had associated gam render + expect(message.trigger).to.be.equal('gam'); + expect(message.auctions[0].adUnits[0].gam).to.deep.equal({ + advertiserId: 1111, + creativeId: 2222, + lineItemId: 3333, + adSlot: '/19968336/header-bid-tag-0' + }); + expect(message.auctions[0].adUnits[1].gam).to.deep.equal({ + advertiserId: 4444, + creativeId: 5555, + lineItemId: 6666, + adSlot: '/19968336/header-bid-tag1' + }); + + // second one should NOT have gam data + request = server.requests[1]; + message = JSON.parse(request.requestBody); + validate(message); + + // trigger should be auctionEnd + expect(message.trigger).to.be.equal('auctionEnd'); + expect(message.auctions[0].adUnits[0].gam).to.be.undefined; + expect(message.auctions[0].adUnits[1].gam).to.be.undefined; + }); + it('should send request when waitForGamSlots is present but no bidWons are sent', function () { config.setConfig({ rubicon: { @@ -2022,6 +2142,35 @@ describe('rubicon analytics adapter', function () { expect(message.auctions[0].adUnits[1].pattern).to.equal('1234/mycoolsite/*&gpt_skyscraper&deviceType=mobile'); }); + it('should pass gpid if defined', function () { + let bidRequest = utils.deepClone(MOCK.BID_REQUESTED); + bidRequest.bids[0].ortb2Imp = { + ext: { + gpid: '1234/mycoolsite/lowerbox' + } + }; + bidRequest.bids[1].ortb2Imp = { + ext: { + gpid: '1234/mycoolsite/leaderboard' + } + }; + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, bidRequest); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + + clock.tick(SEND_TIMEOUT + 1000); + + expect(server.requests.length).to.equal(1); + + let message = JSON.parse(server.requests[0].requestBody); + validate(message); + expect(message.auctions[0].adUnits[0].gpid).to.equal('1234/mycoolsite/lowerbox'); + expect(message.auctions[0].adUnits[1].gpid).to.equal('1234/mycoolsite/leaderboard'); + }); + it('should pass bidderDetail for multibid auctions', function () { let bidResponse = utils.deepClone(MOCK.BID_RESPONSE[1]); bidResponse.targetingBidder = 'rubi2'; @@ -2103,6 +2252,246 @@ describe('rubicon analytics adapter', function () { }); }); + describe('billing events integration', () => { + beforeEach(function () { + rubiconAnalyticsAdapter.enableAnalytics({ + options: { + endpoint: '//localhost:9999/event', + accountId: 1001 + } + }); + // default dmBilling + config.setConfig({ + rubicon: { + dmBilling: { + enabled: false, + vendors: [], + waitForAuction: true + } + } + }) + }); + afterEach(function () { + rubiconAnalyticsAdapter.disableAnalytics(); + }); + const basicBillingAuction = (billingEvents = []) => { + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + + // emit billing events + billingEvents.forEach(ev => events.emit(BILLABLE_EVENT, ev)); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[1]); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON[0]); + events.emit(BID_WON, MOCK.BID_WON[1]); + } + it('should ignore billing events when not enabled', () => { + basicBillingAuction([{ + vendor: 'vendorName', + type: 'auction', + billingId: 'f8558d41-62de-4349-bc7b-2dbee1e69965' + }]); + expect(server.requests.length).to.equal(1); + const request = server.requests[0]; + const message = JSON.parse(request.requestBody); + expect(message.billableEvents).to.be.undefined; + }); + it('should ignore billing events when enabled but vendor is not whitelisted', () => { + // off by default + config.setConfig({ + rubicon: { + dmBilling: { + enabled: true + } + } + }); + basicBillingAuction([{ + vendor: 'vendorName', + type: 'auction', + billingId: 'f8558d41-62de-4349-bc7b-2dbee1e69965' + }]); + expect(server.requests.length).to.equal(1); + const request = server.requests[0]; + const message = JSON.parse(request.requestBody); + expect(message.billableEvents).to.be.undefined; + }); + it('should ignore billing events if billingId is not defined or billingId is not a string', () => { + // off by default + config.setConfig({ + rubicon: { + dmBilling: { + enabled: true, + vendors: ['vendorName'] + } + } + }); + basicBillingAuction([ + { + vendor: 'vendorName', + type: 'auction', + }, + { + vendor: 'vendorName', + type: 'auction', + billingId: true + }, + { + vendor: 'vendorName', + type: 'auction', + billingId: 1233434 + }, + { + vendor: 'vendorName', + type: 'auction', + billingId: null + } + ]); + expect(server.requests.length).to.equal(1); + const request = server.requests[0]; + const message = JSON.parse(request.requestBody); + expect(message.billableEvents).to.be.undefined; + }); + it('should pass along billing event in same payload', () => { + // off by default + config.setConfig({ + rubicon: { + dmBilling: { + enabled: true, + vendors: ['vendorName'] + } + } + }); + basicBillingAuction([{ + vendor: 'vendorName', + type: 'auction', + billingId: 'f8558d41-62de-4349-bc7b-2dbee1e69965' + }]); + expect(server.requests.length).to.equal(1); + const request = server.requests[0]; + const message = JSON.parse(request.requestBody); + expect(message).to.haveOwnProperty('auctions'); + expect(message.billableEvents).to.deep.equal([{ + accountId: 1001, + vendor: 'vendorName', + type: 'general', // mapping all events to endpoint as 'general' for now + billingId: 'f8558d41-62de-4349-bc7b-2dbee1e69965' + }]); + }); + it('should pass along multiple billing events but filter out duplicates', () => { + // off by default + config.setConfig({ + rubicon: { + dmBilling: { + enabled: true, + vendors: ['vendorName'] + } + } + }); + basicBillingAuction([ + { + vendor: 'vendorName', + type: 'auction', + billingId: 'f8558d41-62de-4349-bc7b-2dbee1e69965' + }, + { + vendor: 'vendorName', + type: 'auction', + billingId: '743db6e3-21f2-44d4-917f-cb3488c6076f' + }, + { + vendor: 'vendorName', + type: 'auction', + billingId: 'f8558d41-62de-4349-bc7b-2dbee1e69965' + } + ]); + expect(server.requests.length).to.equal(1); + const request = server.requests[0]; + const message = JSON.parse(request.requestBody); + expect(message).to.haveOwnProperty('auctions'); + expect(message.billableEvents).to.deep.equal([ + { + accountId: 1001, + vendor: 'vendorName', + type: 'general', + billingId: 'f8558d41-62de-4349-bc7b-2dbee1e69965' + }, + { + accountId: 1001, + vendor: 'vendorName', + type: 'general', + billingId: '743db6e3-21f2-44d4-917f-cb3488c6076f' + } + ]); + }); + it('should pass along event right away if no pending auction', () => { + // off by default + config.setConfig({ + rubicon: { + dmBilling: { + enabled: true, + vendors: ['vendorName'] + } + } + }); + + events.emit(BILLABLE_EVENT, { + vendor: 'vendorName', + type: 'auction', + billingId: 'f8558d41-62de-4349-bc7b-2dbee1e69965' + }); + expect(server.requests.length).to.equal(1); + const request = server.requests[0]; + const message = JSON.parse(request.requestBody); + expect(message).to.not.haveOwnProperty('auctions'); + expect(message.billableEvents).to.deep.equal([ + { + accountId: 1001, + vendor: 'vendorName', + type: 'general', + billingId: 'f8558d41-62de-4349-bc7b-2dbee1e69965' + } + ]); + }); + it('should pass along event right away if pending auction but not waiting', () => { + // off by default + config.setConfig({ + rubicon: { + dmBilling: { + enabled: true, + vendors: ['vendorName'], + waitForAuction: false + } + } + }); + // should fire right away, and then auction later + basicBillingAuction([{ + vendor: 'vendorName', + type: 'auction', + billingId: 'f8558d41-62de-4349-bc7b-2dbee1e69965' + }]); + expect(server.requests.length).to.equal(2); + const billingRequest = server.requests[0]; + const billingMessage = JSON.parse(billingRequest.requestBody); + expect(billingMessage).to.not.haveOwnProperty('auctions'); + expect(billingMessage.billableEvents).to.deep.equal([ + { + accountId: 1001, + vendor: 'vendorName', + type: 'general', + billingId: 'f8558d41-62de-4349-bc7b-2dbee1e69965' + } + ]); + // auction event after + const auctionRequest = server.requests[1]; + const auctionMessage = JSON.parse(auctionRequest.requestBody); + // should not double pass events! + expect(auctionMessage).to.not.haveOwnProperty('billableEvents'); + }); + }); + describe('wrapper details passed in', () => { it('should correctly pass in the wrapper details if provided', () => { config.setConfig({ diff --git a/test/spec/modules/rubiconAnalyticsSchema.json b/test/spec/modules/rubiconAnalyticsSchema.json index 39a33867edd..07239ca06ed 100644 --- a/test/spec/modules/rubiconAnalyticsSchema.json +++ b/test/spec/modules/rubiconAnalyticsSchema.json @@ -17,6 +17,11 @@ "required": [ "bidsWon" ] + }, + { + "required": [ + "billableEvents" + ] } ], "properties": { @@ -237,9 +242,44 @@ ] } }, - "wrapperName": { - "type": "string" - } + "billableEvents":{ + "type":"array", + "minItems":1, + "items":{ + "type":"object", + "required":[ + "accountId", + "vendor", + "type", + "billingId" + ], + "properties":{ + "vendor":{ + "type":"string", + "description":"The name of the vendor who emitted the billable event" + }, + "type":{ + "type":"string", + "description":"The type of billable event", + "enum":[ + "impression", + "pageLoad", + "auction", + "request", + "general" + ] + }, + "billingId":{ + "type":"string", + "description":"A UUID which is responsible more mapping this event to" + }, + "accountId": { + "type": "number", + "description": "The account id for the rubicon publisher" + } + } + } + } }, "definitions": { "gam": { @@ -451,4 +491,4 @@ } } } -} \ No newline at end of file +} diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index 6210640f79f..c281c195dd2 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -10,7 +10,7 @@ import { import {parse as parseQuery} from 'querystring'; import {config} from 'src/config.js'; import * as utils from 'src/utils.js'; -import find from 'core-js-pure/features/array/find.js'; +import {find} from 'src/polyfill.js'; import {createEidsArray} from 'modules/userId/eids.js'; const INTEGRATION = `pbjs_lite_v$prebid.version$`; // $prebid.version$ will be substituted in by gulp in built prebid @@ -597,7 +597,7 @@ describe('the rubicon adapter', function () { sandbox.stub(Math, 'random').callsFake(() => 0.1); let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - const referenceOrdering = ['account_id', 'site_id', 'zone_id', 'size_id', 'alt_size_ids', 'p_pos', 'rf', 'p_geo.latitude', 'p_geo.longitude', 'kw', 'tg_v.ucat', 'tg_v.lastsearch', 'tg_v.likes', 'tg_i.rating', 'tg_i.prodtype', 'tk_flint', 'x_source.tid', 'x_source.pchain', 'p_screen_res', 'rp_secure', 'tk_user_key', 'tg_fl.eid', 'rp_maxbids', 'slots', 'rand']; + const referenceOrdering = ['account_id', 'site_id', 'zone_id', 'size_id', 'alt_size_ids', 'p_pos', 'rf', 'p_geo.latitude', 'p_geo.longitude', 'kw', 'tg_v.ucat', 'tg_v.lastsearch', 'tg_v.likes', 'tg_i.rating', 'tg_i.prodtype', 'tk_flint', 'x_source.tid', 'l_pb_bid_id', 'x_source.pchain', 'p_screen_res', 'rp_secure', 'tk_user_key', 'tg_fl.eid', 'rp_maxbids', 'slots', 'rand']; request.data.split('&').forEach((item, i) => { expect(item.split('=')[0]).to.equal(referenceOrdering[i]); @@ -1408,7 +1408,7 @@ describe('the rubicon adapter', function () { expect(data['tg_i.pbadslot']).to.equal('abc'); }); - it('should send \"tg_i.pbadslot\" if \"ortb2Imp.ext.data.pbadslot\" value is a valid string, but all leading slash characters should be removed', function () { + it('should send \"tg_i.pbadslot\" if \"ortb2Imp.ext.data.pbadslot\" value is a valid string', function () { bidderRequest.bids[0].ortb2Imp = { ext: { data: { @@ -1422,7 +1422,45 @@ describe('the rubicon adapter', function () { expect(data).to.be.an('Object'); expect(data).to.have.property('tg_i.pbadslot'); - expect(data['tg_i.pbadslot']).to.equal('a/b/c'); + expect(data['tg_i.pbadslot']).to.equal('/a/b/c'); + }); + + it('should send gpid as p_gpid if valid', function () { + bidderRequest.bids[0].ortb2Imp = { + ext: { + gpid: '/1233/sports&div1' + } + } + + const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + const data = parseQuery(request.data); + + expect(data).to.be.an('Object'); + expect(data).to.have.property('p_gpid'); + expect(data['p_gpid']).to.equal('/1233/sports&div1'); + }); + + it('should send gpid and pbadslot since it is prefered over dfp code', function () { + bidderRequest.bids[0].ortb2Imp = { + ext: { + gpid: '/1233/sports&div1', + data: { + pbadslot: 'pb_slot', + adserver: { + adslot: '/1234/sports', + name: 'gam' + } + } + } + } + + const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + const data = parseQuery(request.data); + + expect(data).to.be.an('Object'); + expect(data['p_gpid']).to.equal('/1233/sports&div1'); + expect(data).to.not.have.property('tg_i.dfp_ad_unit_code'); + expect(data['tg_i.pbadslot']).to.equal('pb_slot'); }); }); @@ -1470,12 +1508,13 @@ describe('the rubicon adapter', function () { expect(data).to.not.have.property('tg_i.dfp_ad_unit_code'); }); - it('should send \"tg_i.dfp_ad_unit_code\" if \"ortb2Imp.ext.data.adServer.adslot\" value is a valid string', function () { + it('should send NOT \"tg_i.dfp_ad_unit_code\" if \"ortb2Imp.ext.data.adServer.adslot\" value is a valid string but not gam', function () { bidderRequest.bids[0].ortb2Imp = { ext: { data: { adserver: { - adslot: 'abc' + adslot: '/a/b/c', + name: 'not gam' } } } @@ -1485,16 +1524,16 @@ describe('the rubicon adapter', function () { const data = parseQuery(request.data); expect(data).to.be.an('Object'); - expect(data).to.have.property('tg_i.dfp_ad_unit_code'); - expect(data['tg_i.dfp_ad_unit_code']).to.equal('abc'); + expect(data).to.not.have.property('tg_i.dfp_ad_unit_code'); }); - it('should send \"tg_i.dfp_ad_unit_code\" if \"ortb2Imp.ext.data.adServer.adslot\" value is a valid string, but all leading slash characters should be removed', function () { + it('should send \"tg_i.dfp_ad_unit_code\" if \"ortb2Imp.ext.data.adServer.adslot\" value is a valid string and name is gam', function () { bidderRequest.bids[0].ortb2Imp = { ext: { data: { adserver: { - adslot: 'a/b/c' + name: 'gam', + adslot: '/a/b/c' } } } @@ -1505,7 +1544,7 @@ describe('the rubicon adapter', function () { expect(data).to.be.an('Object'); expect(data).to.have.property('tg_i.dfp_ad_unit_code'); - expect(data['tg_i.dfp_ad_unit_code']).to.equal('a/b/c'); + expect(data['tg_i.dfp_ad_unit_code']).to.equal('/a/b/c'); }); }); }); @@ -3223,7 +3262,7 @@ describe('the rubicon adapter', function () { label: undefined, placement: { align: 'left', - attachTo: '#outstream_video1_placement', + attachTo: adUnit, position: 'append', }, vastUrl: 'https://test.com/vast.xml', @@ -3343,6 +3382,23 @@ describe('the rubicon adapter', function () { type: 'iframe', url: `${emilyUrl}?gdpr_consent=foo&us_privacy=1NYN` }); }); + + it('should pass gdprApplies', function () { + expect(spec.getUserSyncs({iframeEnabled: true}, {}, { + gdprApplies: true + }, '1NYN')).to.deep.equal({ + type: 'iframe', url: `${emilyUrl}?gdpr=1&us_privacy=1NYN` + }); + }); + + it('should pass all correctly', function () { + expect(spec.getUserSyncs({iframeEnabled: true}, {}, { + gdprApplies: true, + consentString: 'foo' + }, '1NYN')).to.deep.equal({ + type: 'iframe', url: `${emilyUrl}?gdpr=1&gdpr_consent=foo&us_privacy=1NYN` + }); + }); }); describe('get price granularity', function () { diff --git a/test/spec/modules/scaleableAnalyticsAdapter_spec.js b/test/spec/modules/scaleableAnalyticsAdapter_spec.js index 70b94a2b807..c65740252d2 100644 --- a/test/spec/modules/scaleableAnalyticsAdapter_spec.js +++ b/test/spec/modules/scaleableAnalyticsAdapter_spec.js @@ -1,6 +1,6 @@ import scaleableAnalytics from 'modules/scaleableAnalyticsAdapter.js'; import { expect } from 'chai'; -import events from 'src/events.js'; +import * as events from 'src/events.js'; import CONSTANTS from 'src/constants.json'; import { server } from 'test/mocks/xhr.js'; diff --git a/test/spec/modules/seedtagBidAdapter_spec.js b/test/spec/modules/seedtagBidAdapter_spec.js index 159645ff6d6..1e0dca68d00 100644 --- a/test/spec/modules/seedtagBidAdapter_spec.js +++ b/test/spec/modules/seedtagBidAdapter_spec.js @@ -1,14 +1,17 @@ -import { expect } from 'chai' -import { spec, getTimeoutUrl } from 'modules/seedtagBidAdapter.js' -import * as utils from 'src/utils.js' +import { expect } from 'chai'; +import { spec, getTimeoutUrl } from 'modules/seedtagBidAdapter.js'; +import * as utils from 'src/utils.js'; -const PUBLISHER_ID = '0000-0000-01' -const ADUNIT_ID = '000000' +const PUBLISHER_ID = '0000-0000-01'; +const ADUNIT_ID = '000000'; function getSlotConfigs(mediaTypes, params) { return { params: params, - sizes: [[300, 250], [300, 600]], + sizes: [ + [300, 250], + [300, 600], + ], bidId: '30b31c1838de1e', bidderRequestId: '22edbae2733bf6', auctionId: '1d1a030790a475', @@ -17,328 +20,336 @@ function getSlotConfigs(mediaTypes, params) { mediaTypes: mediaTypes, src: 'client', transactionId: 'd704d006-0d6e-4a09-ad6c-179e7e758096', - adUnitCode: 'adunit-code' - } + adUnitCode: 'adunit-code', + }; } function createVideoSlotConfig(mediaType) { return getSlotConfigs(mediaType, { publisherId: PUBLISHER_ID, adUnitId: ADUNIT_ID, - placement: 'video' - }) + placement: 'video', + }); } -describe('Seedtag Adapter', function() { - describe('isBidRequestValid method', function() { - describe('returns true', function() { +describe('Seedtag Adapter', function () { + describe('isBidRequestValid method', function () { + describe('returns true', function () { describe('when banner slot config has all mandatory params', () => { - describe('and placement has the correct value', function() { - const createBannerSlotConfig = placement => { + describe('and placement has the correct value', function () { + const createBannerSlotConfig = (placement) => { return getSlotConfigs( { banner: {} }, { publisherId: PUBLISHER_ID, adUnitId: ADUNIT_ID, - placement + placement, } - ) - } - const placements = ['banner', 'video', 'inImage', 'inScreen', 'inArticle'] - placements.forEach(placement => { - it('should be ' + placement, function() { + ); + }; + const placements = [ + 'banner', + 'video', + 'inImage', + 'inScreen', + 'inArticle', + ]; + placements.forEach((placement) => { + it('should be ' + placement, function () { const isBidRequestValid = spec.isBidRequestValid( createBannerSlotConfig(placement) - ) - expect(isBidRequestValid).to.equal(true) - }) - }) - }) - }) - describe('when video slot has all mandatory params', function() { + ); + expect(isBidRequestValid).to.equal(true); + }); + }); + }); + }); + describe('when video slot has all mandatory params', function () { it('should return true, when video context is instream', function () { const slotConfig = getSlotConfigs( { video: { context: 'instream', - playerSize: [[600, 200]] - } + playerSize: [[600, 200]], + }, }, { publisherId: PUBLISHER_ID, adUnitId: ADUNIT_ID, - placement: 'video' + placement: 'video', } - ) - const isBidRequestValid = spec.isBidRequestValid(slotConfig) - expect(isBidRequestValid).to.equal(true) - }) + ); + const isBidRequestValid = spec.isBidRequestValid(slotConfig); + expect(isBidRequestValid).to.equal(true); + }); it('should return true, when video context is outstream', function () { const slotConfig = getSlotConfigs( { video: { context: 'outstream', - playerSize: [[600, 200]] - } + playerSize: [[600, 200]], + }, }, { publisherId: PUBLISHER_ID, adUnitId: ADUNIT_ID, - placement: 'video' + placement: 'video', } - ) - const isBidRequestValid = spec.isBidRequestValid(slotConfig) - expect(isBidRequestValid).to.equal(true) - }) - }) - }) - describe('returns false', function() { - describe('when params are not correct', function() { + ); + const isBidRequestValid = spec.isBidRequestValid(slotConfig); + expect(isBidRequestValid).to.equal(true); + }); + }); + }); + describe('returns false', function () { + describe('when params are not correct', function () { function createSlotConfig(params) { - return getSlotConfigs({ banner: {} }, params) + return getSlotConfigs({ banner: {} }, params); } - it('does not have the PublisherToken.', function() { + it('does not have the PublisherToken.', function () { const isBidRequestValid = spec.isBidRequestValid( createSlotConfig({ adUnitId: ADUNIT_ID, - placement: 'banner' + placement: 'banner', }) - ) - expect(isBidRequestValid).to.equal(false) - }) - it('does not have the AdUnitId.', function() { + ); + expect(isBidRequestValid).to.equal(false); + }); + it('does not have the AdUnitId.', function () { const isBidRequestValid = spec.isBidRequestValid( createSlotConfig({ publisherId: PUBLISHER_ID, - placement: 'banner' + placement: 'banner', }) - ) - expect(isBidRequestValid).to.equal(false) - }) - it('does not have the placement.', function() { + ); + expect(isBidRequestValid).to.equal(false); + }); + it('does not have the placement.', function () { const isBidRequestValid = spec.isBidRequestValid( createSlotConfig({ publisherId: PUBLISHER_ID, - adUnitId: ADUNIT_ID + adUnitId: ADUNIT_ID, }) - ) - expect(isBidRequestValid).to.equal(false) - }) - it('does not have a the correct placement.', function() { + ); + expect(isBidRequestValid).to.equal(false); + }); + it('does not have a the correct placement.', function () { const isBidRequestValid = spec.isBidRequestValid( createSlotConfig({ publisherId: PUBLISHER_ID, adUnitId: ADUNIT_ID, - placement: 'another_thing' + placement: 'another_thing', }) - ) - expect(isBidRequestValid).to.equal(false) - }) - }) - describe('when video mediaType object is not correct', function() { + ); + expect(isBidRequestValid).to.equal(false); + }); + }); + describe('when video mediaType object is not correct', function () { function createVideoSlotconfig(mediaType) { return getSlotConfigs(mediaType, { publisherId: PUBLISHER_ID, adUnitId: ADUNIT_ID, - placement: 'video' - }) + placement: 'video', + }); } - it('is a void object', function() { + it('is a void object', function () { const isBidRequestValid = spec.isBidRequestValid( createVideoSlotConfig({ video: {} }) - ) - expect(isBidRequestValid).to.equal(false) - }) - it('does not have playerSize.', function() { + ); + expect(isBidRequestValid).to.equal(false); + }); + it('does not have playerSize.', function () { const isBidRequestValid = spec.isBidRequestValid( createVideoSlotConfig({ video: { context: 'instream' } }) - ) - expect(isBidRequestValid).to.equal(false) - }) + ); + expect(isBidRequestValid).to.equal(false); + }); it('is outstream ', function () { const isBidRequestValid = spec.isBidRequestValid( createVideoSlotConfig({ video: { context: 'outstream', - playerSize: [[600, 200]] - } + playerSize: [[600, 200]], + }, }) - ) - expect(isBidRequestValid).to.equal(true) - }) - describe('order does not matter', function() { - it('when video is not the first slot', function() { + ); + expect(isBidRequestValid).to.equal(true); + }); + describe('order does not matter', function () { + it('when video is not the first slot', function () { const isBidRequestValid = spec.isBidRequestValid( createVideoSlotConfig({ banner: {}, video: {} }) - ) - expect(isBidRequestValid).to.equal(false) - }) - it('when video is the first slot', function() { + ); + expect(isBidRequestValid).to.equal(false); + }); + it('when video is the first slot', function () { const isBidRequestValid = spec.isBidRequestValid( createVideoSlotConfig({ video: {}, banner: {} }) - ) - expect(isBidRequestValid).to.equal(false) - }) - }) - }) - }) - }) + ); + expect(isBidRequestValid).to.equal(false); + }); + }); + }); + }); + }); - describe('buildRequests method', function() { + describe('buildRequests method', function () { const bidderRequest = { refererInfo: { referer: 'referer' }, - timeout: 1000 - } + timeout: 1000, + }; const mandatoryParams = { publisherId: PUBLISHER_ID, adUnitId: ADUNIT_ID, - placement: 'banner' - } + placement: 'banner', + }; const inStreamParams = Object.assign({}, mandatoryParams, { video: { - mimes: 'mp4' - } - }) + mimes: 'mp4', + }, + }); const validBidRequests = [ getSlotConfigs({ banner: {} }, mandatoryParams), getSlotConfigs( { video: { context: 'instream', playerSize: [[300, 200]] } }, inStreamParams - ) - ] - it('Url params should be correct ', function() { - const request = spec.buildRequests(validBidRequests, bidderRequest) - expect(request.method).to.equal('POST') - expect(request.url).to.equal('https://s.seedtag.com/c/hb/bid') - }) + ), + ]; + it('Url params should be correct ', function () { + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.method).to.equal('POST'); + expect(request.url).to.equal('https://s.seedtag.com/c/hb/bid'); + }); - it('Common data request should be correct', function() { - const request = spec.buildRequests(validBidRequests, bidderRequest) - const data = JSON.parse(request.data) - expect(data.url).to.equal('referer') - expect(data.publisherToken).to.equal('0000-0000-01') - expect(typeof data.version).to.equal('string') - expect(['fixed', 'mobile', 'unknown'].indexOf(data.connectionType)).to.be.above(-1) - expect(data.bidRequests[0].adUnitCode).to.equal('adunit-code') - }) + it('Common data request should be correct', function () { + const request = spec.buildRequests(validBidRequests, bidderRequest); + const data = JSON.parse(request.data); + expect(data.url).to.equal('referer'); + expect(data.publisherToken).to.equal('0000-0000-01'); + expect(typeof data.version).to.equal('string'); + expect( + ['fixed', 'mobile', 'unknown'].indexOf(data.connectionType) + ).to.be.above(-1); + expect(data.bidRequests[0].adUnitCode).to.equal('adunit-code'); + }); - describe('adPosition param', function() { - it('should sended when publisher set adPosition param', function() { + describe('adPosition param', function () { + it('should sended when publisher set adPosition param', function () { const params = Object.assign({}, mandatoryParams, { - adPosition: 1 - }) - const validBidRequests = [getSlotConfigs({ banner: {} }, params)] - const request = spec.buildRequests(validBidRequests, bidderRequest) - const data = JSON.parse(request.data) - expect(data.bidRequests[0].adPosition).to.equal(1) - }) - it('should not sended when publisher has not set adPosition param', function() { + adPosition: 1, + }); + const validBidRequests = [getSlotConfigs({ banner: {} }, params)]; + const request = spec.buildRequests(validBidRequests, bidderRequest); + const data = JSON.parse(request.data); + expect(data.bidRequests[0].adPosition).to.equal(1); + }); + it('should not sended when publisher has not set adPosition param', function () { const validBidRequests = [ - getSlotConfigs({ banner: {} }, mandatoryParams) - ] - const request = spec.buildRequests(validBidRequests, bidderRequest) - const data = JSON.parse(request.data) - expect(data.bidRequests[0].adPosition).to.equal(undefined) - }) - }) + getSlotConfigs({ banner: {} }, mandatoryParams), + ]; + const request = spec.buildRequests(validBidRequests, bidderRequest); + const data = JSON.parse(request.data); + expect(data.bidRequests[0].adPosition).to.equal(undefined); + }); + }); - describe('GDPR params', function() { - describe('when there arent consent management platform', function() { - it('cmp should be false', function() { - const request = spec.buildRequests(validBidRequests, bidderRequest) - const data = JSON.parse(request.data) - expect(data.cmp).to.equal(false) - }) - }) - describe('when there are consent management platform', function() { - it('cmps should be true and ga should not sended, when gdprApplies is undefined', function() { + describe('GDPR params', function () { + describe('when there arent consent management platform', function () { + it('cmp should be false', function () { + const request = spec.buildRequests(validBidRequests, bidderRequest); + const data = JSON.parse(request.data); + expect(data.cmp).to.equal(false); + }); + }); + describe('when there are consent management platform', function () { + it('cmps should be true and ga should not sended, when gdprApplies is undefined', function () { bidderRequest['gdprConsent'] = { gdprApplies: undefined, - consentString: 'consentString' - } - const request = spec.buildRequests(validBidRequests, bidderRequest) - const data = JSON.parse(request.data) - expect(data.cmp).to.equal(true) - expect(Object.keys(data).indexOf('data')).to.equal(-1) - expect(data.cd).to.equal('consentString') - }) - it('cmps should be true and all gdpr parameters should be sended, when there are gdprApplies', function() { + consentString: 'consentString', + }; + const request = spec.buildRequests(validBidRequests, bidderRequest); + const data = JSON.parse(request.data); + expect(data.cmp).to.equal(true); + expect(Object.keys(data).indexOf('data')).to.equal(-1); + expect(data.cd).to.equal('consentString'); + }); + it('cmps should be true and all gdpr parameters should be sended, when there are gdprApplies', function () { bidderRequest['gdprConsent'] = { gdprApplies: true, - consentString: 'consentString' - } - const request = spec.buildRequests(validBidRequests, bidderRequest) - const data = JSON.parse(request.data) - expect(data.cmp).to.equal(true) - expect(data.ga).to.equal(true) - expect(data.cd).to.equal('consentString') - }) - it('should expose gvlid', function() { - expect(spec.gvlid).to.equal(157) - }) - }) - }) + consentString: 'consentString', + }; + const request = spec.buildRequests(validBidRequests, bidderRequest); + const data = JSON.parse(request.data); + expect(data.cmp).to.equal(true); + expect(data.ga).to.equal(true); + expect(data.cd).to.equal('consentString'); + }); + it('should expose gvlid', function () { + expect(spec.gvlid).to.equal(157); + }); + }); + }); - describe('BidRequests params', function() { - const request = spec.buildRequests(validBidRequests, bidderRequest) - const data = JSON.parse(request.data) - const bidRequests = data.bidRequests - it('should request a Banner', function() { - const bannerBid = bidRequests[0] - expect(bannerBid.id).to.equal('30b31c1838de1e') + describe('BidRequests params', function () { + const request = spec.buildRequests(validBidRequests, bidderRequest); + const data = JSON.parse(request.data); + const bidRequests = data.bidRequests; + it('should request a Banner', function () { + const bannerBid = bidRequests[0]; + expect(bannerBid.id).to.equal('30b31c1838de1e'); expect(bannerBid.transactionId).to.equal( 'd704d006-0d6e-4a09-ad6c-179e7e758096' - ) - expect(bannerBid.supplyTypes[0]).to.equal('display') - expect(bannerBid.adUnitId).to.equal('000000') - expect(bannerBid.sizes[0][0]).to.equal(300) - expect(bannerBid.sizes[0][1]).to.equal(250) - expect(bannerBid.sizes[1][0]).to.equal(300) - expect(bannerBid.sizes[1][1]).to.equal(600) - expect(bannerBid.requestCount).to.equal(1) - }) - it('should request an InStream Video', function() { - const videoBid = bidRequests[1] - expect(videoBid.id).to.equal('30b31c1838de1e') + ); + expect(bannerBid.supplyTypes[0]).to.equal('display'); + expect(bannerBid.adUnitId).to.equal('000000'); + expect(bannerBid.sizes[0][0]).to.equal(300); + expect(bannerBid.sizes[0][1]).to.equal(250); + expect(bannerBid.sizes[1][0]).to.equal(300); + expect(bannerBid.sizes[1][1]).to.equal(600); + expect(bannerBid.requestCount).to.equal(1); + }); + it('should request an InStream Video', function () { + const videoBid = bidRequests[1]; + expect(videoBid.id).to.equal('30b31c1838de1e'); expect(videoBid.transactionId).to.equal( 'd704d006-0d6e-4a09-ad6c-179e7e758096' - ) - expect(videoBid.supplyTypes[0]).to.equal('video') - expect(videoBid.adUnitId).to.equal('000000') - expect(videoBid.videoParams.mimes).to.equal('mp4') - expect(videoBid.videoParams.w).to.equal(300) - expect(videoBid.videoParams.h).to.equal(200) - expect(videoBid.sizes[0][0]).to.equal(300) - expect(videoBid.sizes[0][1]).to.equal(250) - expect(videoBid.sizes[1][0]).to.equal(300) - expect(videoBid.sizes[1][1]).to.equal(600) - expect(videoBid.requestCount).to.equal(1) - }) - }) - }) + ); + expect(videoBid.supplyTypes[0]).to.equal('video'); + expect(videoBid.adUnitId).to.equal('000000'); + expect(videoBid.videoParams.mimes).to.equal('mp4'); + expect(videoBid.videoParams.w).to.equal(300); + expect(videoBid.videoParams.h).to.equal(200); + expect(videoBid.sizes[0][0]).to.equal(300); + expect(videoBid.sizes[0][1]).to.equal(250); + expect(videoBid.sizes[1][0]).to.equal(300); + expect(videoBid.sizes[1][1]).to.equal(600); + expect(videoBid.requestCount).to.equal(1); + }); + }); + }); - describe('interpret response method', function() { - it('should return a void array, when the server response are not correct.', function() { - const request = { data: JSON.stringify({}) } + describe('interpret response method', function () { + it('should return a void array, when the server response are not correct.', function () { + const request = { data: JSON.stringify({}) }; const serverResponse = { - body: {} - } - const bids = spec.interpretResponse(serverResponse, request) - expect(typeof bids).to.equal('object') - expect(bids.length).to.equal(0) - }) - it('should return a void array, when the server response have no bids.', function() { - const request = { data: JSON.stringify({}) } - const serverResponse = { body: { bids: [] } } - const bids = spec.interpretResponse(serverResponse, request) - expect(typeof bids).to.equal('object') - expect(bids.length).to.equal(0) - }) - describe('when the server response return a bid', function() { - describe('the bid is a banner', function() { - it('should return a banner bid', function() { - const request = { data: JSON.stringify({}) } + body: {}, + }; + const bids = spec.interpretResponse(serverResponse, request); + expect(typeof bids).to.equal('object'); + expect(bids.length).to.equal(0); + }); + it('should return a void array, when the server response have no bids.', function () { + const request = { data: JSON.stringify({}) }; + const serverResponse = { body: { bids: [] } }; + const bids = spec.interpretResponse(serverResponse, request); + expect(typeof bids).to.equal('object'); + expect(bids.length).to.equal(0); + }); + describe('when the server response return a bid', function () { + describe('the bid is a banner', function () { + it('should return a banner bid', function () { + const request = { data: JSON.stringify({}) }; const serverResponse = { body: { bids: [ @@ -352,28 +363,30 @@ describe('Seedtag Adapter', function() { mediaType: 'display', ttl: 360, nurl: 'testurl.com/nurl', - adomain: ['advertiserdomain.com'] - } + adomain: ['advertiserdomain.com'], + }, ], - cookieSync: { url: '' } - } - } - const bids = spec.interpretResponse(serverResponse, request) - expect(bids.length).to.equal(1) - expect(bids[0].requestId).to.equal('2159a54dc2566f') - expect(bids[0].cpm).to.equal(0.5) - expect(bids[0].width).to.equal(728) - expect(bids[0].height).to.equal(90) - expect(bids[0].currency).to.equal('USD') - expect(bids[0].netRevenue).to.equal(true) - expect(bids[0].ad).to.equal('content') - expect(bids[0].nurl).to.equal('testurl.com/nurl') - expect(bids[0].meta.advertiserDomains).to.deep.equal(['advertiserdomain.com']) - }) - }) - describe('the bid is a video', function() { - it('should return a instream bid', function() { - const request = { data: JSON.stringify({}) } + cookieSync: { url: '' }, + }, + }; + const bids = spec.interpretResponse(serverResponse, request); + expect(bids.length).to.equal(1); + expect(bids[0].requestId).to.equal('2159a54dc2566f'); + expect(bids[0].cpm).to.equal(0.5); + expect(bids[0].width).to.equal(728); + expect(bids[0].height).to.equal(90); + expect(bids[0].currency).to.equal('USD'); + expect(bids[0].netRevenue).to.equal(true); + expect(bids[0].ad).to.equal('content'); + expect(bids[0].nurl).to.equal('testurl.com/nurl'); + expect(bids[0].meta.advertiserDomains).to.deep.equal([ + 'advertiserdomain.com', + ]); + }); + }); + describe('the bid is a video', function () { + it('should return a instream bid', function () { + const request = { data: JSON.stringify({}) }; const serverResponse = { body: { bids: [ @@ -386,114 +399,124 @@ describe('Seedtag Adapter', function() { height: 90, mediaType: 'video', ttl: 360, - nurl: undefined - } + nurl: undefined, + }, ], - cookieSync: { url: '' } - } - } - const bids = spec.interpretResponse(serverResponse, request) - expect(bids.length).to.equal(1) - expect(bids[0].requestId).to.equal('2159a54dc2566f') - expect(bids[0].cpm).to.equal(0.5) - expect(bids[0].width).to.equal(728) - expect(bids[0].height).to.equal(90) - expect(bids[0].currency).to.equal('USD') - expect(bids[0].netRevenue).to.equal(true) - expect(bids[0].vastXml).to.equal('content') - expect(bids[0].meta.advertiserDomains).to.deep.equal([]) - }) - }) - }) - }) + cookieSync: { url: '' }, + }, + }; + const bids = spec.interpretResponse(serverResponse, request); + expect(bids.length).to.equal(1); + expect(bids[0].requestId).to.equal('2159a54dc2566f'); + expect(bids[0].cpm).to.equal(0.5); + expect(bids[0].width).to.equal(728); + expect(bids[0].height).to.equal(90); + expect(bids[0].currency).to.equal('USD'); + expect(bids[0].netRevenue).to.equal(true); + expect(bids[0].vastXml).to.equal('content'); + expect(bids[0].meta.advertiserDomains).to.deep.equal([]); + }); + }); + }); + }); - describe('user syncs method', function() { - it('should return empty array, when iframe sync option are disabled.', function() { - const syncOption = { iframeEnabled: false } - const serverResponses = [{ body: { cookieSync: 'someUrl' } }] - const cookieSyncArray = spec.getUserSyncs(syncOption, serverResponses) - expect(cookieSyncArray.length).to.equal(0) - }) - it('should return empty array, when the server response are wrong.', function() { - const syncOption = { iframeEnabled: true } - const serverResponses = [{ body: {} }] - const cookieSyncArray = spec.getUserSyncs(syncOption, serverResponses) - expect(cookieSyncArray.length).to.equal(0) - }) - it('should return empty array, when the server response are void.', function() { - const syncOption = { iframeEnabled: true } - const serverResponses = [{ body: { cookieSync: '' } }] - const cookieSyncArray = spec.getUserSyncs(syncOption, serverResponses) - expect(cookieSyncArray.length).to.equal(0) - }) - it('should return a array with the cookie sync, when the server response with a cookie sync.', function() { - const syncOption = { iframeEnabled: true } - const serverResponses = [{ body: { cookieSync: 'someUrl' } }] - const cookieSyncArray = spec.getUserSyncs(syncOption, serverResponses) - expect(cookieSyncArray.length).to.equal(1) - expect(cookieSyncArray[0].type).to.equal('iframe') - expect(cookieSyncArray[0].url).to.equal('someUrl') - }) - }) + describe('user syncs method', function () { + it('should return empty array, when iframe sync option are disabled.', function () { + const syncOption = { iframeEnabled: false }; + const serverResponses = [{ body: { cookieSync: 'someUrl' } }]; + const cookieSyncArray = spec.getUserSyncs(syncOption, serverResponses); + expect(cookieSyncArray.length).to.equal(0); + }); + it('should return empty array, when the server response are wrong.', function () { + const syncOption = { iframeEnabled: true }; + const serverResponses = [{ body: {} }]; + const cookieSyncArray = spec.getUserSyncs(syncOption, serverResponses); + expect(cookieSyncArray.length).to.equal(0); + }); + it('should return empty array, when the server response are void.', function () { + const syncOption = { iframeEnabled: true }; + const serverResponses = [{ body: { cookieSync: '' } }]; + const cookieSyncArray = spec.getUserSyncs(syncOption, serverResponses); + expect(cookieSyncArray.length).to.equal(0); + }); + it('should return a array with the cookie sync, when the server response with a cookie sync.', function () { + const syncOption = { iframeEnabled: true }; + const serverResponses = [{ body: { cookieSync: 'someUrl' } }]; + const cookieSyncArray = spec.getUserSyncs(syncOption, serverResponses); + expect(cookieSyncArray.length).to.equal(1); + expect(cookieSyncArray[0].type).to.equal('iframe'); + expect(cookieSyncArray[0].url).to.equal('someUrl'); + }); + }); describe('onTimeout', function () { - beforeEach(function() { - sinon.stub(utils, 'triggerPixel') - }) + beforeEach(function () { + sinon.stub(utils, 'triggerPixel'); + }); - afterEach(function() { - utils.triggerPixel.restore() - }) + afterEach(function () { + utils.triggerPixel.restore(); + }); it('should return the correct endpoint', function () { - const params = { publisherId: '0000', adUnitId: '11111' } - const timeoutData = [{ params: [ params ] }]; + const params = { publisherId: '0000', adUnitId: '11111' }; + const timeout = 3000; + const timeoutData = [{ params: [params], timeout }]; const timeoutUrl = getTimeoutUrl(timeoutData); expect(timeoutUrl).to.equal( 'https://s.seedtag.com/se/hb/timeout?publisherToken=' + - params.publisherId + - '&adUnitId=' + - params.adUnitId - ) - }) + params.publisherId + + '&adUnitId=' + + params.adUnitId + + '&timeout=' + + timeout + ); + }); - it('should set the timeout pixel', function() { - const params = { publisherId: '0000', adUnitId: '11111' } - const timeoutData = [{ params: [ params ] }]; - spec.onTimeout(timeoutData) - expect(utils.triggerPixel.calledWith('https://s.seedtag.com/se/hb/timeout?publisherToken=' + - params.publisherId + - '&adUnitId=' + - params.adUnitId)).to.equal(true); - }) - }) + it('should set the timeout pixel', function () { + const params = { publisherId: '0000', adUnitId: '11111' }; + const timeout = 3000; + const timeoutData = [{ params: [params], timeout }]; + spec.onTimeout(timeoutData); + expect( + utils.triggerPixel.calledWith( + 'https://s.seedtag.com/se/hb/timeout?publisherToken=' + + params.publisherId + + '&adUnitId=' + + params.adUnitId + + '&timeout=' + + timeout + ) + ).to.equal(true); + }); + }); describe('onBidWon', function () { - beforeEach(function() { - sinon.stub(utils, 'triggerPixel') - }) + beforeEach(function () { + sinon.stub(utils, 'triggerPixel'); + }); - afterEach(function() { - utils.triggerPixel.restore() - }) + afterEach(function () { + utils.triggerPixel.restore(); + }); - describe('without nurl', function() { - const bid = {} + describe('without nurl', function () { + const bid = {}; - it('does not create pixel ', function() { - spec.onBidWon(bid) + it('does not create pixel ', function () { + spec.onBidWon(bid); expect(utils.triggerPixel.called).to.equal(false); - }) - }) + }); + }); describe('with nurl', function () { - const nurl = 'http://seedtag_domain/won' - const bid = { nurl } + const nurl = 'http://seedtag_domain/won'; + const bid = { nurl }; - it('creates nurl pixel if bid nurl', function() { - spec.onBidWon({ nurl }) + it('creates nurl pixel if bid nurl', function () { + spec.onBidWon({ nurl }); expect(utils.triggerPixel.calledWith(nurl)).to.equal(true); - }) - }) - }) -}) + }); + }); + }); +}); diff --git a/test/spec/modules/sharedIdSystem_spec.js b/test/spec/modules/sharedIdSystem_spec.js index 534d0b3f381..8ef34a1599e 100644 --- a/test/spec/modules/sharedIdSystem_spec.js +++ b/test/spec/modules/sharedIdSystem_spec.js @@ -50,10 +50,10 @@ describe('SharedId System', function () { expect(callbackSpy.calledOnce).to.be.true; expect(callbackSpy.lastCall.lastArg).to.equal(UUID); }); - it('should log message if coppa is set', function () { + it('should abort if coppa is set', function () { coppaDataHandlerDataStub.returns('true'); - sharedIdSystemSubmodule.getId({}); - expect(utils.logInfo.args[0][0]).to.exist.and.to.equal('PubCommonId: IDs not provided for coppa requests, exiting PubCommonId'); + const result = sharedIdSystemSubmodule.getId({}); + expect(result).to.be.undefined; }); }); describe('SharedId System extendId()', function () { @@ -85,10 +85,56 @@ describe('SharedId System', function () { let pubcommId = sharedIdSystemSubmodule.extendId(config, undefined, 'TestId').id; expect(pubcommId).to.equal('TestId'); }); - it('should log message if coppa is set', function () { + it('should abort if coppa is set', function () { coppaDataHandlerDataStub.returns('true'); - sharedIdSystemSubmodule.extendId({}, undefined, 'TestId'); - expect(utils.logInfo.args[0][0]).to.exist.and.to.equal('PubCommonId: IDs not provided for coppa requests, exiting PubCommonId'); + const result = sharedIdSystemSubmodule.extendId({params: {extend: true}}, undefined, 'TestId'); + expect(result).to.be.undefined; + }); + }); + + describe('SharedID System domainOverride', () => { + let sandbox, domain, cookies, rejectCookiesFor; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + sandbox.stub(document, 'domain').get(() => domain); + cookies = {}; + sandbox.stub(storage, 'getCookie').callsFake((key) => cookies[key]); + rejectCookiesFor = null; + sandbox.stub(storage, 'setCookie').callsFake((key, value, expires, sameSite, domain) => { + if (domain !== rejectCookiesFor) { + if (expires != null) { + expires = new Date(expires); + } + if (expires == null || expires > Date.now()) { + cookies[key] = value; + } else { + delete cookies[key]; + } + } + }); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should return TLD if cookies can be set there', () => { + domain = 'sub.domain.com'; + rejectCookiesFor = 'com'; + expect(sharedIdSystemSubmodule.domainOverride()).to.equal('domain.com'); + }); + + it('should return undefined when cookies cannot be set', () => { + domain = 'sub.domain.com'; + rejectCookiesFor = 'sub.domain.com'; + expect(sharedIdSystemSubmodule.domainOverride()).to.be.undefined; + }); + + it('should return half-way domain if parent domain rejects cookies', () => { + domain = 'inner.outer.domain.com'; + rejectCookiesFor = 'domain.com'; + expect(sharedIdSystemSubmodule.domainOverride()).to.equal('outer.domain.com'); }); }); }); diff --git a/test/spec/modules/sharethroughBidAdapter_spec.js b/test/spec/modules/sharethroughBidAdapter_spec.js index db21af5f6b3..39238cc877f 100644 --- a/test/spec/modules/sharethroughBidAdapter_spec.js +++ b/test/spec/modules/sharethroughBidAdapter_spec.js @@ -1,4 +1,5 @@ import { expect } from 'chai'; +import * as sinon from 'sinon'; import { sharethroughAdapterSpec, sharethroughInternal } from 'modules/sharethroughBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; import { config } from 'src/config'; @@ -157,7 +158,6 @@ describe('sharethrough adapter spec', function () { startdelay: 42, skipmin: 10, skipafter: 20, - placement: 1, delivery: 1, companiontype: 'companion type', companionad: 'companion ad', @@ -205,6 +205,7 @@ describe('sharethrough adapter spec', function () { expect(openRtbReq.cur).to.deep.equal(['USD']); expect(openRtbReq.tmax).to.equal(242); + expect(Object.keys(openRtbReq.site)).to.have.length(3); expect(openRtbReq.site.domain).not.to.be.undefined; expect(openRtbReq.site.page).not.to.be.undefined; expect(openRtbReq.site.ref).to.equal('https://referer.com'); @@ -256,6 +257,17 @@ describe('sharethrough adapter spec', function () { }); }); + describe('no referer provided', () => { + beforeEach(() => { + bidderRequest = {}; + }); + + it('should set referer to undefined', () => { + const openRtbReq = spec.buildRequests(bidRequests, bidderRequest)[0].data; + expect(openRtbReq.site.ref).to.be.undefined; + }); + }); + describe('regulation', () => { describe('gdpr', () => { it('should populate request accordingly when gdpr applies', () => { @@ -419,17 +431,90 @@ describe('sharethrough adapter spec', function () { expect(videoImp.startdelay).to.equal(0); expect(videoImp.skipmin).to.equal(0); expect(videoImp.skipafter).to.equal(0); - expect(videoImp.placement).to.be.undefined; + expect(videoImp.placement).to.equal(1); expect(videoImp.delivery).to.be.undefined; expect(videoImp.companiontype).to.be.undefined; expect(videoImp.companionad).to.be.undefined; }); - it('should not return a video impression if context is outstream', () => { - bidRequests[1].mediaTypes.video.context = 'outstream'; - const builtRequest = spec.buildRequests(bidRequests, bidderRequest)[1]; + describe('outstream', () => { + it('should use placement value if provided', () => { + bidRequests[1].mediaTypes.video.context = 'outstream'; + bidRequests[1].mediaTypes.video.placement = 3; + + const builtRequest = spec.buildRequests(bidRequests, bidderRequest)[1]; + const videoImp = builtRequest.data.imp[0].video; + + expect(videoImp.placement).to.equal(3); + }); + + it('should default placement to 4 if not provided', () => { + bidRequests[1].mediaTypes.video.context = 'outstream'; + + const builtRequest = spec.buildRequests(bidRequests, bidderRequest)[1]; + const videoImp = builtRequest.data.imp[0].video; + + expect(videoImp.placement).to.equal(4); + }); + }); + }); + + describe('first party data', () => { + const firstPartyData = { + site: { + name: 'example', + keywords: 'power tools, drills', + search: 'drill', + content: { + userrating: '4', + }, + ext: { + data: { + pageType: 'article', + category: 'repair', + }, + }, + }, + user: { + yob: 1985, + gender: 'm', + ext: { + data: { + registered: true, + interests: ['cars'], + }, + }, + }, + }; + + let configStub; + + beforeEach(() => { + configStub = sinon.stub(config, 'getConfig'); + configStub.withArgs('ortb2').returns(firstPartyData); + }); + + afterEach(() => { + configStub.restore(); + }); + + it('should include first party data in open rtb request, site section', () => { + const openRtbReq = spec.buildRequests(bidRequests, bidderRequest)[0].data; + + expect(openRtbReq.site.name).to.equal(firstPartyData.site.name); + expect(openRtbReq.site.keywords).to.equal(firstPartyData.site.keywords); + expect(openRtbReq.site.search).to.equal(firstPartyData.site.search); + expect(openRtbReq.site.content).to.deep.equal(firstPartyData.site.content); + expect(openRtbReq.site.ext).to.deep.equal(firstPartyData.site.ext); + }); + + it('should include first party data in open rtb request, user section', () => { + const openRtbReq = spec.buildRequests(bidRequests, bidderRequest)[0].data; - expect(builtRequest).to.be.undefined; + expect(openRtbReq.user.yob).to.equal(firstPartyData.user.yob); + expect(openRtbReq.user.gender).to.equal(firstPartyData.user.gender); + expect(openRtbReq.user.ext.data).to.deep.equal(firstPartyData.user.ext.data); + expect(openRtbReq.user.ext.eids).not.to.be.undefined; }); }); }); diff --git a/test/spec/modules/showheroes-bsBidAdapter_spec.js b/test/spec/modules/showheroes-bsBidAdapter_spec.js index ad2210c18c6..69e8343dfc9 100644 --- a/test/spec/modules/showheroes-bsBidAdapter_spec.js +++ b/test/spec/modules/showheroes-bsBidAdapter_spec.js @@ -13,6 +13,7 @@ const adomain = ['showheroes.com']; const gdpr = { 'gdprConsent': { + 'apiVersion': 2, 'consentString': 'BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA', 'gdprApplies': true } @@ -332,6 +333,7 @@ describe('shBidAdapter', function () { { 'cpm': 5, 'creativeId': 'c_38b373e1e31c18', + 'adUnitCode': 'adunit-code-1', 'currency': 'EUR', 'width': 640, 'height': 480, diff --git a/test/spec/modules/sizeMappingV2_spec.js b/test/spec/modules/sizeMappingV2_spec.js index ab06ddc8f51..9bbd472c7e0 100644 --- a/test/spec/modules/sizeMappingV2_spec.js +++ b/test/spec/modules/sizeMappingV2_spec.js @@ -13,10 +13,11 @@ import { getAdUnitDetail, getFilteredMediaTypes, getBids, - internal + internal, setupAdUnitMediaTypes } from '../../../modules/sizeMappingV2.js'; import { adUnitSetupChecks } from '../../../src/prebid.js'; +import {deepClone} from '../../../src/utils.js'; const AD_UNITS = [{ code: 'div-gpt-ad-1460505748561-0', @@ -193,49 +194,23 @@ describe('sizeMappingV2', function () { utils.logError.restore(); }); - it('should filter out adUnit if it does not contain the required property mediaTypes', function () { - let adUnits = utils.deepClone(AD_UNITS); - delete adUnits[0].mediaTypes; - // before checkAdUnitSetupHook is called, the length of adUnits should be '2' - expect(adUnits.length).to.equal(2); - adUnits = checkAdUnitSetupHook(adUnits); - - // after checkAdUnitSetupHook is called, the length of adUnits should be '1' - expect(adUnits.length).to.equal(1); - expect(adUnits[0].code).to.equal('div-gpt-ad-1460505748561-1'); - }); + describe('basic validation', () => { + let validateAdUnit; - it('should filter out adUnit if it does not contain the required property "bids"', function() { - let adUnits = utils.deepClone(AD_UNITS); - delete adUnits[0].mediaTypes; - // before checkAdUnitSetupHook is called, the length of the adUnits should equal '2' - expect(adUnits.length).to.equal(2); - adUnits = checkAdUnitSetupHook(adUnits); - - // after checkAdUnitSetupHook is called, the length of the adUnits should be '1' - expect(adUnits.length).to.equal(1); - expect(adUnits[0].code).to.equal('div-gpt-ad-1460505748561-1'); - }); - - it('should filter out adUnit if it has declared property mediaTypes with an empty object', function () { - let adUnits = utils.deepClone(AD_UNITS); - adUnits[0].mediaTypes = {}; - // before checkAdUnitSetupHook is called, the length of adUnits should be '2' - expect(adUnits.length).to.equal(2); - adUnits = checkAdUnitSetupHook(adUnits); - - // after checkAdUnitSetupHook is called, the length of adUnits should be '1' - expect(adUnits.length).to.equal(1); - expect(adUnits[0].code).to.equal('div-gpt-ad-1460505748561-1'); - }); + beforeEach(() => { + validateAdUnit = sinon.stub(adUnitSetupChecks, 'validateAdUnit'); + }); - it('should log an error message if Ad Unit does not contain the required property "mediaTypes"', function () { - let adUnits = utils.deepClone(AD_UNITS); - delete adUnits[0].mediaTypes; + afterEach(() => { + validateAdUnit.restore(); + }); - checkAdUnitSetupHook(adUnits); - sinon.assert.callCount(utils.logError, 1); - sinon.assert.calledWith(utils.logError, 'Detected adUnit.code \'div-gpt-ad-1460505748561-0\' did not have a \'mediaTypes\' object defined. This is a required field for the auction, so this adUnit has been removed.'); + it('should filter out adUnits that do not pass adUnitSetupChecks.validateAdUnit', () => { + validateAdUnit.returns(null); + const adUnits = checkAdUnitSetupHook(utils.deepClone(AD_UNITS)); + AD_UNITS.forEach((u) => sinon.assert.calledWith(validateAdUnit, u)); + expect(adUnits.length).to.equal(0); + }); }); describe('banner mediaTypes checks', function () { @@ -1106,14 +1081,14 @@ describe('sizeMappingV2', function () { internal.checkBidderSizeConfigFormat.restore(); internal.getActiveSizeBucket.restore(); }); - it('should return an empty array if the bidder sizeConfig object is not formatted correctly', function () { + it('should return an empty set if the bidder sizeConfig object is not formatted correctly', function () { const sizeConfig = [ { minViewPort: [], relevantMediaTypes: ['none'] }, { minViewPort: [700, 0], relevantMediaTypes: ['banner', 'video'] } ]; const activeViewport = [720, 600]; const relevantMediaTypes = getRelevantMediaTypesForBidder(sizeConfig, activeViewport); - expect(relevantMediaTypes).to.deep.equal([]); + expect(relevantMediaTypes.size).to.equal(0) }); it('should call function checkBidderSizeConfigFormat() once', function () { @@ -1140,220 +1115,51 @@ describe('sizeMappingV2', function () { sinon.assert.calledWith(internal.getActiveSizeBucket, sizeConfig, activeViewport); }); - it('should return the array contained in "relevantMediaTypes" property whose sizeBucket matches with the current viewport', function () { + it('should return the types contained in "relevantMediaTypes" property whose sizeBucket matches with the current viewport', function () { const sizeConfig = [ { minViewPort: [0, 0], relevantMediaTypes: ['none'] }, { minViewPort: [700, 0], relevantMediaTypes: ['banner', 'video'] } ]; const activeVewport = [720, 600]; const relevantMediaTypes = getRelevantMediaTypesForBidder(sizeConfig, activeVewport); - expect(relevantMediaTypes).to.deep.equal(['banner', 'video']); + expect([...relevantMediaTypes]).to.deep.equal(['banner', 'video']); }); }); - describe('getAdUnitDetail(auctionId, adUnit, labels)', function () { + describe('getAdUnitDetail', function () { const adUnitDetailFixture_1 = { - adUnitCode: 'div-gpt-ad-1460505748561-0', - mediaTypes: { - banner: { - sizeConfig: [ - { minViewPort: [0, 0], sizes: [] }, // remove if < 750px - { minViewPort: [750, 0], sizes: [[300, 250], [300, 600]] }, // between 750px and 1199px - { minViewPort: [1200, 0], sizes: [[970, 90], [728, 90], [300, 250]] }, // between 1200px and 1599px - { minViewPort: [1600, 0], sizes: [[1000, 300], [970, 90], [728, 90], [300, 250]] } // greater than 1600px - ] - }, - video: { - context: 'instream', - sizeConfig: [ - { minViewPort: [0, 0], playerSize: [] }, - { minViewPort: [800, 0], playerSize: [[640, 400]] }, - { minViewPort: [1200, 0], playerSize: [] } - ] - }, - native: { - image: { - required: true, - sizes: [150, 50] - }, - title: { - required: true, - len: 80 - }, - sponsoredBy: { - required: true - }, - clickUrl: { - required: true - }, - privacyLink: { - required: false - }, - body: { - required: true - }, - icon: { - required: true, - sizes: [50, 50] - }, - sizeConfig: [ - { minViewPort: [0, 0], active: false }, - { minViewPort: [600, 0], active: true }, - { minViewPort: [1000, 0], active: false } - ] - } - }, sizeBucketToSizeMap: {}, activeViewport: {}, - transformedMediaTypes: {}, - cacheHits: 0, - instance: 1, - isLabelActivated: true, - }; - const adUnitDetailFixture_2 = { - adUnitCode: 'div-gpt-ad-1460505748561-1', - mediaTypes: { - banner: { - sizes: [[300, 250], [300, 600]] - }, - video: { - context: 'instream', - playerSize: [300, 460] - } - }, - sizeBucketToSizeMap: {}, - activeViewport: {}, - cacheHits: 0, - instance: 1, - isLabelActivated: true, transformedMediaTypes: { banner: {}, video: {} } } - // adunit with same code at adUnitDetailFixture_1 but differnet mediaTypes object - const adUnitDetailFixture_3 = { - adUnitCode: 'div-gpt-ad-1460505748561-0', - mediaTypes: { - banner: { - sizeConfig: [ - { minViewPort: [0, 0], sizes: [] }, - { minViewPort: [1000, 0], sizes: [[1000, 300], [1000, 90], [970, 250], [970, 90], [728, 90]] } - ] - } - }, - sizeBucketToSizeMap: {}, - activeViewport: {}, - transformedMediaTypes: {}, - cacheHits: 0, - instance: 1, - isLabelActivated: true, - } const labels = ['mobile']; beforeEach(function () { - sinon - .stub(sizeMappingInternalStore, 'getAuctionDetail') - .withArgs('a1b2c3') - .returns({ - usingSizeMappingV2: true, - adUnits: [adUnitDetailFixture_1] - }); - - sinon - .stub(sizeMappingInternalStore, 'setAuctionDetail') - .withArgs('a1b2c3', adUnitDetailFixture_2); - const getFilteredMediaTypesStub = sinon.stub(internal, 'getFilteredMediaTypes'); getFilteredMediaTypesStub .withArgs(AD_UNITS[1].mediaTypes) - .returns(adUnitDetailFixture_2); - - getFilteredMediaTypesStub - .withArgs(adUnitDetailFixture_3.mediaTypes) - .returns(adUnitDetailFixture_3); - + .returns(adUnitDetailFixture_1); sinon.spy(utils, 'logInfo'); sinon.spy(utils, 'deepEqual'); }); afterEach(function () { - sizeMappingInternalStore.getAuctionDetail.restore(); - sizeMappingInternalStore.setAuctionDetail.restore(); internal.getFilteredMediaTypes.restore(); utils.logInfo.restore(); utils.deepEqual.restore(); }); - it('should return adUnit detail object from "sizeMappingInternalStore" if adUnit is already present in the store', function () { - const [adUnit] = utils.deepClone(AD_UNITS); - const adUnitDetail = getAdUnitDetail('a1b2c3', adUnit, labels); - sinon.assert.callCount(sizeMappingInternalStore.getAuctionDetail, 1); - sinon.assert.callCount(utils.deepEqual, 1); - sinon.assert.callCount(internal.getFilteredMediaTypes, 0); - expect(adUnitDetail.cacheHits).to.equal(1); - expect(adUnitDetail).to.deep.equal(adUnitDetailFixture_1); - }); - - it('should NOT return adunit detail object from "sizeMappingInternalStore" if adUnit with the SAME CODE BUT DIFFERENT MEDIATYPES OBJECT is present in the store', function () { - const [adUnit] = utils.deepClone(AD_UNITS); - adUnit.mediaTypes = { - banner: { - sizeConfig: [ - { minViewPort: [0, 0], sizes: [] }, - { minViewPort: [1000, 0], sizes: [[1000, 300], [1000, 90], [970, 250], [970, 90], [728, 90]] } - ] - } - }; - const adUnitDetail = getAdUnitDetail('a1b2c3', adUnit, labels); - sinon.assert.callCount(sizeMappingInternalStore.getAuctionDetail, 1); - sinon.assert.callCount(utils.deepEqual, 1); - expect(adUnitDetail).to.not.deep.equal(adUnitDetailFixture_1); - sinon.assert.callCount(internal.getFilteredMediaTypes, 1); - }); - - it('should store value in "sizeMappingInterStore" object if adUnit is NOT preset in this object', function () { - const [, adUnit] = utils.deepClone(AD_UNITS); - const adUnitDetail = getAdUnitDetail('a1b2c3', adUnit, labels); - sinon.assert.callCount(sizeMappingInternalStore.setAuctionDetail, 1); - sinon.assert.callCount(internal.getFilteredMediaTypes, 1); - expect(adUnitDetail).to.deep.equal(adUnitDetailFixture_2); - }); - it('should log info message to show the details for activeSizeBucket', function () { const [, adUnit] = utils.deepClone(AD_UNITS); - getAdUnitDetail('a1b2c3', adUnit, labels); + getAdUnitDetail(adUnit, labels, 1); sinon.assert.callCount(utils.logInfo, 1); - sinon.assert.calledWith(utils.logInfo, `Size Mapping V2:: Ad Unit: div-gpt-ad-1460505748561-1(1) => Active size buckets after filtration: `, adUnitDetailFixture_2.sizeBucketToSizeMap); - }); - - it('should increment "instance" count if presence of "Identical ad units" is detected', function () { - const adUnit = { - code: 'div-gpt-ad-1460505748561-0', - mediaTypes: { - banner: { - sizeConfig: [{ minViewPort: [0, 0], sizes: [[300, 300]] }] - } - }, - bids: [{ - bidder: 'appnexus', - params: 12 - }] - }; - - internal.getFilteredMediaTypes.restore(); - - sinon.stub(internal, 'getFilteredMediaTypes') - .withArgs(adUnit.mediaTypes) - .returns({ mediaTypes: {}, sizeBucketToSizeMap: {}, activeViewPort: [], transformedMediaTypes: {} }); - - const adUnitDetail = getAdUnitDetail('a1b2c3', adUnit, labels); - sinon.assert.callCount(sizeMappingInternalStore.setAuctionDetail, 1); - sinon.assert.callCount(internal.getFilteredMediaTypes, 1); - expect(adUnitDetail.instance).to.equal(2); + sinon.assert.calledWith(utils.logInfo, `Size Mapping V2:: Ad Unit: div-gpt-ad-1460505748561-1(1) => Active size buckets after filtration: `, adUnitDetailFixture_1.sizeBucketToSizeMap); }); it('should not execute "getFilteredMediaTypes" function if label is not activated on the ad unit', function () { const [adUnit] = utils.deepClone(AD_UNITS); adUnit.labelAny = ['tablet']; - getAdUnitDetail('a1b2c3', adUnit, labels); + getAdUnitDetail(adUnit, labels, 1); // assertions sinon.assert.callCount(internal.getFilteredMediaTypes, 0); @@ -1376,57 +1182,8 @@ describe('sizeMappingV2', function () { utils.getWindowTop.restore(); utils.logWarn.restore(); }); - it('should return filteredMediaTypes object with all four properties (mediaTypes, transformedMediaTypes, activeViewport, sizeBucketToSizeMap) evaluated correctly', function () { + it('should return filteredMediaTypes object with all properties (transformedMediaTypes, activeViewport, sizeBucketToSizeMap) evaluated correctly', function () { const [adUnit] = utils.deepClone(AD_UNITS); - const expectedMediaTypes = { - banner: { - sizeConfig: [ - { minViewPort: [0, 0], sizes: [] }, // remove if < 750px - { minViewPort: [750, 0], sizes: [[300, 250], [300, 600]] }, // between 750px and 1199px - { minViewPort: [1200, 0], sizes: [[970, 90], [728, 90], [300, 250]] }, // between 1200px and 1599px - { minViewPort: [1600, 0], sizes: [[1000, 300], [970, 90], [728, 90], [300, 250]] } // greater than 1600px - ] - }, - video: { - context: 'instream', - sizeConfig: [ - { minViewPort: [0, 0], playerSize: [] }, - { minViewPort: [800, 0], playerSize: [[640, 400]] }, - { minViewPort: [1200, 0], playerSize: [] } - ] - }, - native: { - image: { - required: true, - sizes: [150, 50] - }, - title: { - required: true, - len: 80 - }, - sponsoredBy: { - required: true - }, - clickUrl: { - required: true - }, - privacyLink: { - required: false - }, - body: { - required: true - }, - icon: { - required: true, - sizes: [50, 50] - }, - sizeConfig: [ - { minViewPort: [0, 0], active: false }, - { minViewPort: [600, 0], active: true }, - { minViewPort: [1000, 0], active: false } - ] - } - }; const expectedSizeBucketToSizeMap = { banner: { activeSizeBucket: [1600, 0], @@ -1459,8 +1216,7 @@ describe('sizeMappingV2', function () { ] } }; - const { mediaTypes, sizeBucketToSizeMap, activeViewport, transformedMediaTypes } = getFilteredMediaTypes(adUnit.mediaTypes); - expect(mediaTypes).to.deep.equal(expectedMediaTypes); + const { sizeBucketToSizeMap, activeViewport, transformedMediaTypes } = getFilteredMediaTypes(adUnit.mediaTypes); expect(activeViewport).to.deep.equal(expectedActiveViewport); expect(sizeBucketToSizeMap).to.deep.equal(expectedSizeBucketToSizeMap); expect(transformedMediaTypes).to.deep.equal(expectedTransformedMediaTypes); @@ -1478,7 +1234,8 @@ describe('sizeMappingV2', function () { }); }); - describe('getBids({ bidderCode, auctionId, bidderRequestId, adUnits, labels, src })', function () { + describe('setupAdUnitsForLabels', function () { + let adUnits, adUnitDetail; const basic_AdUnit = [{ code: 'adUnit1', mediaTypes: { @@ -1508,46 +1265,31 @@ describe('sizeMappingV2', function () { }], transactionId: '123456' }]; - const adUnitDetailFixture = { - adUnitCode: 'adUnit1', - transactionId: '123456', - sizes: [[300, 200], [400, 600]], - mediaTypes: { - banner: { - sizeConfig: [ - { minViewPort: [0, 0], sizes: [] }, - { minViewPort: [600, 0], sizes: [[300, 200], [400, 600]] } - ] - } - }, - sizeBucketToSizeMap: { - banner: { - activeSizeBucket: [[500, 0]], - activeSizeDimensions: [[300, 200], [400, 600]] - } - }, - activeViewport: [560, 260], - transformedMediaTypes: { - banner: { - filteredSizeConfig: [ - { minViewPort: [500, 0], sizes: [[300, 200], [400, 600]] } - ], - sizeConfig: [ - { minViewPort: [0, 0], sizes: [[]] }, - { minViewPort: [500, 0], sizes: [[300, 200], [400, 600]] } - ], - sizes: [[300, 200], [400, 600]] - } - }, - isLabelActivated: true, - instance: 1, - cacheHits: 0 - }; + + const bidderMap = (adUnit) => Object.fromEntries(adUnit.bids.map((bid) => [bid.bidder, bid])); + beforeEach(function () { + adUnits = deepClone(basic_AdUnit); + adUnitDetail = { + activeViewport: [560, 260], + transformedMediaTypes: { + banner: { + filteredSizeConfig: [ + { minViewPort: [500, 0], sizes: [[300, 200], [400, 600]] } + ], + sizeConfig: [ + { minViewPort: [0, 0], sizes: [[]] }, + { minViewPort: [500, 0], sizes: [[300, 200], [400, 600]] } + ], + sizes: [[300, 200], [400, 600]] + } + }, + isLabelActivated: true, + }; sinon .stub(internal, 'getAdUnitDetail') - .withArgs('6d51e2d7-1447-4242-b6af-aaa5525a2c6e', basic_AdUnit[0], []) - .returns(adUnitDetailFixture); + .withArgs(adUnits[0], []) + .callsFake(() => adUnitDetail); sinon.spy(internal, 'getRelevantMediaTypesForBidder'); @@ -1564,153 +1306,82 @@ describe('sizeMappingV2', function () { utils.logWarn.restore(); }); - it('should return an array of bids specific to the bidder', function () { - const expectedMediaTypes = { - banner: { - filteredSizeConfig: [ - { minViewPort: [500, 0], sizes: [[300, 200], [400, 600]] } - ], - sizeConfig: [ - { minViewPort: [0, 0], sizes: [[]] }, - { minViewPort: [500, 0], sizes: [[300, 200], [400, 600]] } - ], - sizes: [[300, 200], [400, 600]] - } + it('should update adUnit mediaTypes', function () { + adUnitDetail = { + activeViewport: [560, 260], + transformedMediaTypes: { + banner: { + filteredSizeConfig: [ + { minViewPort: [500, 0], sizes: [[300, 200], [400, 600]] } + ], + sizeConfig: [ + { minViewPort: [0, 0], sizes: [[]] }, + { minViewPort: [500, 0], sizes: [[300, 200], [400, 600]] } + ], + sizes: [[300, 200], [400, 600]] + } + }, + isLabelActivated: true, }; - const bidRequests_1 = getBids({ - bidderCode: 'appnexus', - auctionId: '6d51e2d7-1447-4242-b6af-aaa5525a2c6e', - bidderRequestId: '393a43193a0ac', - adUnits: basic_AdUnit, - labels: [], - src: 'client' - }); - expect(bidRequests_1[0].mediaTypes).to.deep.equal(expectedMediaTypes); - expect(bidRequests_1[0].bidder).to.equal('appnexus'); - - const bidRequests_2 = getBids({ - bidderCode: 'rubicon', - auctionId: '6d51e2d7-1447-4242-b6af-aaa5525a2c6e', - bidderRequestId: '393a43193a0aa', - adUnits: basic_AdUnit, - labels: [], - src: 'client' - }); - expect(bidRequests_2[0]).to.be.undefined; + const actual = setupAdUnitMediaTypes(adUnits, [])[0]; + + expect(actual.mediaTypes).to.deep.equal(adUnitDetail.transformedMediaTypes); + const bids = bidderMap(actual); + expect(bids.appnexus).to.not.be.undefined; + expect(bids.appnexus.mediaTypes).to.be.undefined; + expect(bids.rubicon).to.be.undefined; sinon.assert.callCount(internal.getRelevantMediaTypesForBidder, 1); }); it('should log an error message if ad unit is disabled because there are no active media types left after size config filtration', function () { - internal.getAdUnitDetail.restore(); - - const adUnit = utils.deepClone(basic_AdUnit); - adUnit[0].mediaTypes.banner.sizeConfig = [ + adUnits[0].mediaTypes.banner.sizeConfig = [ { minViewPort: [0, 0], sizes: [] }, { minViewPort: [600, 0], sizes: [[300, 200], [400, 600]] } ]; - const adUnitDetailFixture = { - adUnitCode: 'adUnit1', - mediaTypes: { - banner: { - sizeConfig: [ - { minViewPort: [0, 0], sizes: [] }, - { minViewPort: [600, 0], sizes: [[300, 200], [400, 600]] } - ] - } - }, - sizeBucketToSizeMap: { - banner: { - activeSizeBucket: [0, 0], - activeSizeDimensions: [[]] - } - }, + adUnitDetail = { activeViewport: [560, 260], transformedMediaTypes: {}, isLabelActivated: true, - instance: 1, - cacheHits: 0 }; - sinon - .stub(internal, 'getAdUnitDetail') - .withArgs('6d51e2d7-1447-4242-b6af-aaa5525a2c6e', adUnit[0]) - .returns(adUnitDetailFixture); - - const bidRequests = getBids({ - bidderCode: 'appnexus', - auctionId: '6d51e2d7-1447-4242-b6af-aaa5525a2c6e', - bidderRequestId: '393a43193a0ac', - adUnits: adUnit, - labels: [], - src: 'client' - }); - expect(bidRequests[0]).to.be.undefined; + const actual = setupAdUnitMediaTypes(adUnits, [])[0]; + expect(actual).to.be.undefined; sinon.assert.callCount(utils.logInfo, 1); sinon.assert.calledWith(utils.logInfo, `Size Mapping V2:: Ad Unit: adUnit1(1) => Ad unit disabled since there are no active media types after sizeConfig filtration.`); }); it('should throw an error if bidder level sizeConfig is not configured properly', function () { - internal.getAdUnitDetail.restore(); - - const adUnit = utils.deepClone(basic_AdUnit); - adUnit[0].bids[1].sizeConfig = [ + adUnits[0].bids[1].sizeConfig = [ { minViewPort: [], relevantMediaTypes: ['none'] }, { minViewPort: [700, 0], relevantMediaTypes: ['banner'] } ]; - - sinon - .stub(internal, 'getAdUnitDetail') - .withArgs('6d51e2d7-1447-4242-b6af-aaa5525a2c6e', adUnit[0]) - .returns(adUnitDetailFixture); - - const bidRequests = getBids({ - bidderCode: 'rubicon', - auctionId: '6d51e2d7-1447-4242-b6af-aaa5525a2c6e', - bidderRequestId: '393a43193a0ac', - adUnits: adUnit, - labels: [], - src: 'client' - }); - - expect(bidRequests[0]).to.not.be.undefined; + const actual = setupAdUnitMediaTypes(adUnits, [])[0]; + expect(actual).to.not.be.undefined; + const bids = bidderMap(actual); + expect(bids.rubicon.mediaTypes).to.be.undefined; sinon.assert.callCount(utils.logError, 1); - sinon.assert.calledWith(utils.logError, `Size Mapping V2:: Ad Unit: adUnit1(1), Bidder: rubicon => 'sizeConfig' is not configured properly. This bidder won't be eligible for sizeConfig checks and will remail active.`); + sinon.assert.calledWith(utils.logError, `Size Mapping V2:: Ad Unit: adUnit1(1), Bidder: rubicon => 'sizeConfig' is not configured properly. This bidder won't be eligible for sizeConfig checks and will remain active.`); }); - it('should ensure bidder relevantMediaTypes is a subset of active media types at the ad unit level', function () { - internal.getAdUnitDetail.restore(); - - const adUnit = utils.deepClone(basic_AdUnit); - adUnit[0].bids[1].sizeConfig = [ + it('should ensure only relevant sizes are in adUnit.mediaTypes', function () { + adUnits[0].bids[1].sizeConfig = [ { minViewPort: [0, 0], relevantMediaTypes: ['none'] }, { minViewPort: [400, 0], relevantMediaTypes: ['banner'] } ]; - sinon - .stub(internal, 'getAdUnitDetail') - .withArgs('6d51e2d7-1447-4242-b6af-aaa5525a2c6e', adUnit[0]) - .returns(adUnitDetailFixture); - - const bidRequests = getBids({ - bidderCode: 'rubicon', - auctionId: '6d51e2d7-1447-4242-b6af-aaa5525a2c6e', - bidderRequestId: '393a43193a0ac', - adUnits: adUnit, - labels: [], - src: 'client' - }); - expect(bidRequests[0]).to.not.be.undefined; - expect(bidRequests[0].mediaTypes.banner).to.not.be.undefined; - expect(bidRequests[0].mediaTypes.banner.sizes).to.deep.equal([[300, 200], [400, 600]]); + const actual = setupAdUnitMediaTypes(adUnits, [])[0]; + expect(actual).to.not.be.undefined; + const bids = bidderMap(actual); + expect(bids.rubicon.mediaTypes).to.be.undefined; + expect(bids.appnexus.mediaTypes).to.be.undefined; + expect(actual.mediaTypes.banner).to.not.be.undefined; + expect(actual.mediaTypes.banner.sizes).to.deep.equal([[300, 200], [400, 600]]); }); - it('should logInfo if bidder relevantMediaTypes contains media type that is not active at the ad unit level', function () { - internal.getAdUnitDetail.restore(); - - const adUnit = utils.deepClone(basic_AdUnit); - adUnit[0].mediaTypes = { + it('should remove bidder if its relevantMediaTypes contains media type that is not active at the ad unit level', function () { + adUnits[0].mediaTypes = { banner: { sizeConfig: [ { minViewPort: [0, 0], sizes: [] }, @@ -1725,60 +1396,22 @@ describe('sizeMappingV2', function () { } }; - adUnit[0].bids[1].sizeConfig = [ + adUnits[0].bids[1].sizeConfig = [ { minViewPort: [0, 0], relevantMediaTypes: ['none'] }, { minViewPort: [200, 0], relevantMediaTypes: ['banner'] } ]; - const adUnitDetailFixture = { - adUnitCode: 'adUnit1', - mediaTypes: { - banner: { - sizeConfig: [ - { minViewPort: [0, 0], sizes: [] }, - { minViewPort: [700, 0], sizes: [[300, 200], [400, 600]] } - ] - }, - native: { - sizeConfig: [ - { minViewPort: [0, 0], active: false }, - { minViewPort: [400, 0], active: true } - ] - } - }, - sizeBucketToSizeMap: { - banner: { - activeSizeBucket: [0, 0], - activeSizeDimensions: [[]] - }, - native: { - activeSizeBucket: [400, 0], - activeSizeDimensions: 'NA' - } - }, + adUnitDetail = { activeViewport: [560, 260], transformedMediaTypes: { native: {} }, isLabelActivated: true, - instance: 1, - cacheHits: 0 }; - sinon - .stub(internal, 'getAdUnitDetail') - .withArgs('6d51e2d7-1447-4242-b6af-aaa5525a2c6e', adUnit[0], []) - .returns(adUnitDetailFixture); - - const bidRequests = getBids({ - bidderCode: 'rubicon', - auctionId: '6d51e2d7-1447-4242-b6af-aaa5525a2c6e', - bidderRequestId: '393a43193a0ac', - adUnits: adUnit, - labels: [], - src: 'client' - }); - expect(bidRequests[0]).to.be.undefined; + const actual = setupAdUnitMediaTypes(adUnits, [])[0]; + const bids = bidderMap(actual); + expect(bids.rubicon).to.be.undefined; sinon.assert.callCount(utils.logInfo, 1); sinon.assert.calledWith(utils.logInfo, `Size Mapping V2:: Ad Unit: adUnit1(1), Bidder: rubicon => 'relevantMediaTypes' does not match with any of the active mediaTypes at the Ad Unit level. This bidder is disabled.`); }); @@ -1788,38 +1421,19 @@ describe('sizeMappingV2', function () { .stub(utils, 'isValidMediaTypes') .returns(false); - getBids({ - bidderCode: 'appnexus', - auctionId: '6d51e2d7-1447-4242-b6af-aaa5525a2c6e', - bidderRequestId: '393a43193a0ac', - adUnits: basic_AdUnit, - labels: [], - src: 'client' - }); + try { + setupAdUnitMediaTypes(adUnits, []); + } finally { + utils.isValidMediaTypes.restore(); + } + sinon.assert.callCount(utils.logWarn, 1); sinon.assert.calledWith(utils.logWarn, `Size Mapping V2:: Ad Unit: adUnit1 => Ad unit has declared invalid 'mediaTypes' or has not declared a 'mediaTypes' property`); - - utils.isValidMediaTypes.restore(); }); it('should log a message if ad unit is disabled due to a failing label check', function () { - internal.getAdUnitDetail.restore(); - const adUnitDetail = Object.assign({}, adUnitDetailFixture); adUnitDetail.isLabelActivated = false; - sinon - .stub(internal, 'getAdUnitDetail') - .withArgs('6d51e2d7-1447-4242-b6af-aaa5525a2c6e', basic_AdUnit[0], []) - .returns(adUnitDetail); - - getBids({ - bidderCode: 'appnexus', - auctionId: '6d51e2d7-1447-4242-b6af-aaa5525a2c6e', - bidderRequestId: '393a43193a0ac', - adUnits: basic_AdUnit, - labels: [], - src: 'client' - }); - + setupAdUnitMediaTypes(adUnits, []); sinon.assert.callCount(utils.logInfo, 1); sinon.assert.calledWith(utils.logInfo, `Size Mapping V2:: Ad Unit: adUnit1(1) => Ad unit is disabled due to failing label check.`); }); @@ -1827,19 +1441,25 @@ describe('sizeMappingV2', function () { it('should log a message if bidder is disabled due to a failing label check', function () { const stub = sinon.stub(internal, 'isLabelActivated').returns(false); - getBids({ - bidderCode: 'appnexus', - auctionId: '6d51e2d7-1447-4242-b6af-aaa5525a2c6e', - bidderRequestId: '393a43193a0ac', - adUnits: basic_AdUnit, - labels: [], - src: 'client' - }); + try { + setupAdUnitMediaTypes(adUnits, []); + } finally { + stub.restore(); + } - sinon.assert.callCount(utils.logInfo, 1); + sinon.assert.callCount(utils.logInfo, 2); // called once for each bidder sinon.assert.calledWith(utils.logInfo, `Size Mapping V2:: Ad Unit: adUnit1(1), Bidder: appnexus => Label check for this bidder has failed. This bidder is disabled.`); + }); - internal.isLabelActivated.restore(); - }) + it('should set adUnit.bids[].mediaTypes if the bid mediaTypes should differ from the adUnit', () => { + adUnits[0].mediaTypes.native = {}; + adUnits[0].bids[1].sizeConfig = [ + { minViewPort: [0, 0], relevantMediaTypes: ['banner'] } + ]; + adUnitDetail.transformedMediaTypes.native = {}; + const actual = setupAdUnitMediaTypes(adUnits, [])[0]; + const bids = bidderMap(actual); + expect(bids.rubicon.mediaTypes).to.deep.equal({banner: adUnitDetail.transformedMediaTypes.banner}); + }); }); }); diff --git a/test/spec/modules/smaatoBidAdapter_spec.js b/test/spec/modules/smaatoBidAdapter_spec.js index 801aef514fc..38df03652b1 100644 --- a/test/spec/modules/smaatoBidAdapter_spec.js +++ b/test/spec/modules/smaatoBidAdapter_spec.js @@ -127,6 +127,15 @@ describe('smaatoBidAdapterTest', () => { } }; + let sandbox; + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); + + afterEach(() => { + sandbox.restore(); + }) + it('auction type is 1 (first price auction)', () => { const reqs = spec.buildRequests([singleBannerBidRequest], defaultBidderRequest); @@ -294,6 +303,16 @@ describe('smaatoBidAdapterTest', () => { expect(req.source.ext.schain).to.not.exist; }); + it('sends instl if instl exists', () => { + const instl = { instl: 1 }; + const bidRequestWithInstl = Object.assign({}, singleBannerBidRequest, {ortb2Imp: instl}); + + const reqs = spec.buildRequests([bidRequestWithInstl], defaultBidderRequest); + + const req = extractPayloadOfFirstAndOnlyRequest(reqs); + expect(req.imp[0].instl).to.equal(1); + }); + it('sends tmax', () => { const reqs = spec.buildRequests([singleBannerBidRequest], defaultBidderRequest); @@ -309,12 +328,15 @@ describe('smaatoBidAdapterTest', () => { }); it('sends first party data', () => { - this.sandbox = sinon.sandbox.create() - this.sandbox.stub(config, 'getConfig').callsFake(key => { + sandbox.stub(config, 'getConfig').callsFake(key => { const config = { ortb2: { site: { - keywords: 'power tools,drills' + keywords: 'power tools,drills', + publisher: { + id: 'otherpublisherid', + name: 'publishername' + } }, user: { keywords: 'a,b', @@ -335,7 +357,6 @@ describe('smaatoBidAdapterTest', () => { expect(req.user.ext.consent).to.equal(CONSENT_STRING); expect(req.site.keywords).to.eql('power tools,drills'); expect(req.site.publisher.id).to.equal('publisherId'); - this.sandbox.restore(); }); it('has no user ids', () => { @@ -442,6 +463,16 @@ describe('smaatoBidAdapterTest', () => { expect(req.imp[0].bidfloor).to.be.equal(0.456); }); + it('sends instl if instl exists', () => { + const instl = { instl: 1 }; + const bidRequestWithInstl = Object.assign({}, singleVideoBidRequest, {ortb2Imp: instl}); + + const reqs = spec.buildRequests([bidRequestWithInstl], defaultBidderRequest); + + const req = extractPayloadOfFirstAndOnlyRequest(reqs); + expect(req.imp[0].instl).to.equal(1); + }); + it('splits multi format bid requests', () => { const combinedBannerAndVideoBidRequest = { bidder: 'smaato', @@ -523,6 +554,17 @@ describe('smaatoBidAdapterTest', () => { expect(req.imp[1].video.sequence).to.be.equal(2); }); + it('sends instl if instl exists', () => { + const instl = { instl: 1 }; + const bidRequestWithInstl = Object.assign({}, longFormVideoBidRequest, {ortb2Imp: instl}); + + const reqs = spec.buildRequests([bidRequestWithInstl], defaultBidderRequest); + + const req = extractPayloadOfFirstAndOnlyRequest(reqs); + expect(req.imp[0].instl).to.equal(1); + expect(req.imp[1].instl).to.equal(1); + }); + it('sends bidfloor when configured', () => { const longFormVideoBidRequestWithFloor = Object.assign({}, longFormVideoBidRequest); longFormVideoBidRequestWithFloor.getFloor = function(arg) { diff --git a/test/spec/modules/smarthubBidAdapter_spec.js b/test/spec/modules/smarthubBidAdapter_spec.js new file mode 100644 index 00000000000..05fb1424dca --- /dev/null +++ b/test/spec/modules/smarthubBidAdapter_spec.js @@ -0,0 +1,392 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/smarthubBidAdapter'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; + +const bidder = 'smarthub' + +describe('SmartHubBidAdapter', function () { + const bids = [ + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + partnerName: 'testname', + seat: 'testSeat', + token: 'testBanner', + iabCat: ['IAB1-1', 'IAB3-1', 'IAB4-3'], + minBidfloor: 10, + pos: 1, + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [VIDEO]: { + playerSize: [[300, 300]], + minduration: 5, + maxduration: 60, + } + }, + params: { + partnerName: 'testname', + seat: 'testSeat', + token: 'testVideo', + iabCat: ['IAB1-1', 'IAB3-1', 'IAB4-3'], + minBidfloor: 10, + pos: 1, + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [NATIVE]: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + } + }, + params: { + partnerName: 'testname', + seat: 'testSeat', + token: 'testToken', + iabCat: ['IAB1-1', 'IAB3-1', 'IAB4-3'], + minBidfloor: 10, + pos: 1, + } + } + ]; + + const invalidBid = { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + + } + } + + const bidderRequest = { + uspConsent: '1---', + gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + refererInfo: { + referer: 'https://test.com' + } + }; + + describe('isBidRequestValid', function () { + it('Should return true if there are bidId, params and key parameters present', function () { + expect(spec.isBidRequestValid(bids[0])).to.be.true; + }); + it('Should return false if at least one of parameters is not present', function () { + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let [serverRequest] = spec.buildRequests(bids, bidderRequest); + + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + + it('Returns valid URL', function () { + expect(serverRequest.url).to.equal('https://testname-prebid.smart-hub.io/pbjs'); + }); + + it('Returns general data valid', function () { + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.all.keys('deviceWidth', + 'deviceHeight', + 'language', + 'secure', + 'host', + 'page', + 'placements', + 'coppa', + 'ccpa', + 'gdpr', + 'tmax' + ); + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.language).to.be.a('string'); + expect(data.secure).to.be.within(0, 1); + expect(data.host).to.be.a('string'); + expect(data.page).to.be.a('string'); + expect(data.coppa).to.be.a('number'); + expect(data.gdpr).to.be.a('string'); + expect(data.ccpa).to.be.a('string'); + expect(data.tmax).to.be.a('number'); + expect(data.placements).to.have.lengthOf(3); + }); + + it('Returns valid placements', function () { + const { placements } = serverRequest.data; + for (let i = 0, len = placements.length; i < len; i++) { + const placement = placements[i]; + expect(placement.adFormat).to.be.oneOf([BANNER, VIDEO, NATIVE]); + expect(placement.bidId).to.be.a('string'); + expect(placement.partnerName).to.be.a('string'); + expect(placement.seat).to.be.a('string'); + expect(placement.token).to.be.a('string'); + expect(placement.iabCat).to.be.an('array'); + expect(placement.minBidfloor).to.be.a('number'); + expect(placement.pos).to.be.within(0, 7); + expect(placement.schain).to.be.an('object'); + expect(placement.bidfloor).to.exist.and.to.equal(0); + + if (placement.adFormat === BANNER) { + expect(placement.sizes).to.be.an('array'); + } + switch (placement.adFormat) { + case BANNER: + expect(placement.sizes).to.be.an('array'); + break; + case VIDEO: + expect(placement.playerSize).to.be.an('array'); + expect(placement.minduration).to.be.an('number'); + expect(placement.maxduration).to.be.an('number'); + break; + case NATIVE: + expect(placement.native).to.be.an('object'); + break; + } + } + }); + + it('Returns data with gdprConsent and without uspConsent', function () { + delete bidderRequest.uspConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest[0].data; + expect(data.gdpr).to.exist; + expect(data.gdpr).to.be.a('string'); + expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + expect(data.ccpa).to.not.exist; + delete bidderRequest.gdprConsent; + }); + + it('Returns data with uspConsent and without gdprConsent', function () { + bidderRequest.uspConsent = '1---'; + delete bidderRequest.gdprConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest[0].data; + expect(data.ccpa).to.exist; + expect(data.ccpa).to.be.a('string'); + expect(data.ccpa).to.equal(bidderRequest.uspConsent); + expect(data.gdpr).to.not.exist; + }); + + it('Returns empty data if no valid requests are passed', function () { + serverRequest = spec.buildRequests([], bidderRequest); + expect(serverRequest).to.be.an('array').that.is.empty; + }); + }); + + describe('interpretResponse', function () { + it('Should interpret banner response', function () { + const banner = { + body: [{ + mediaType: 'banner', + width: 300, + height: 250, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let dataItem = bannerResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta'); + expect(dataItem.requestId).to.equal(banner.body[0].requestId); + expect(dataItem.cpm).to.equal(banner.body[0].cpm); + expect(dataItem.width).to.equal(banner.body[0].width); + expect(dataItem.height).to.equal(banner.body[0].height); + expect(dataItem.ad).to.equal(banner.body[0].ad); + expect(dataItem.ttl).to.equal(banner.body[0].ttl); + expect(dataItem.creativeId).to.equal(banner.body[0].creativeId); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal(banner.body[0].currency); + expect(dataItem.meta).to.be.an('object').that.has.property('advertiserDomains'); + }); + it('Should interpret video response', function () { + const video = { + body: [{ + vastUrl: 'test.com', + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + width: 300, + height: 250, + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let videoResponses = spec.interpretResponse(video); + expect(videoResponses).to.be.an('array').that.is.not.empty; + + let dataItem = videoResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'vastUrl', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta', 'width', 'height'); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.5); + expect(dataItem.vastUrl).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.meta).to.be.an('object').that.has.property('advertiserDomains'); + }); + it('Should interpret native response', function () { + const native = { + body: [{ + mediaType: 'native', + native: { + clickUrl: 'test.com', + title: 'Test', + image: 'test.com', + impressionTrackers: ['test.com'], + }, + ttl: 120, + cpm: 0.4, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let nativeResponses = spec.interpretResponse(native); + expect(nativeResponses).to.be.an('array').that.is.not.empty; + + let dataItem = nativeResponses[0]; + expect(dataItem).to.have.keys('requestId', 'cpm', 'ttl', 'creativeId', 'netRevenue', 'currency', 'mediaType', 'native', 'meta'); + expect(dataItem.native).to.have.keys('clickUrl', 'impressionTrackers', 'title', 'image') + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.native.clickUrl).to.equal('test.com'); + expect(dataItem.native.title).to.equal('Test'); + expect(dataItem.native.image).to.equal('test.com'); + expect(dataItem.native.impressionTrackers).to.be.an('array').that.is.not.empty; + expect(dataItem.native.impressionTrackers[0]).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.meta).to.be.an('object').that.has.property('advertiserDomains'); + }); + it('Should return an empty array if invalid banner response is passed', function () { + const invBanner = { + body: [{ + width: 300, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + + let serverResponses = spec.interpretResponse(invBanner); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid video response is passed', function () { + const invVideo = { + body: [{ + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invVideo); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid native response is passed', function () { + const invNative = { + body: [{ + mediaType: 'native', + clickUrl: 'test.com', + title: 'Test', + impressionTrackers: ['test.com'], + ttl: 120, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }] + }; + let serverResponses = spec.interpretResponse(invNative); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid response is passed', function () { + const invalid = { + body: [{ + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invalid); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); +}); diff --git a/test/spec/modules/sortableAnalyticsAdapter_spec.js b/test/spec/modules/sortableAnalyticsAdapter_spec.js index 258c4b8f74d..9300756eae2 100644 --- a/test/spec/modules/sortableAnalyticsAdapter_spec.js +++ b/test/spec/modules/sortableAnalyticsAdapter_spec.js @@ -1,6 +1,6 @@ import {expect} from 'chai'; import sortableAnalyticsAdapter, {TIMEOUT_FOR_REGISTRY, DEFAULT_PBID_TIMEOUT} from 'modules/sortableAnalyticsAdapter.js'; -import events from 'src/events.js'; +import * as events from 'src/events.js'; import CONSTANTS from 'src/constants.json'; import * as prebidGlobal from 'src/prebidGlobal.js'; import {server} from 'test/mocks/xhr.js'; @@ -148,7 +148,6 @@ describe('Sortable Analytics Adapter', function() { } } }); - sortableAnalyticsAdapter.enableAnalytics(initialConfig); }); @@ -203,11 +202,11 @@ describe('Sortable Analytics Adapter', function() { brc: 1, brid: ['10141593b1d84a', '37a8760be6db23'], rs: ['300x250', '728x90'], - btcp: [0.70, 0.50], + btcp: [0.70, 0.50].map(n => n * 0.95), btcc: 'USD', btin: true, btsrc: 'sortable', - c: [0.70, 0.50], + c: [0.70, 0.50].map(n => n * 0.95), cc: 'USD', did: null, inr: true, diff --git a/test/spec/modules/sovrnBidAdapter_spec.js b/test/spec/modules/sovrnBidAdapter_spec.js index 729c48c28f4..09a61c82b6c 100644 --- a/test/spec/modules/sovrnBidAdapter_spec.js +++ b/test/spec/modules/sovrnBidAdapter_spec.js @@ -5,7 +5,7 @@ import * as utils from 'src/utils.js' const ENDPOINT = `https://ap.lijit.com/rtb/bid?src=$$REPO_AND_VERSION$$`; -const adUnitBidRequest = { +const baseBidRequest = { 'bidder': 'sovrn', 'params': { 'tagid': 403370 @@ -19,7 +19,7 @@ const adUnitBidRequest = { 'bidderRequestId': '22edbae2733bf6', 'auctionId': '1d1a030790a475', } -const bidderRequest = { +const baseBidderRequest = { refererInfo: { referer: 'http://example.com/page.html', } @@ -28,28 +28,35 @@ const bidderRequest = { describe('sovrnBidAdapter', function() { describe('isBidRequestValid', function () { it('should return true when required params found', function () { - expect(spec.isBidRequestValid(adUnitBidRequest)).to.equal(true); + expect(spec.isBidRequestValid(baseBidRequest)).to.equal(true); }); it('should return false when tagid not passed correctly', function () { - const bid = {...adUnitBidRequest} - const params = adUnitBidRequest.params - bid.params = {...params} - bid.params.tagid = 'ABCD' - expect(spec.isBidRequestValid(bid)).to.equal(false) + const bidRequest = { + ...baseBidRequest, + 'params': { + ...baseBidRequest.params, + 'tagid': 'ABCD' + }, + }; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); }); it('should return false when require params are not passed', function () { - const bid = {...adUnitBidRequest} - bid.params = {}; - expect(spec.isBidRequestValid(bid)).to.equal(false); + const bidRequest = { + ...baseBidRequest, + 'params': {} + } + + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); }); }); describe('buildRequests', function () { describe('basic bid parameters', function() { - const bidRequests = [adUnitBidRequest]; - const request = spec.buildRequests(bidRequests, bidderRequest); + const request = spec.buildRequests([baseBidRequest], baseBidderRequest); + const payload = JSON.parse(request.data); it('sends bid request to our endpoint via POST', function () { expect(request.method).to.equal('POST'); @@ -60,15 +67,65 @@ describe('sovrnBidAdapter', function() { }); it('sets the proper banner object', function() { + const bannerBidRequest = { + ...baseBidRequest, + 'mediaTypes': { + banner: {} + } + } + const request = spec.buildRequests([bannerBidRequest], baseBidderRequest) + + const payload = JSON.parse(request.data) + const impression = payload.imp[0] + + expect(impression.banner.format).to.deep.equal([{w: 300, h: 250}, {w: 300, h: 600}]) + expect(impression.banner.w).to.equal(1) + expect(impression.banner.h).to.equal(1) + }) + + it('sets the proper video object', function() { + const width = 640 + const height = 480 + const mimes = ['video/mp4', 'application/javascript'] + const protocols = [2, 5] + const minduration = 5 + const maxduration = 60 + const startdelay = 0 + const videoBidRequest = { + ...baseBidRequest, + 'mediaTypes': { + video: { + mimes, + protocols, + playerSize: [[width, height], [360, 240]], + minduration, + maxduration, + startdelay + } + } + } + const request = spec.buildRequests([videoBidRequest], baseBidderRequest) + const payload = JSON.parse(request.data) - expect(payload.imp[0].banner.format).to.deep.equal([{w: 300, h: 250}, {w: 300, h: 600}]) - expect(payload.imp[0].banner.w).to.equal(1) - expect(payload.imp[0].banner.h).to.equal(1) + const impression = payload.imp[0] + + expect(impression.video.w).to.equal(width) + expect(impression.video.h).to.equal(height) + expect(impression.video.mimes).to.have.same.members(mimes) + expect(impression.video.protocols).to.have.same.members(protocols) + expect(impression.video.minduration).to.equal(minduration) + expect(impression.video.maxduration).to.equal(maxduration) + expect(impression.video.startdelay).to.equal(startdelay) }) - it('includes the ad unit code int the request', function() { - const payload = JSON.parse(request.data); - expect(payload.imp[0].adunitcode).to.equal('adunit-code') + it('gets correct site info', function() { + expect(payload.site.page).to.equal('http://example.com/page.html'); + expect(payload.site.domain).to.equal('example.com'); + }); + + it('includes the ad unit code in the request', function() { + const impression = payload.imp[0] + expect(impression.adunitcode).to.equal('adunit-code') }) it('converts tagid to string', function () { @@ -77,109 +134,79 @@ describe('sovrnBidAdapter', function() { }) it('accepts a single array as a size', function() { - const singleSize = [{ - 'bidder': 'sovrn', + const singleSizeBidRequest = { + ...baseBidRequest, 'params': { - 'tagid': '403370', 'iv': 'vet' }, - 'adUnitCode': 'adunit-code', 'sizes': [300, 250], - 'bidId': '30b31c1838de1e', - 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475' - }] - const request = spec.buildRequests(singleSize, bidderRequest) + 'mediaTypes': { + banner: {} + }, + } + const request = spec.buildRequests([singleSizeBidRequest], baseBidderRequest) + const payload = JSON.parse(request.data) - expect(payload.imp[0].banner.format).to.deep.equal([{w: 300, h: 250}]) - expect(payload.imp[0].banner.w).to.equal(1) - expect(payload.imp[0].banner.h).to.equal(1) + const impression = payload.imp[0] + + expect(impression.banner.format).to.deep.equal([{w: 300, h: 250}]) + expect(impression.banner.w).to.equal(1) + expect(impression.banner.h).to.equal(1) }) it('sends \'iv\' as query param if present', function () { - const ivBidRequests = [{ - 'bidder': 'sovrn', - 'params': { - 'tagid': '403370', - 'iv': 'vet' - }, - 'adUnitCode': 'adunit-code', - 'sizes': [ - [300, 250], - [300, 600] - ], - 'bidId': '30b31c1838de1e', - 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475' - }]; - const bidderRequest = { - refererInfo: { - referer: 'http://example.com/page.html', + const ivBidRequest = { + ...baseBidRequest, + params: { + iv: 'vet' } - }; - const request = spec.buildRequests(ivBidRequests, bidderRequest); + } + const request = spec.buildRequests([ivBidRequest], baseBidderRequest) expect(request.url).to.contain('iv=vet') }); it('sends gdpr info if exists', function () { - let consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; const bidderRequest = { + ...baseBidderRequest, 'bidderCode': 'sovrn', 'auctionId': '1d1a030790a475', 'bidderRequestId': '22edbae2733bf6', 'timeout': 3000, - gdprConsent: { - consentString: consentString, - gdprApplies: true + 'gdprConsent': { + 'consentString': 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A==', + 'gdprApplies': true }, - refererInfo: { - referer: 'http://example.com/page.html', - } + 'bids': [baseBidRequest] }; - bidderRequest.bids = [adUnitBidRequest]; - const data = JSON.parse(spec.buildRequests([adUnitBidRequest], bidderRequest).data); + const { regs, user } = JSON.parse(spec.buildRequests([baseBidRequest], bidderRequest).data) - expect(data.regs.ext.gdpr).to.exist.and.to.be.a('number'); - expect(data.regs.ext.gdpr).to.equal(1); - expect(data.user.ext.consent).to.exist.and.to.be.a('string'); - expect(data.user.ext.consent).to.equal(consentString); - }); + expect(regs.ext.gdpr).to.exist.and.to.be.a('number') + expect(regs.ext.gdpr).to.equal(1) + expect(user.ext.consent).to.exist.and.to.be.a('string') + expect(user.ext.consent).to.equal(bidderRequest.gdprConsent.consentString) + }) it('should send us_privacy if bidderRequest has a value for uspConsent', function () { - const uspString = '1NYN'; const bidderRequest = { + ...baseBidderRequest, 'bidderCode': 'sovrn', 'auctionId': '1d1a030790a475', 'bidderRequestId': '22edbae2733bf6', 'timeout': 3000, - uspConsent: uspString, - refererInfo: { - referer: 'http://example.com/page.html', - } - }; - bidderRequest.bids = [adUnitBidRequest]; + 'uspConsent': '1NYN', + 'bids': [baseBidRequest] + } - const data = JSON.parse(spec.buildRequests([adUnitBidRequest], bidderRequest).data); + const data = JSON.parse(spec.buildRequests([baseBidRequest], bidderRequest).data) - expect(data.regs.ext['us_privacy']).to.equal(uspString); - }); + expect(data.regs.ext['us_privacy']).to.equal(bidderRequest.uspConsent) + }) it('should add schain if present', function() { - const schainRequests = [{ - 'bidder': 'sovrn', - 'params': { - 'tagid': 403370 - }, - 'adUnitCode': 'adunit-code', - 'sizes': [ - [300, 250], - [300, 600] - ], - 'bidId': '30b31c1838de1e', - 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475', + const schainRequest = { + ...baseBidRequest, 'schain': { 'ver': '1.0', 'complete': 1, @@ -192,100 +219,89 @@ describe('sovrnBidAdapter', function() { } ] } - }].concat(adUnitBidRequest); - const bidderRequest = { - refererInfo: { - referer: 'http://example.com/page.html', - } - }; - const data = JSON.parse(spec.buildRequests(schainRequests, bidderRequest).data); + } + const schainRequests = [schainRequest, baseBidRequest] + + const data = JSON.parse(spec.buildRequests(schainRequests, baseBidderRequest).data) expect(data.source.ext.schain.nodes.length).to.equal(1) - }); + }) - it('should add ids to the bid request', function() { - const criteoIdRequest = [{ - 'bidder': 'sovrn', - 'params': { - 'tagid': 403370 - }, - 'adUnitCode': 'adunit-code', - 'sizes': [ - [300, 250], - [300, 600] - ], - 'bidId': '30b31c1838de1e', - 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475', - 'userId': { - 'criteoId': 'A_CRITEO_ID', - 'tdid': 'SOMESORTOFID', - } - }].concat(adUnitBidRequest); - const bidderRequest = { - refererInfo: { - referer: 'http://example.com/page.html', + it('should add eds to the bid request', function() { + const criteoIdRequest = { + ...baseBidRequest, + userId: { + criteoId: 'A_CRITEO_ID', + tdid: 'SOMESORTOFID', } - }; - - const data = JSON.parse(spec.buildRequests(criteoIdRequest, bidderRequest).data); - expect(data.user.ext.eids[0].source).to.equal('criteo.com') - expect(data.user.ext.eids[0].uids[0].id).to.equal('A_CRITEO_ID') - expect(data.user.ext.eids[0].uids[0].atype).to.equal(1) - expect(data.user.ext.eids[1].source).to.equal('adserver.org') - expect(data.user.ext.eids[1].uids[0].id).to.equal('SOMESORTOFID') - expect(data.user.ext.eids[1].uids[0].ext.rtiPartner).to.equal('TDID') - expect(data.user.ext.eids[1].uids[0].atype).to.equal(1) - expect(data.user.ext.tpid[0].source).to.equal('criteo.com') - expect(data.user.ext.tpid[0].uid).to.equal('A_CRITEO_ID') - expect(data.user.ext.prebid_criteoid).to.equal('A_CRITEO_ID') - }); + } + const criteoIdRequests = [criteoIdRequest, baseBidRequest] + + const ext = JSON.parse(spec.buildRequests(criteoIdRequests, baseBidderRequest).data).user.ext + const firstEID = ext.eids[0] + const secondEID = ext.eids[1] + + expect(firstEID.source).to.equal('criteo.com') + expect(firstEID.uids[0].id).to.equal('A_CRITEO_ID') + expect(firstEID.uids[0].atype).to.equal(1) + expect(secondEID.source).to.equal('adserver.org') + expect(secondEID.uids[0].id).to.equal('SOMESORTOFID') + expect(secondEID.uids[0].ext.rtiPartner).to.equal('TDID') + expect(secondEID.uids[0].atype).to.equal(1) + expect(ext.tpid[0].source).to.equal('criteo.com') + expect(ext.tpid[0].uid).to.equal('A_CRITEO_ID') + expect(ext.prebid_criteoid).to.equal('A_CRITEO_ID') + }) it('should ignore empty segments', function() { - const request = spec.buildRequests([adUnitBidRequest], bidderRequest) + const request = spec.buildRequests([baseBidRequest], baseBidderRequest) const payload = JSON.parse(request.data) + expect(payload.imp[0].ext).to.be.undefined }) it('should pass the segments param value as trimmed deal ids array', function() { - const segmentsRequests = [{ - 'bidder': 'sovrn', - 'params': { - 'segments': ' test1,test2 ' - }, - 'adUnitCode': 'adunit-code', - 'sizes': [ - [300, 250], - [300, 600] - ], - 'bidId': '30b31c1838de1e', - 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475' - }] - const request = spec.buildRequests(segmentsRequests, bidderRequest) - const payload = JSON.parse(request.data) - expect(payload.imp[0].ext.deals[0]).to.equal('test1') - expect(payload.imp[0].ext.deals[1]).to.equal('test2') + const segmentsRequest = { + ...baseBidRequest, + params: { + segments: ' test1,test2 ' + } + } + const request = spec.buildRequests([segmentsRequest], baseBidderRequest) + const deals = JSON.parse(request.data).imp[0].ext.deals + + expect(deals[0]).to.equal('test1') + expect(deals[1]).to.equal('test2') }) it('should use the floor provided from the floor module if present', function() { - const floorBid = {...adUnitBidRequest, getFloor: () => ({currency: 'USD', floor: 1.10})} - floorBid.params = { - tagid: 1234, - bidfloor: 2.00 + const floorBid = { + ...baseBidRequest, + getFloor: () => ({currency: 'USD', floor: 1.10}), + params: { + tagid: 1234, + bidfloor: 2.00 + } } - const request = spec.buildRequests([floorBid], bidderRequest) + + const request = spec.buildRequests([floorBid], baseBidderRequest) const payload = JSON.parse(request.data) + expect(payload.imp[0].bidfloor).to.equal(1.10) }) it('should use the floor from the param if there is no floor from the floor module', function() { - const floorBid = {...adUnitBidRequest, getFloor: () => ({})} + const floorBid = { + ...baseBidRequest, + getFloor: () => ({}) + } floorBid.params = { tagid: 1234, bidfloor: 2.00 } - const request = spec.buildRequests([floorBid], bidderRequest) - const payload = JSON.parse(request.data) - expect(payload.imp[0].bidfloor).to.equal(2.00) + + const request = spec.buildRequests([floorBid], baseBidderRequest) + const impression = JSON.parse(request.data).imp[0] + + expect(impression.bidfloor).to.equal(2.00) }) describe('First Party Data', function () { let sandbox @@ -307,54 +323,79 @@ describe('sovrnBidAdapter', function() { data: 'some user data' } } - }; - return utils.deepAccess(cfg, key); - }); - const request = spec.buildRequests([adUnitBidRequest], bidderRequest) - const payload = JSON.parse(request.data) - expect(payload.user.data).to.equal('some user data') - expect(payload.site.keywords).to.equal('test keyword') - expect(payload.site.page).to.equal('http://example.com/page.html') - expect(payload.site.domain).to.equal('example.com') + } + return utils.deepAccess(cfg, key) + }) + + const request = spec.buildRequests([baseBidRequest], baseBidderRequest) + const { user, site } = JSON.parse(request.data) + + expect(user.data).to.equal('some user data') + expect(site.keywords).to.equal('test keyword') + expect(site.page).to.equal('http://example.com/page.html') + expect(site.domain).to.equal('example.com') }) it('should append impression first party data', function () { - const fpdBid = {...adUnitBidRequest} - fpdBid.ortb2Imp = { - ext: { - data: { - pbadslot: 'homepage-top-rect', - adUnitSpecificAttribute: '123' + const fpdBidRequest = { + ...baseBidRequest, + ortb2Imp: { + ext: { + data: { + pbadslot: 'homepage-top-rect', + adUnitSpecificAttribute: '123' + } } } } - const request = spec.buildRequests([fpdBid], bidderRequest) + + const request = spec.buildRequests([fpdBidRequest], baseBidderRequest) const payload = JSON.parse(request.data) + expect(payload.imp[0].ext.data.pbadslot).to.equal('homepage-top-rect') expect(payload.imp[0].ext.data.adUnitSpecificAttribute).to.equal('123') }) it('should not overwrite deals when impression fpd is present', function() { - const fpdBid = {...adUnitBidRequest} - fpdBid.params = {...adUnitBidRequest.params} - fpdBid.params.segments = 'seg1, seg2' - fpdBid.ortb2Imp = { - ext: { - data: { - pbadslot: 'homepage-top-rect', - adUnitSpecificAttribute: '123' + const fpdBid = { + ...baseBidRequest, + params: { + segments: 'seg1, seg2' + }, + ortb2Imp: { + ext: { + data: { + pbadslot: 'homepage-top-rect', + adUnitSpecificAttribute: '123' + } } } } - const request = spec.buildRequests([fpdBid], bidderRequest) - const payload = JSON.parse(request.data) - expect(payload.imp[0].ext.data.pbadslot).to.equal('homepage-top-rect') - expect(payload.imp[0].ext.data.adUnitSpecificAttribute).to.equal('123') - expect(payload.imp[0].ext.deals).to.deep.equal(['seg1', 'seg2']) + + const request = spec.buildRequests([fpdBid], baseBidderRequest) + const impression = JSON.parse(request.data).imp[0] + + expect(impression.ext.data.pbadslot).to.equal('homepage-top-rect') + expect(impression.ext.data.adUnitSpecificAttribute).to.equal('123') + expect(impression.ext.deals).to.deep.equal(['seg1', 'seg2']) }) }) }); describe('interpretResponse', function () { let response; + const baseResponse = { + 'requestId': '263c448586f5a1', + 'cpm': 0.45882675, + 'width': 728, + 'height': 90, + 'creativeId': 'creativelycreatedcreativecreative', + 'dealId': null, + 'currency': 'USD', + 'netRevenue': true, + 'mediaType': 'banner', + 'ad': decodeURIComponent(``), + 'ttl': 90, + 'meta': { advertiserDomains: [] } + } beforeEach(function () { response = { body: { @@ -376,106 +417,175 @@ describe('sovrnBidAdapter', function() { }); it('should get the correct bid response', function () { - let expectedResponse = [{ - 'requestId': '263c448586f5a1', - 'cpm': 0.45882675, - 'width': 728, - 'height': 90, - 'creativeId': 'creativelycreatedcreativecreative', - 'dealId': null, - 'currency': 'USD', - 'netRevenue': true, - 'mediaType': 'banner', + const expectedResponse = { + ...baseResponse, 'ad': decodeURIComponent(`>`), 'ttl': 60000, - 'meta': { advertiserDomains: [] } - }]; + }; + + const result = spec.interpretResponse(response); - let result = spec.interpretResponse(response); - expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedResponse[0])); + expect(result[0]).to.have.deep.keys(expectedResponse) }); it('crid should default to the bid id if not on the response', function () { delete response.body.seatbid[0].bid[0].crid; - let expectedResponse = [{ - 'requestId': '263c448586f5a1', - 'cpm': 0.45882675, - 'width': 728, - 'height': 90, + + const expectedResponse = { + ...baseResponse, 'creativeId': response.body.seatbid[0].bid[0].id, - 'dealId': null, - 'currency': 'USD', - 'netRevenue': true, - 'mediaType': 'banner', 'ad': decodeURIComponent(``), - 'ttl': 90, - 'meta': { advertiserDomains: [] } - }]; + } - let result = spec.interpretResponse(response); - expect(result[0]).to.deep.equal(expectedResponse[0]); + const result = spec.interpretResponse(response); + + expect(result[0]).to.deep.equal(expectedResponse); }); it('should get correct bid response when dealId is passed', function () { response.body.seatbid[0].bid[0].dealid = 'baking'; - - let expectedResponse = [{ - 'requestId': '263c448586f5a1', - 'cpm': 0.45882675, - 'width': 728, - 'height': 90, - 'creativeId': 'creativelycreatedcreativecreative', + const expectedResponse = { + ...baseResponse, 'dealId': 'baking', - 'currency': 'USD', - 'netRevenue': true, - 'mediaType': 'banner', - 'ad': decodeURIComponent(``), - 'ttl': 90, - 'meta': { advertiserDomains: [] } - }]; + } + + const result = spec.interpretResponse(response) - let result = spec.interpretResponse(response); - expect(result[0]).to.deep.equal(expectedResponse[0]); + expect(result[0]).to.deep.equal(expectedResponse); }); it('should get correct bid response when ttl is set', function () { - response.body.seatbid[0].bid[0].ext = { 'ttl': 480 }; - - let expectedResponse = [{ - 'requestId': '263c448586f5a1', - 'cpm': 0.45882675, - 'width': 728, - 'height': 90, - 'creativeId': 'creativelycreatedcreativecreative', - 'dealId': null, - 'currency': 'USD', - 'netRevenue': true, - 'mediaType': 'banner', - 'ad': decodeURIComponent(``), + response.body.seatbid[0].bid[0].ext = { 'ttl': 480 } + + const expectedResponse = { + ...baseResponse, 'ttl': 480, - 'meta': { advertiserDomains: [] } - }]; + } + + const result = spec.interpretResponse(response) + + expect(result[0]).to.deep.equal(expectedResponse) + }) - let result = spec.interpretResponse(response); - expect(result[0]).to.deep.equal(expectedResponse[0]); + it('handles empty bid response', function () { + const response = { + body: { + 'id': '37386aade21a71', + 'seatbid': [] + } + }; + + const result = spec.interpretResponse(response) + + expect(result.length).to.equal(0); + }); + }); + + describe('interpretResponse video', function () { + let videoResponse; + const bidAdm = 'key%3Dvalue'; + const decodedBidAdm = decodeURIComponent(bidAdm); + const baseVideoResponse = { + 'requestId': '263c448586f5a1', + 'cpm': 0.45882675, + 'width': 640, + 'height': 480, + 'creativeId': 'creativelycreatedcreativecreative', + 'dealId': null, + 'currency': 'USD', + 'netRevenue': true, + 'mediaType': 'video', + 'ttl': 90, + 'meta': { advertiserDomains: [] }, + 'vastXml': decodedBidAdm + } + beforeEach(function () { + videoResponse = { + body: { + 'id': '37386aade21a71', + 'seatbid': [{ + 'bid': [{ + 'id': 'a_403370_332fdb9b064040ddbec05891bd13ab28', + 'crid': 'creativelycreatedcreativecreative', + 'impid': '263c448586f5a1', + 'price': 0.45882675, + 'nurl': '', + 'adm': bidAdm, + 'h': 480, + 'w': 640 + }] + }] + } + }; }); + it('should get the correct bid response', function () { + const expectedResponse = { + ...baseVideoResponse, + 'ttl': 60000, + }; + + const result = spec.interpretResponse(videoResponse); + + expect(result[0]).to.have.deep.keys(expectedResponse) + }); + + it('crid should default to the bid id if not on the response', function () { + delete videoResponse.body.seatbid[0].bid[0].crid; + + const expectedResponse = { + ...baseVideoResponse, + 'creativeId': videoResponse.body.seatbid[0].bid[0].id, + } + + const result = spec.interpretResponse(videoResponse); + + expect(result[0]).to.deep.equal(expectedResponse); + }); + + it('should get correct bid response when dealId is passed', function () { + videoResponse.body.seatbid[0].bid[0].dealid = 'baking'; + const expectedResponse = { + ...baseVideoResponse, + 'dealId': 'baking', + } + + const result = spec.interpretResponse(videoResponse) + + expect(result[0]).to.deep.equal(expectedResponse); + }); + + it('should get correct bid response when ttl is set', function () { + videoResponse.body.seatbid[0].bid[0].ext = { 'ttl': 480 } + + const expectedResponse = { + ...baseVideoResponse, + 'ttl': 480, + } + + const result = spec.interpretResponse(videoResponse) + + expect(result[0]).to.deep.equal(expectedResponse) + }) + it('handles empty bid response', function () { - let response = { + const response = { body: { 'id': '37386aade21a71', 'seatbid': [] } }; - let result = spec.interpretResponse(response); + + const result = spec.interpretResponse(response) + expect(result.length).to.equal(0); }); }); describe('getUserSyncs ', function() { - let syncOptions = { iframeEnabled: true, pixelEnabled: false }; - let iframeDisabledSyncOptions = { iframeEnabled: false, pixelEnabled: false }; - let serverResponse = [ + const syncOptions = { iframeEnabled: true, pixelEnabled: false }; + const iframeDisabledSyncOptions = { iframeEnabled: false, pixelEnabled: false }; + const serverResponse = [ { 'body': { 'id': '546956d68c757f', @@ -524,14 +634,14 @@ describe('sovrnBidAdapter', function() { ]; it('should return if iid present on server response & iframe syncs enabled', function() { - const expectedReturnStatement = [ - { - 'type': 'iframe', - 'url': 'https://ap.lijit.com/beacon?informer=13487408', - } - ]; + const expectedReturnStatement = { + 'type': 'iframe', + 'url': 'https://ap.lijit.com/beacon?informer=13487408', + } + const returnStatement = spec.getUserSyncs(syncOptions, serverResponse); - expect(returnStatement[0]).to.deep.equal(expectedReturnStatement[0]); + + expect(returnStatement[0]).to.deep.equal(expectedReturnStatement); }); it('should include gdpr consent string if present', function() { @@ -539,26 +649,26 @@ describe('sovrnBidAdapter', function() { gdprApplies: 1, consentString: 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A==' } - const expectedReturnStatement = [ - { - 'type': 'iframe', - 'url': `https://ap.lijit.com/beacon?gdpr_consent=${gdprConsent.consentString}&informer=13487408`, - } - ]; + const expectedReturnStatement = { + 'type': 'iframe', + 'url': `https://ap.lijit.com/beacon?gdpr_consent=${gdprConsent.consentString}&informer=13487408`, + } + const returnStatement = spec.getUserSyncs(syncOptions, serverResponse, gdprConsent, ''); - expect(returnStatement[0]).to.deep.equal(expectedReturnStatement[0]); + + expect(returnStatement[0]).to.deep.equal(expectedReturnStatement); }); it('should include us privacy string if present', function() { const uspString = '1NYN'; - const expectedReturnStatement = [ - { - 'type': 'iframe', - 'url': `https://ap.lijit.com/beacon?us_privacy=${uspString}&informer=13487408`, - } - ]; + const expectedReturnStatement = { + 'type': 'iframe', + 'url': `https://ap.lijit.com/beacon?us_privacy=${uspString}&informer=13487408`, + } + const returnStatement = spec.getUserSyncs(syncOptions, serverResponse, null, uspString); - expect(returnStatement[0]).to.deep.equal(expectedReturnStatement[0]); + + expect(returnStatement[0]).to.deep.equal(expectedReturnStatement); }); it('should include all privacy strings if present', function() { @@ -567,57 +677,35 @@ describe('sovrnBidAdapter', function() { consentString: 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A==' } const uspString = '1NYN'; - const expectedReturnStatement = [ - { - 'type': 'iframe', - 'url': `https://ap.lijit.com/beacon?gdpr_consent=${gdprConsent.consentString}&us_privacy=${uspString}&informer=13487408`, - } - ]; - const returnStatement = spec.getUserSyncs(syncOptions, serverResponse, gdprConsent, uspString); - expect(returnStatement[0]).to.deep.equal(expectedReturnStatement[0]); + const expectedReturnStatement = { + 'type': 'iframe', + 'url': `https://ap.lijit.com/beacon?gdpr_consent=${gdprConsent.consentString}&us_privacy=${uspString}&informer=13487408`, + } + + const returnStatement = spec.getUserSyncs(syncOptions, serverResponse, gdprConsent, uspString) + + expect(returnStatement[0]).to.deep.equal(expectedReturnStatement) }); it('should not return if iid missing on server response', function() { const returnStatement = spec.getUserSyncs(syncOptions, []); + expect(returnStatement).to.be.empty; }); it('should not return if iframe syncs disabled', function() { const returnStatement = spec.getUserSyncs(iframeDisabledSyncOptions, serverResponse); + expect(returnStatement).to.be.empty; }); it('should include pixel syncs', function() { - let pixelEnabledOptions = { iframeEnabled: false, pixelEnabled: true }; - const resp2 = { + const pixelEnabledOptions = { iframeEnabled: false, pixelEnabled: true } + + const otherResponce = { + ...serverResponse, 'body': { - 'id': '546956d68c757f-2', - 'seatbid': [ - { - 'bid': [ - { - 'id': 'a_448326_16c2ada014224bee815a90d2248322f5-2', - 'impid': '2a3826aae345f4', - 'price': 1.0099999904632568, - 'nurl': 'http://localhost/rtb/impression?bannerid=220958&campaignid=3890&rtb_tid=15588614-75d2-40ab-b27e-13d2127b3c2e&rpid=1295&seatid=seat1&zoneid=448326&cb=26900712&tid=a_448326_16c2ada014224bee815a90d2248322f5', - 'adm': 'yo a creative', - 'crid': 'cridprebidrtb', - 'w': 160, - 'h': 600 - }, - { - 'id': 'a_430392_beac4c1515da4576acf6cb9c5340b40c-2', - 'impid': '3cf96fd26ed4c5', - 'price': 1.0099999904632568, - 'nurl': 'http://localhost/rtb/impression?bannerid=220957&campaignid=3890&rtb_tid=5bc0e68b-3492-448d-a6f9-26fa3fd0b646&rpid=1295&seatid=seat1&zoneid=430392&cb=62735099&tid=a_430392_beac4c1515da4576acf6cb9c5340b40c', - 'adm': 'yo a creative', - 'crid': 'cridprebidrtb', - 'w': 300, - 'h': 250 - }, - ] - } - ], + ...serverResponse.body, 'ext': { 'iid': 13487408, sync: { @@ -631,10 +719,11 @@ describe('sovrnBidAdapter', function() { ] } } - }, - 'headers': {} + } } - const returnStatement = spec.getUserSyncs(pixelEnabledOptions, [...serverResponse, resp2]); + + const returnStatement = spec.getUserSyncs(pixelEnabledOptions, [...serverResponse, otherResponce]) + expect(returnStatement.length).to.equal(4); expect(returnStatement).to.deep.include.members([ { type: 'image', url: 'http://idprovider1.com' }, @@ -642,34 +731,25 @@ describe('sovrnBidAdapter', function() { { type: 'image', url: 'http://idprovider3.com' }, { type: 'image', url: 'http://idprovider4.com' } ]); - }); - }); + }) + }) describe('prebid 3 upgrade', function() { - const bidRequests = [{ - 'bidder': 'sovrn', + const bidRequest = { + ...baseBidRequest, 'params': { 'tagid': '403370' }, - 'adUnitCode': 'adunit-code', - mediaTypes: { - banner: { - sizes: [ + 'mediaTypes': { + 'banner': { + 'sizes': [ [300, 250], [300, 600] ] } }, - 'bidId': '30b31c1838de1e', - 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475' - }]; - const bidderRequest = { - refererInfo: { - referer: 'http://example.com/page.html', - } }; - const request = spec.buildRequests(bidRequests, bidderRequest); + const request = spec.buildRequests([bidRequest], baseBidderRequest); const payload = JSON.parse(request.data); it('gets sizes from mediaTypes.banner', function() { diff --git a/test/spec/modules/sspBCBidAdapter_spec.js b/test/spec/modules/sspBCBidAdapter_spec.js index d9f3ca84a3a..a0c4837bb51 100644 --- a/test/spec/modules/sspBCBidAdapter_spec.js +++ b/test/spec/modules/sspBCBidAdapter_spec.js @@ -514,8 +514,8 @@ describe('SSPBC adapter', function () { }); it('should send gdpr data', function () { - expect(payload.regs).to.be.an('object').and.to.have.property('[ortb_extensions.gdpr]', 1); - expect(payload.user).to.be.an('object').and.to.have.property('[ortb_extensions.consent]', bidRequest.gdprConsent.consentString); + expect(payload.regs).to.be.an('object').and.to.have.property('gdpr', 1); + expect(payload.user).to.be.an('object').and.to.have.property('consent', bidRequest.gdprConsent.consentString); }); it('should send net info and pvid', function () { @@ -653,7 +653,7 @@ describe('SSPBC adapter', function () { let nativeBid = resultNative[0]; expect(nativeBid).to.have.keys('bidderCode', 'cpm', 'creativeId', 'currency', 'width', 'height', 'meta', 'mediaType', 'netRevenue', 'requestId', 'ttl', 'native'); - expect(nativeBid.native).to.have.keys('image', 'icon', 'title', 'sponsoredBy', 'body', 'clickUrl', 'impressionTrackers'); + expect(nativeBid.native).to.have.keys('image', 'icon', 'title', 'sponsoredBy', 'body', 'clickUrl', 'impressionTrackers', 'javascriptTrackers'); }); }); @@ -668,8 +668,8 @@ describe('SSPBC adapter', function () { }); it('should send no syncs, if frame sync is not allowed', function () { - expect(syncResultImage).to.be.undefined; - expect(syncResultNone).to.be.undefined; + expect(syncResultImage).to.have.length(0); ; + expect(syncResultNone).to.have.length(0); ; }); }); @@ -686,7 +686,7 @@ describe('SSPBC adapter', function () { let notificationPayload = spec.onBidWon(bid); expect(notificationPayload).to.have.property('event').that.equals('bidWon'); expect(notificationPayload).to.have.property('requestId').that.equals(bid.auctionId); - expect(notificationPayload).to.have.property('adUnit').that.deep.equals([bid.adUnitCode]); + expect(notificationPayload).to.have.property('tagid').that.deep.equals([bid.adUnitCode]); expect(notificationPayload).to.have.property('siteId').that.is.an('array'); expect(notificationPayload).to.have.property('slotId').that.is.an('array'); }); @@ -707,7 +707,7 @@ describe('SSPBC adapter', function () { expect(notificationPayload).to.have.property('event').that.equals('timeout'); expect(notificationPayload).to.have.property('requestId').that.equals(bids_timeouted[0].auctionId); - expect(notificationPayload).to.have.property('adUnit').that.deep.equals([bids_timeouted[0].adUnitCode, bids_timeouted[1].adUnitCode]); + expect(notificationPayload).to.have.property('tagid').that.deep.equals([bids_timeouted[0].adUnitCode, bids_timeouted[1].adUnitCode]); }); }); }); diff --git a/test/spec/modules/stroeerCoreBidAdapter_spec.js b/test/spec/modules/stroeerCoreBidAdapter_spec.js index 6f24da85cfe..e723523de31 100644 --- a/test/spec/modules/stroeerCoreBidAdapter_spec.js +++ b/test/spec/modules/stroeerCoreBidAdapter_spec.js @@ -2,7 +2,7 @@ import {assert} from 'chai'; import {spec} from 'modules/stroeerCoreBidAdapter.js'; import * as utils from 'src/utils.js'; import {BANNER, VIDEO} from '../../../src/mediaTypes.js'; -import find from 'core-js-pure/features/array/find.js'; +import {find} from 'src/polyfill.js'; describe('stroeerCore bid adapter', function () { let sandbox; diff --git a/test/spec/modules/synacormediaBidAdapter_spec.js b/test/spec/modules/synacormediaBidAdapter_spec.js index 5f3633ec311..b9a02799219 100644 --- a/test/spec/modules/synacormediaBidAdapter_spec.js +++ b/test/spec/modules/synacormediaBidAdapter_spec.js @@ -244,6 +244,13 @@ describe('synacormediaBidAdapter ', function () { rtiPartner: 'TDID' } }] + }, + { + source: 'neustar.biz', + uids: [{ + id: 'neustar809-044-23njhwer3', + atype: 1 + }] } ]; @@ -989,7 +996,7 @@ describe('synacormediaBidAdapter ', function () { netRevenue: true, mediaType: 'video', ad: '\n\n\n\nSynacor Media Ad Server - 9999\nhttps://uat-net.technoratimedia.com/openrtb/tags?ID=QVVDVElPTl9JRD1lOTBhYWU1My1hZDkwLTRkNDEtYTQxMC1lZDY1MjIxMDc0ZGMmQVVDVElPTl9CSURfSUQ9MTEzMzkxMjgwMDE2OTIzMzd-OTk5OX4wJkFVQ1RJT05fU0VBVF9JRD05OTk5JkFVQ1RJT05fSU1QX0lEPXYyZGE3MzIyYjJkZjYxZi02NDB4NDgwJkFDVE9SX1JFRj1ha2thLnRjcDovL2F3cy1lYXN0MUBhZHMxMy5jYXAtdXNlMS5zeW5hY29yLmNvbToyNTUxL3VzZXIvJGNMYmZiIy0xOTk4NTIzNTk3JlNFQVRfSUQ9cHJlYmlk&AUCTION_PRICE=0.45\n\n\n', - ttl: 60, + ttl: 420, meta: { advertiserDomains: ['psacentral.org'] }, videoCacheKey: 'QVVDVElPTl9JRD1lOTBhYWU1My1hZDkwLTRkNDEtYTQxMC1lZDY1MjIxMDc0ZGMmQVVDVElPTl9CSURfSUQ9MTEzMzkxMjgwMDE2OTIzMzd-OTk5OX4wJkFVQ1RJT05fU0VBVF9JRD05OTk5JkFVQ1RJT05fSU1QX0lEPXYyZGE3MzIyYjJkZjYxZi02NDB4NDgwJkFDVE9SX1JFRj1ha2thLnRjcDovL2F3cy1lYXN0MUBhZHMxMy5jYXAtdXNlMS5zeW5hY29yLmNvbToyNTUxL3VzZXIvJGNMYmZiIy0xOTk4NTIzNTk3JlNFQVRfSUQ9cHJlYmlk', vastUrl: 'https://uat-net.technoratimedia.com/openrtb/tags?ID=QVVDVElPTl9JRD1lOTBhYWU1My1hZDkwLTRkNDEtYTQxMC1lZDY1MjIxMDc0ZGMmQVVDVElPTl9CSURfSUQ9MTEzMzkxMjgwMDE2OTIzMzd-OTk5OX4wJkFVQ1RJT05fU0VBVF9JRD05OTk5JkFVQ1RJT05fSU1QX0lEPXYyZGE3MzIyYjJkZjYxZi02NDB4NDgwJkFDVE9SX1JFRj1ha2thLnRjcDovL2F3cy1lYXN0MUBhZHMxMy5jYXAtdXNlMS5zeW5hY29yLmNvbToyNTUxL3VzZXIvJGNMYmZiIy0xOTk4NTIzNTk3JlNFQVRfSUQ9cHJlYmlk&AUCTION_PRICE=0.45' @@ -1010,7 +1017,7 @@ describe('synacormediaBidAdapter ', function () { netRevenue: true, mediaType: BANNER, ad: '', - ttl: 60 + ttl: 420 }); }); @@ -1032,7 +1039,7 @@ describe('synacormediaBidAdapter ', function () { netRevenue: true, mediaType: BANNER, ad: '', - ttl: 60 + ttl: 420 }); expect(resp[1]).to.eql({ @@ -1045,7 +1052,7 @@ describe('synacormediaBidAdapter ', function () { netRevenue: true, mediaType: BANNER, ad: '', - ttl: 60 + ttl: 420 }); }); @@ -1156,7 +1163,7 @@ describe('synacormediaBidAdapter ', function () { netRevenue: true, mediaType: 'video', ad: '\n\n\n\nSynacor Media Ad Server - 9999\nhttps://uat-net.technoratimedia.com/openrtb/tags?ID=QVVDVElPTl9JRD1lOTBhYWU1My1hZDkwLTRkNDEtYTQxMC1lZDY1MjIxMDc0ZGMmQVVDVElPTl9CSURfSUQ9MTEzMzkxMjgwMDE2OTIzMzd-OTk5OX4wJkFVQ1RJT05fU0VBVF9JRD05OTk5JkFVQ1RJT05fSU1QX0lEPXYyZGE3MzIyYjJkZjYxZi02NDB4NDgwJkFDVE9SX1JFRj1ha2thLnRjcDovL2F3cy1lYXN0MUBhZHMxMy5jYXAtdXNlMS5zeW5hY29yLmNvbToyNTUxL3VzZXIvJGNMYmZiIy0xOTk4NTIzNTk3JlNFQVRfSUQ9cHJlYmlk&AUCTION_PRICE=0.45\n\n\n', - ttl: 60, + ttl: 420, meta: { advertiserDomains: ['psacentral.org'] }, videoCacheKey: 'QVVDVElPTl9JRD1lOTBhYWU1My1hZDkwLTRkNDEtYTQxMC1lZDY1MjIxMDc0ZGMmQVVDVElPTl9CSURfSUQ9MTEzMzkxMjgwMDE2OTIzMzd-OTk5OX4wJkFVQ1RJT05fU0VBVF9JRD05OTk5JkFVQ1RJT05fSU1QX0lEPXYyZGE3MzIyYjJkZjYxZi02NDB4NDgwJkFDVE9SX1JFRj1ha2thLnRjcDovL2F3cy1lYXN0MUBhZHMxMy5jYXAtdXNlMS5zeW5hY29yLmNvbToyNTUxL3VzZXIvJGNMYmZiIy0xOTk4NTIzNTk3JlNFQVRfSUQ9cHJlYmlk', vastUrl: 'https://uat-net.technoratimedia.com/openrtb/tags?ID=QVVDVElPTl9JRD1lOTBhYWU1My1hZDkwLTRkNDEtYTQxMC1lZDY1MjIxMDc0ZGMmQVVDVElPTl9CSURfSUQ9MTEzMzkxMjgwMDE2OTIzMzd-OTk5OX4wJkFVQ1RJT05fU0VBVF9JRD05OTk5JkFVQ1RJT05fSU1QX0lEPXYyZGE3MzIyYjJkZjYxZi02NDB4NDgwJkFDVE9SX1JFRj1ha2thLnRjcDovL2F3cy1lYXN0MUBhZHMxMy5jYXAtdXNlMS5zeW5hY29yLmNvbToyNTUxL3VzZXIvJGNMYmZiIy0xOTk4NTIzNTk3JlNFQVRfSUQ9cHJlYmlk&AUCTION_PRICE=0.45' @@ -1209,9 +1216,63 @@ describe('synacormediaBidAdapter ', function () { netRevenue: true, mediaType: BANNER, ad: '', - ttl: 60 + ttl: 420 }); }); + + it('should return ttl equal to DEFAULT_TTL_MAX if bid.exp and bid.ext["imds.tv"].ttl are both undefined', function() { + const br = { ...bidResponse }; + serverResponse.body.seatbid[0].bid.push(br); + const resp = spec.interpretResponse(serverResponse, bidRequest); + expect(resp).to.be.an('array').to.have.lengthOf(1); + expect(resp[0]).to.have.property('ttl'); + expect(resp[0].ttl).to.equal(420); + }); + + it('should return ttl equal to bid.ext["imds.tv"].ttl if it is defined but bid.exp is undefined', function() { + let br = { ext: { 'imds.tv': { ttl: 4321 } }, ...bidResponse }; + serverResponse.body.seatbid[0].bid.push(br); + let resp = spec.interpretResponse(serverResponse, bidRequest); + expect(resp).to.be.an('array').to.have.lengthOf(1); + expect(resp[0]).to.have.property('ttl'); + expect(resp[0].ttl).to.equal(4321); + }); + + it('should return ttl equal to bid.exp if bid.exp is less than or equal to DEFAULT_TTL_MAX and bid.ext["imds.tv"].ttl is undefined', function() { + const br = { exp: 123, ...bidResponse }; + serverResponse.body.seatbid[0].bid.push(br); + const resp = spec.interpretResponse(serverResponse, bidRequest); + expect(resp).to.be.an('array').to.have.lengthOf(1); + expect(resp[0]).to.have.property('ttl'); + expect(resp[0].ttl).to.equal(123); + }); + + it('should return ttl equal to DEFAULT_TTL_MAX if bid.exp is greater than DEFAULT_TTL_MAX and bid.ext["imds.tv"].ttl is undefined', function() { + const br = { exp: 4321, ...bidResponse }; + serverResponse.body.seatbid[0].bid.push(br); + const resp = spec.interpretResponse(serverResponse, bidRequest); + expect(resp).to.be.an('array').to.have.lengthOf(1); + expect(resp[0]).to.have.property('ttl'); + expect(resp[0].ttl).to.equal(420); + }); + + it('should return ttl equal to bid.exp if bid.exp is less than or equal to bid.ext["imds.tv"].ttl', function() { + const br = { exp: 1234, ext: { 'imds.tv': { ttl: 4321 } }, ...bidResponse }; + serverResponse.body.seatbid[0].bid.push(br); + const resp = spec.interpretResponse(serverResponse, bidRequest); + expect(resp).to.be.an('array').to.have.lengthOf(1); + expect(resp[0]).to.have.property('ttl'); + expect(resp[0].ttl).to.equal(1234); + }); + + it('should return ttl equal to bid.ext["imds.tv"].ttl if bid.exp is greater than bid.ext["imds.tv"].ttl', function() { + const br = { exp: 4321, ext: { 'imds.tv': { ttl: 1234 } }, ...bidResponse }; + serverResponse.body.seatbid[0].bid.push(br); + const resp = spec.interpretResponse(serverResponse, bidRequest); + expect(resp).to.be.an('array').to.have.lengthOf(1); + expect(resp[0]).to.have.property('ttl'); + expect(resp[0].ttl).to.equal(1234); + }); }); describe('getUserSyncs', function () { it('should return a usersync when iframes is enabled', function () { @@ -1231,4 +1292,74 @@ describe('synacormediaBidAdapter ', function () { expect(usersyncs).to.be.an('array').that.is.empty; }); }); + + describe('Bid Requests with price module should use if available', function () { + let validVideoBidRequest = { + bidder: 'synacormedia', + params: { + bidfloor: '0.50', + seatId: 'prebid', + placementId: 'demo1', + pos: 1, + video: {} + }, + renderer: { + url: '../syncOutstreamPlayer.js' + }, + mediaTypes: { + video: { + playerSize: [[300, 250]], + context: 'outstream' + } + }, + adUnitCode: 'div-1', + transactionId: '0869f34e-090b-4b20-84ee-46ff41405a39', + sizes: [[300, 250]], + bidId: '22b3a2268d9f0e', + bidderRequestId: '1d195910597e13', + auctionId: '3375d336-2aea-4ee7-804c-6d26b621ad20', + src: 'client', + bidRequestsCount: 1, + bidderRequestsCount: 1, + bidderWinsCount: 0 + }; + + let validBannerBidRequest = { + bidId: '9876abcd', + sizes: [[300, 250]], + params: { + bidfloor: '0.50', + seatId: 'prebid', + placementId: '1234', + } + }; + + let bidderRequest = { + refererInfo: { + referer: 'http://localhost:9999/' + }, + bidderCode: 'synacormedia', + auctionId: 'f8a75621-d672-4cbb-9275-3db7d74fb110' + }; + + it('should return valid bidfloor using price module for banner/video impression', function () { + let bannerRequest = spec.buildRequests([validBannerBidRequest], bidderRequest); + let videoRequest = spec.buildRequests([validVideoBidRequest], bidderRequest); + + expect(bannerRequest.data.imp[0].bidfloor).to.equal(0.5); + expect(videoRequest.data.imp[0].bidfloor).to.equal(0.5); + + let priceModuleFloor = 3; + let floorResponse = { currency: 'USD', floor: priceModuleFloor }; + + validBannerBidRequest.getFloor = () => { return floorResponse; }; + validVideoBidRequest.getFloor = () => { return floorResponse; }; + + bannerRequest = spec.buildRequests([validBannerBidRequest], bidderRequest); + videoRequest = spec.buildRequests([validVideoBidRequest], bidderRequest); + + expect(bannerRequest.data.imp[0].bidfloor).to.equal(priceModuleFloor); + expect(videoRequest.data.imp[0].bidfloor).to.equal(priceModuleFloor); + }); + }); }); diff --git a/test/spec/modules/tappxBidAdapter_spec.js b/test/spec/modules/tappxBidAdapter_spec.js index 49d739ed3be..8866670df77 100644 --- a/test/spec/modules/tappxBidAdapter_spec.js +++ b/test/spec/modules/tappxBidAdapter_spec.js @@ -134,12 +134,6 @@ describe('Tappx bid adapter', function () { assert.isTrue(spec.isBidRequestValid(c_BIDREQUEST.bids[0]), JSON.stringify(c_BIDREQUEST)); }); - it('should return false when params are missing', function () { - let badBidRequestParam = JSON.parse(JSON.stringify(c_BIDREQUEST)); - delete badBidRequestParam.bids[0].params; - assert.isFalse(spec.isBidRequestValid(badBidRequestParam.bids[0])); - }); - it('should return false when tappxkey is missing', function () { let badBidRequestTpxkey = JSON.parse(JSON.stringify(c_BIDREQUEST)); ; delete badBidRequestTpxkey.bids[0].params.tappxkey; @@ -165,21 +159,11 @@ describe('Tappx bid adapter', function () { assert.isTrue(spec.isBidRequestValid(badBidRequestNwEp.bids[0])); }); - it('should return false mimes param is missing', function () { - let badBidRequest_mimes = c_BIDDERREQUEST_V; - delete badBidRequest_mimes.bids.mediaTypes.video; - badBidRequest_mimes.bids.mediaTypes.video = {}; - badBidRequest_mimes.bids.mediaTypes.video.context = 'instream'; - badBidRequest_mimes.bids.mediaTypes.video.playerSize = [320, 250]; - assert.isFalse(spec.isBidRequestValid(badBidRequest_mimes.bids), badBidRequest_mimes); - }); - it('should return false for not instream/outstream requests', function () { let badBidRequest_v = c_BIDDERREQUEST_V; delete badBidRequest_v.bids.mediaTypes.banner; badBidRequest_v.bids.mediaTypes.video = {}; badBidRequest_v.bids.mediaTypes.video.context = ''; - badBidRequest_v.bids.mediaTypes.video.mimes = [ 'video/mp4', 'application/javascript' ]; badBidRequest_v.bids.mediaTypes.video.playerSize = [320, 250]; assert.isFalse(spec.isBidRequestValid(badBidRequest_v.bids)); }); @@ -232,7 +216,6 @@ describe('Tappx bid adapter', function () { validBidRequests_V[0].mediaTypes.video = {}; validBidRequests_V[0].mediaTypes.video.playerSize = [640, 480]; validBidRequests_V[0].mediaTypes.video.context = 'instream'; - validBidRequests_V[0].mediaTypes.video.mimes = [ 'video/mp4', 'application/javascript' ]; bidderRequest_V.bids.mediaTypes.context = 'instream'; @@ -252,7 +235,6 @@ describe('Tappx bid adapter', function () { validBidRequests_Voutstream[0].mediaTypes.video = {}; validBidRequests_Voutstream[0].mediaTypes.video.playerSize = [640, 480]; validBidRequests_Voutstream[0].mediaTypes.video.context = 'outstream'; - validBidRequests_Voutstream[0].mediaTypes.video.mimes = [ 'video/mp4', 'application/javascript' ]; bidderRequest_VOutstream.bids.mediaTypes.context = 'outstream'; @@ -273,7 +255,6 @@ describe('Tappx bid adapter', function () { validBidRequests_Voutstream[0].mediaTypes.video.rewarded = 1; validBidRequests_Voutstream[0].mediaTypes.video.playerSize = [640, 480]; validBidRequests_Voutstream[0].mediaTypes.video.context = 'outstream'; - validBidRequests_Voutstream[0].mediaTypes.video.mimes = [ 'video/mp4', 'application/javascript' ]; bidderRequest_VOutstream.bids.mediaTypes.context = 'outstream'; diff --git a/test/spec/modules/targetVideoBidAdapter_spec.js b/test/spec/modules/targetVideoBidAdapter_spec.js new file mode 100644 index 00000000000..0ce6f0fb70d --- /dev/null +++ b/test/spec/modules/targetVideoBidAdapter_spec.js @@ -0,0 +1,96 @@ +import { spec } from '../../../modules/targetVideoBidAdapter.js' + +describe('TargetVideo Bid Adapter', function() { + const bannerRequest = [{ + bidder: 'targetVideo', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + params: { + placementId: 12345, + } + }]; + + it('Test the bid validation function', function() { + const validBid = spec.isBidRequestValid(bannerRequest[0]); + const invalidBid = spec.isBidRequestValid(null); + + expect(validBid).to.be.true; + expect(invalidBid).to.be.false; + }); + + it('Test the request processing function', function () { + const request = spec.buildRequests(bannerRequest, bannerRequest[0]); + expect(request).to.not.be.empty; + + const payload = JSON.parse(request.data); + expect(payload).to.not.be.empty; + expect(payload.sdk).to.deep.equal({ + source: 'pbjs', + version: '$prebid.version$' + }); + expect(payload.tags[0].id).to.equal(12345); + expect(payload.tags[0].gpid).to.equal('targetVideo'); + expect(payload.tags[0].ad_types[0]).to.equal('video'); + }); + + it('Handle nobid responses', function () { + const responseBody = { + 'version': '0.0.1', + 'tags': [{ + 'uuid': '84ab500420319d', + 'tag_id': 5976557, + 'auction_id': '297492697822162468', + 'nobid': true + }] + }; + const bidderRequest = null; + + const bidResponse = spec.interpretResponse({ body: responseBody }, {bidderRequest}); + expect(bidResponse.length).to.equal(0); + }); + + it('Test the response parsing function', function () { + const responseBody = { + 'tags': [{ + 'uuid': '84ab500420319d', + 'ads': [{ + 'ad_type': 'video', + 'cpm': 0.500000, + 'notify_url': 'https://www.target-video.com/', + 'rtb': { + 'video': { + 'player_width': 640, + 'player_height': 360, + 'asset_url': 'https://www.target-video.com/' + } + } + }] + }] + }; + const bidderRequest = { + bids: [{ + bidId: '84ab500420319d', + adUnitCode: 'code', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + } + }] + }; + + const bidResponse = spec.interpretResponse({ body: responseBody }, {bidderRequest}); + expect(bidResponse).to.not.be.empty; + + const bid = bidResponse[0]; + expect(bid).to.not.be.empty; + expect(bid.cpm).to.equal(0.675); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.ad).to.include('') + expect(bid.ad).to.include('initPlayer') + }); +}); diff --git a/test/spec/modules/teadsBidAdapter_spec.js b/test/spec/modules/teadsBidAdapter_spec.js index 83f5045cca1..75ed7452b57 100644 --- a/test/spec/modules/teadsBidAdapter_spec.js +++ b/test/spec/modules/teadsBidAdapter_spec.js @@ -614,15 +614,13 @@ describe('teadsBidAdapter', () => { } ]; - it('should add gpid if ortb2Imp.ext.data.pbadslot is present and is non empty (and ortb2Imp.ext.data.adserver.adslot is not present)', function () { + it('should add gpid if ortb2Imp.ext.gpid is present and is non empty', function () { const updatedBidRequests = bidRequests.map(function(bidRequest, index) { return { ...bidRequest, ortb2Imp: { ext: { - data: { - pbadslot: '1111/home-left-' + index - } + gpid: '1111/home-left-' + index } } }; @@ -635,41 +633,12 @@ describe('teadsBidAdapter', () => { expect(payload.data[1].gpid).to.equal('1111/home-left-1'); }); - it('should add gpid if ortb2Imp.ext.data.pbadslot is present and is non empty (even if ortb2Imp.ext.data.adserver.adslot is present and is non empty too)', function () { - const updatedBidRequests = bidRequests.map(function(bidRequest, index) { - return { - ...bidRequest, - ortb2Imp: { - ext: { - data: { - pbadslot: '1111/home-left-' + index, - adserver: { - adslot: '1111/home-left/div-' + index - } - } - } - } - }; - } - ); - const request = spec.buildRequests(updatedBidRequests, bidderResquestDefault); - const payload = JSON.parse(request.data); - - expect(payload.data[0].gpid).to.equal('1111/home-left-0'); - expect(payload.data[1].gpid).to.equal('1111/home-left-1'); - }); - - it('should not add gpid if both ortb2Imp.ext.data.pbadslot and ortb2Imp.ext.data.adserver.adslot are present but empty', function () { + it('should not add gpid if ortb2Imp.ext.gpid is present but empty', function () { const updatedBidRequests = bidRequests.map(bidRequest => ({ ...bidRequest, ortb2Imp: { ext: { - data: { - pbadslot: '', - adserver: { - adslot: '' - } - } + gpid: '' } } })); @@ -682,36 +651,22 @@ describe('teadsBidAdapter', () => { }); }); - it('should not add gpid if both ortb2Imp.ext.data.pbadslot and ortb2Imp.ext.data.adserver.adslot are not present', function () { - const request = spec.buildRequests(bidRequests, bidderResquestDefault); + it('should not add gpid if ortb2Imp.ext.gpid is not present', function () { + const updatedBidRequests = bidRequests.map(bidRequest => ({ + ...bidRequest, + ortb2Imp: { + ext: { + } + } + })); + + const request = spec.buildRequests(updatedBidRequests, bidderResquestDefault); const payload = JSON.parse(request.data); return payload.data.forEach(bid => { expect(bid).not.to.have.property('gpid'); }); }); - - it('should add gpid if ortb2Imp.ext.data.pbadslot is not present but ortb2Imp.ext.data.adserver.adslot is present and is non empty', function () { - const updatedBidRequests = bidRequests.map(function(bidRequest, index) { - return { - ...bidRequest, - ortb2Imp: { - ext: { - data: { - adserver: { - adslot: '1111/home-left-' + index - } - } - } - } - }; - }); - const request = spec.buildRequests(updatedBidRequests, bidderResquestDefault); - const payload = JSON.parse(request.data); - - expect(payload.data[0].gpid).to.equal('1111/home-left-0'); - expect(payload.data[1].gpid).to.equal('1111/home-left-1'); - }); }); function checkMediaTypesSizes(mediaTypes, expectedSizes) { diff --git a/test/spec/modules/telariaBidAdapter_spec.js b/test/spec/modules/telariaBidAdapter_spec.js index 25649115cc1..457dd568764 100644 --- a/test/spec/modules/telariaBidAdapter_spec.js +++ b/test/spec/modules/telariaBidAdapter_spec.js @@ -234,9 +234,7 @@ describe('TelariaAdapter', () => { }]; it('should get correct bid response', () => { - let expectedResponseKeys = ['bidderCode', 'width', 'height', 'statusMessage', 'adId', 'mediaType', 'source', - 'getStatusCode', 'getSize', 'requestId', 'cpm', 'creativeId', 'vastXml', - 'vastUrl', 'currency', 'netRevenue', 'ttl', 'ad', 'meta']; + let expectedResponseKeys = ['requestId', 'cpm', 'creativeId', 'vastXml', 'vastUrl', 'mediaType', 'width', 'height', 'currency', 'netRevenue', 'ttl', 'ad', 'meta']; let bidRequest = spec.buildRequests(stub, BIDDER_REQUEST)[0]; bidRequest.bidId = '1234'; diff --git a/test/spec/modules/trustpidSystem_spec.js b/test/spec/modules/trustpidSystem_spec.js new file mode 100644 index 00000000000..97e87e3a303 --- /dev/null +++ b/test/spec/modules/trustpidSystem_spec.js @@ -0,0 +1,232 @@ +import { expect } from 'chai'; +import { trustpidSubmodule } from 'modules/trustpidSystem.js'; +import { storage } from 'modules/trustpidSystem.js'; + +describe('trustpid System', () => { + const connectDataKey = 'fcIdConnectData'; + const connectDomainKey = 'fcIdConnectDomain'; + + const getStorageData = (idGraph) => { + if (!idGraph) { + idGraph = {id: 501, domain: ''}; + } + return { + 'connectId': { + 'idGraph': [idGraph], + } + } + }; + + it('should have the correct module name declared', () => { + expect(trustpidSubmodule.name).to.equal('trustpid'); + }); + + describe('trustpid getId()', () => { + afterEach(() => { + storage.removeDataFromLocalStorage(connectDataKey); + storage.removeDataFromLocalStorage(connectDomainKey); + }); + + it('it should return object with key callback', () => { + expect(trustpidSubmodule.getId()).to.have.property('callback'); + }); + + it('should return object with key callback with value type - function', () => { + storage.setDataInLocalStorage(connectDataKey, JSON.stringify(getStorageData())); + expect(trustpidSubmodule.getId()).to.have.property('callback'); + expect(typeof trustpidSubmodule.getId().callback).to.be.equal('function'); + }); + + it('tests if localstorage & JSON works properly ', () => { + const idGraph = { + 'domain': 'domainValue', + 'umid': 'umidValue', + }; + storage.setDataInLocalStorage(connectDataKey, JSON.stringify(getStorageData(idGraph))); + expect(JSON.parse(storage.getDataFromLocalStorage(connectDataKey))).to.have.property('connectId'); + }); + + it('returns {callback: func} if domains don\'t match', () => { + const idGraph = { + 'domain': 'domainValue', + 'umid': 'umidValue', + }; + storage.setDataInLocalStorage(connectDomainKey, JSON.stringify('differentDomainValue')); + storage.setDataInLocalStorage(connectDataKey, JSON.stringify(getStorageData(idGraph))); + expect(trustpidSubmodule.getId()).to.have.property('callback'); + }); + + it('returns {id: {trustpid: data.trustpid}} if we have the right data stored in the localstorage ', () => { + const idGraph = { + 'domain': 'uat.mno.link', + 'umid': 'umidValue', + }; + storage.setDataInLocalStorage(connectDomainKey, JSON.stringify('uat.mno.link')); + storage.setDataInLocalStorage(connectDataKey, JSON.stringify(getStorageData(idGraph))); + const response = trustpidSubmodule.getId(); + expect(response).to.have.property('id'); + expect(response.id).to.have.property('trustpid'); + expect(response.id.trustpid).to.be.equal('umidValue-xxxx'); + }); + + it('returns {trustpid: data.trustpid} if we have the right data stored in the localstorage right after the callback is called', (done) => { + const idGraph = { + 'domain': 'uat.mno.link', + 'umid': 'umidValue', + }; + const response = trustpidSubmodule.getId(); + expect(response).to.have.property('callback'); + expect(response.callback.toString()).contain('result(callback)'); + + if (typeof response.callback === 'function') { + storage.setDataInLocalStorage(connectDomainKey, JSON.stringify('uat.mno.link')); + storage.setDataInLocalStorage(connectDataKey, JSON.stringify(getStorageData(idGraph))); + response.callback(function (result) { + expect(result).to.not.be.null; + expect(result).to.have.property('trustpid'); + expect(result.trustpid).to.be.equal('umidValue-xxxx'); + done() + }) + } + }); + + it('returns null if domains don\'t match', (done) => { + const idGraph = { + 'domain': 'uat.mno.link', + 'umid': 'umidValue', + }; + storage.setDataInLocalStorage(connectDomainKey, JSON.stringify('differentDomainValue')); + storage.setDataInLocalStorage(connectDataKey, JSON.stringify(getStorageData(idGraph))); + + const response = trustpidSubmodule.getId(); + expect(response).to.have.property('callback'); + expect(response.callback.toString()).contain('result(callback)'); + + if (typeof response.callback === 'function') { + setTimeout(() => { + expect(JSON.parse(storage.getDataFromLocalStorage(connectDomainKey))).to.be.equal('differentDomainValue'); + }, 100) + response.callback(function (result) { + expect(result).to.be.null; + done() + }) + } + }); + + it('returns {trustpid: data.trustpid} if we have the right data stored in the localstorage right after 500ms delay', (done) => { + const idGraph = { + 'domain': 'uat.mno.link', + 'umid': 'umidValue', + }; + + const response = trustpidSubmodule.getId(); + expect(response).to.have.property('callback'); + expect(response.callback.toString()).contain('result(callback)'); + + if (typeof response.callback === 'function') { + setTimeout(() => { + storage.setDataInLocalStorage(connectDomainKey, JSON.stringify('uat.mno.link')); + storage.setDataInLocalStorage(connectDataKey, JSON.stringify(getStorageData(idGraph))); + }, 500); + response.callback(function (result) { + expect(result).to.not.be.null; + expect(result).to.have.property('trustpid'); + expect(result.trustpid).to.be.equal('umidValue-xxxx'); + done() + }) + } + }); + + it('returns null if we have the data stored in the localstorage after 500ms delay and the max (waiting) delay is only 200ms ', (done) => { + const idGraph = { + 'domain': 'uat.mno.link', + 'umid': 'umidValue', + }; + + const response = trustpidSubmodule.getId({params: {maxDelayTime: 200}}); + expect(response).to.have.property('callback'); + expect(response.callback.toString()).contain('result(callback)'); + + if (typeof response.callback === 'function') { + setTimeout(() => { + storage.setDataInLocalStorage(connectDomainKey, JSON.stringify('uat.mno.link')); + storage.setDataInLocalStorage(connectDataKey, JSON.stringify(getStorageData(idGraph))); + }, 500); + response.callback(function (result) { + expect(result).to.be.null; + done() + }) + } + }); + }); + + describe('trustpid decode()', () => { + const VALID_API_RESPONSES = [ + { + expected: '32a97f612', + payload: { + trustpid: '32a97f612' + } + }, + { + expected: '32a97f61', + payload: { + trustpid: '32a97f61', + } + }, + ]; + VALID_API_RESPONSES.forEach(responseData => { + it('should return a newly constructed object with the trustpid for a payload with {trustpid: value}', () => { + expect(trustpidSubmodule.decode(responseData.payload)).to.deep.equal( + {trustpid: responseData.expected} + ); + }); + }); + + [{}, '', {foo: 'bar'}].forEach((response) => { + it(`should return null for an invalid response "${JSON.stringify(response)}"`, () => { + expect(trustpidSubmodule.decode(response)).to.be.null; + }); + }); + }); + + describe('trustpid messageHandler for acronyms', () => { + afterEach(() => { + storage.removeDataFromLocalStorage(connectDataKey); + storage.removeDataFromLocalStorage(connectDomainKey); + }); + + const domains = [ + {domain: 'tmi.mno.link', acronym: 'ndye'}, + {domain: 'tmi.vodafone.de', acronym: 'pqnx'}, + {domain: 'tmi.telekom.de', acronym: 'avgw'}, + {domain: 'tmi.tmid.es', acronym: 'kjws'}, + {domain: 'uat.mno.link', acronym: 'xxxx'}, + {domain: 'es.tmiservice.orange.com', acronym: 'aplw'}, + ]; + + domains.forEach(({domain, acronym}) => { + it(`correctly sets trustpid value and acronym to ${acronym} for ${domain} domain`, (done) => { + const idGraph = { + 'domain': domain, + 'umid': 'umidValue', + }; + + storage.setDataInLocalStorage(connectDomainKey, JSON.stringify(domain)); + storage.setDataInLocalStorage(connectDataKey, JSON.stringify(getStorageData(idGraph))); + + const eventData = { + data: `{\"msgType\":\"MNOSELECTOR\",\"body\":{\"url\":\"https://${domain}/some/path\"}}` + }; + + window.dispatchEvent(new MessageEvent('message', eventData)); + + const response = trustpidSubmodule.getId(); + expect(response).to.have.property('id'); + expect(response.id).to.have.property('trustpid'); + expect(response.id.trustpid).to.be.equal(`umidValue-${acronym}`); + done(); + }); + }); + }); +}); diff --git a/test/spec/modules/trustxBidAdapter_spec.js b/test/spec/modules/trustxBidAdapter_spec.js index e710bd6d00f..b34813948fc 100644 --- a/test/spec/modules/trustxBidAdapter_spec.js +++ b/test/spec/modules/trustxBidAdapter_spec.js @@ -401,30 +401,68 @@ describe('TrustXAdapter', function () { expect(payload.site.content).to.deep.equal(jsContent); }); - it('if segment is present in permutive targeting, payload must have right params', function () { - const permSegments = [{id: 'test_perm_1'}, {id: 'test_perm_2'}]; - const bidRequestsWithPermutiveTargeting = bidRequests.map((bid) => { - return Object.assign({ - rtd: { - p_standard: { - targeting: { - segments: permSegments - } + it('should have user.data filled from config ortb2.user.data', function () { + const userData = [ + { + name: 'someName', + segment: [1, 2, { anyKey: 'anyVal' }, 'segVal', { id: 'segId' }, { value: 'segValue' }, { id: 'segId2', name: 'segName' }] + }, + { + name: 'permutive.com', + segment: [1, 2, 'segVal', { id: 'segId' }, { anyKey: 'anyVal' }, { value: 'segValue' }, { id: 'segId2', name: 'segName' }] + }, + { + someKey: 'another data' + } + ]; + + const getConfigStub = sinon.stub(config, 'getConfig').callsFake( + arg => arg === 'ortb2.user.data' ? userData : null); + const request = spec.buildRequests([bidRequests[0]], bidderRequest); + const payload = parseRequest(request.data); + expect(payload.user.data).to.deep.equal(userData); + getConfigStub.restore(); + }); + + it('should have right value in user.data when jwpsegments are present', function () { + const userData = [ + { + name: 'someName', + segment: [1, 2, { anyKey: 'anyVal' }, 'segVal', { id: 'segId' }, { value: 'segValue' }, { id: 'segId2', name: 'segName' }] + }, + { + name: 'permutive.com', + segment: [1, 2, 'segVal', { id: 'segId' }, { anyKey: 'anyVal' }, { value: 'segValue' }, { id: 'segId2', name: 'segName' }] + }, + { + someKey: 'another data' + } + ]; + const getConfigStub = sinon.stub(config, 'getConfig').callsFake( + arg => arg === 'ortb2.user.data' ? userData : null); + + const jsContent = {id: 'test_jw_content_id'}; + const jsSegments = ['test_seg_1', 'test_seg_2']; + const bidRequestsWithJwTargeting = Object.assign({}, bidRequests[0], { + rtd: { + jwplayer: { + targeting: { + segments: jsSegments, + content: jsContent } } - }, bid); + } }); - const request = spec.buildRequests(bidRequestsWithPermutiveTargeting, bidderRequest); - expect(request.data).to.be.an('string'); + const request = spec.buildRequests([bidRequestsWithJwTargeting], bidderRequest); const payload = parseRequest(request.data); - expect(payload).to.have.property('user'); expect(payload.user.data).to.deep.equal([{ - name: 'permutive', + name: 'iow_labs_pub_data', segment: [ - {name: 'p_standard', value: permSegments[0].id}, - {name: 'p_standard', value: permSegments[1].id} + {name: 'jwpseg', value: jsSegments[0]}, + {name: 'jwpseg', value: jsSegments[1]} ] - }]); + }, ...userData]); + getConfigStub.restore(); }); it('should contain the keyword values if it present in ortb2.(site/user)', function () { @@ -501,7 +539,7 @@ describe('TrustXAdapter', function () { getConfigStub.restore(); }); - it('shold be right tmax when timeout in config is less then timeout in bidderRequest', function() { + it('should be right tmax when timeout in config is less then timeout in bidderRequest', function() { const getConfigStub = sinon.stub(config, 'getConfig').callsFake( arg => arg === 'bidderTimeout' ? 2000 : null); const request = spec.buildRequests([bidRequests[0]], bidderRequest); @@ -510,7 +548,7 @@ describe('TrustXAdapter', function () { expect(payload.tmax).to.equal(2000); getConfigStub.restore(); }); - it('shold be right tmax when timeout in bidderRequest is less then timeout in config', function() { + it('should be right tmax when timeout in bidderRequest is less then timeout in config', function() { const getConfigStub = sinon.stub(config, 'getConfig').callsFake( arg => arg === 'bidderTimeout' ? 5000 : null); const request = spec.buildRequests([bidRequests[0]], bidderRequest); @@ -561,6 +599,39 @@ describe('TrustXAdapter', function () { divid: bidRequests[2].adUnitCode }); }); + it('should contain imp[].instl if available', function() { + const ortb2Imp = [{ + instl: 1 + }, { + instl: 2, + ext: { + data: { + adserver: { + name: 'ad_server_name', + adslot: '/222222/slot' + }, + pbadslot: '/222222/slot' + } + } + }]; + const bidRequestsWithOrtb2Imp = bidRequests.slice(0, 3).map((bid, ind) => { + return Object.assign(ortb2Imp[ind] ? { ortb2Imp: ortb2Imp[ind] } : {}, bid); + }); + const request = spec.buildRequests(bidRequestsWithOrtb2Imp, bidderRequest); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload.imp[0].instl).to.equal(1); + expect(payload.imp[1].ext).to.deep.equal({ + divid: bidRequests[1].adUnitCode, + data: ortb2Imp[1].ext.data, + gpid: ortb2Imp[1].ext.data.adserver.adslot + }); + expect(payload.imp[1].instl).to.equal(2); + expect(payload.imp[2].ext).to.deep.equal({ + divid: bidRequests[2].adUnitCode + }); + expect(payload.imp[2].instl).to.be.undefined; + }); it('all id like request fields must be a string', function () { const bidderRequestWithNumId = Object.assign({}, bidderRequest, { bidderRequestId: 123123, auctionId: 345345543 }); diff --git a/test/spec/modules/ttdBidAdapter_spec.js b/test/spec/modules/ttdBidAdapter_spec.js new file mode 100644 index 00000000000..099e5e56c33 --- /dev/null +++ b/test/spec/modules/ttdBidAdapter_spec.js @@ -0,0 +1,1313 @@ +import { expect } from 'chai'; +import { spec } from 'modules/ttdBidAdapter'; +import { deepClone } from 'src/utils.js'; +import { config } from 'src/config'; + +describe('ttdBidAdapter', function () { + function testBuildRequests(bidRequests, bidderRequestBase) { + let clonedBidderRequest = deepClone(bidderRequestBase); + clonedBidderRequest.bids = bidRequests; + return spec.buildRequests(bidRequests, clonedBidderRequest); + } + + describe('isBidRequestValid', function() { + function makeBid() { + return { + 'bidder': 'ttd', + 'params': { + 'supplySourceId': 'supplier', + 'publisherId': '22222222', + 'placementId': 'some-PlacementId_1', + 'siteId': 'testSiteId' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [300, 250] + ] + } + }, + 'adUnitCode': 'adunit-code', + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; + } + + describe('core', function () { + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(makeBid())).to.equal(true); + }); + + it('should return false when publisherId not passed', function () { + let bid = makeBid(); + delete bid.params.publisherId; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when supplySourceId not passed', function () { + let bid = makeBid(); + delete bid.params.supplySourceId; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when publisherId is longer than 64 characters', function () { + let bid = makeBid(); + bid.params.publisherId = '1111111111111111111111111111111111111111111111111111111111111111111111'; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when siteId not passed', function () { + let bid = makeBid(); + delete bid.params.siteId; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when siteId is longer than 50 characters', function () { + let bid = makeBid(); + bid.params.siteId = '1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111' + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when placementId not passed', function () { + let bid = makeBid(); + delete bid.params.placementId; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when the placementId is longer than 128 characters', function () { + let bid = makeBid(); + bid.params.placementId = '1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111'; // 130 characters + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false if neither mediaTypes.banner nor mediaTypes.video is passed', function () { + let bid = makeBid(); + delete bid.mediaTypes + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('banner', function () { + it('should return true if banner.pos is passed correctly', function () { + let bid = makeBid(); + bid.mediaTypes.banner.pos = 1; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + }); + + describe('video', function () { + function makeBid() { + return { + 'bidder': 'ttd', + 'params': { + 'supplySourceId': 'supplier', + 'publisherId': '22222222', + 'siteId': 'testSiteId123', + 'placementId': 'somePlacementId' + }, + 'mediaTypes': { + 'video': { + 'minduration': 5, + 'maxduration': 30, + 'playerSize': [640, 480], + 'api': [1, 3], + 'mimes': ['video/mp4'], + 'protocols': [2, 3, 5, 6] + } + }, + 'adUnitCode': 'adunit-code', + 'sizes': [ + [300, 250] + ], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; + } + + it('should return true if required parameters are passed', function () { + let bid = makeBid(); + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false if maxduration is missing', function () { + let bid = makeBid(); + delete bid.mediaTypes.video.maxduration; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false if api is missing', function () { + let bid = makeBid(); + delete bid.mediaTypes.video.api; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false if mimes is missing', function () { + let bid = makeBid(); + delete bid.mediaTypes.video.mimes; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false if protocols is missing', function () { + let bid = makeBid(); + delete bid.mediaTypes.video.protocols; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + }); + + describe('getUserSyncs', function () { + it('to check the user sync iframe', function () { + const syncOptions = { + pixelEnabled: true + }; + const gdprConsentString = 'BON3G4EON3G4EAAABAENAA____ABl____A'; + const gdprConsent = { + consentString: gdprConsentString, + gdprApplies: true + }; + const uspConsent = '1YYY'; + + let syncs = spec.getUserSyncs(syncOptions, [], gdprConsent, uspConsent); + expect(syncs).to.have.lengthOf(1); + expect(syncs[0].type).to.equal('image'); + + let params = new URLSearchParams(new URL(syncs[0].url).search); + expect(params.get('us_privacy')).to.equal(uspConsent); + expect(params.get('ust')).to.equal('image'); + expect(params.get('gdpr')).to.equal('1'); + expect(params.get('gdpr_consent')).to.equal(gdprConsentString); + }); + }); + + describe('buildRequests-banner', function () { + const baseBannerBidRequests = [{ + 'bidder': 'ttd', + 'params': { + 'supplySourceId': 'supplier', + 'publisherId': '13144370', + 'placementId': '1gaa015', + 'siteId': 'testSiteId123' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 250], [300, 600]] + } + }, + 'sizes': [[300, 250], [300, 600]], + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': '8651474f-58b1-4368-b812-84f8c937a099', + 'bidId': '243310435309b5', + 'bidderRequestId': '18084284054531', + 'auctionId': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'src': 'client', + 'bidRequestsCount': 1 + }]; + + const baseBidderRequest = { + 'bidderCode': 'ttd', + 'auctionId': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'bidderRequestId': '18084284054531', + 'auctionStart': 1540945362095, + 'timeout': 3000, + 'refererInfo': { + 'referer': 'https://www.example.com/test', + 'reachedTop': true, + 'numIframes': 0, + 'stack': [ + 'https://www.example.com/test' + ] + }, + 'start': 1540945362099, + 'doneCbCallCount': 0 + }; + + it('sends bid request to our endpoint that makes sense', function () { + const request = testBuildRequests(baseBannerBidRequests, baseBidderRequest); + expect(request.method).to.equal('POST'); + expect(request.url).to.be.not.empty; + expect(request.data).to.be.not.null; + }); + + it('sets impression id to ad unit\'s bid id', function () { + const requestBody = testBuildRequests(baseBannerBidRequests, baseBidderRequest).data; + expect(requestBody.imp[0].id).to.equal('243310435309b5'); + }); + + it('sends bid requests to the correct endpoint', function () { + const url = testBuildRequests(baseBannerBidRequests, baseBidderRequest).url; + expect(url).to.equal('https://direct.adsrvr.org/bid/bidder/supplier'); + }); + + it('sends publisher id, site id, and placement id', function () { + const requestBody = testBuildRequests(baseBannerBidRequests, baseBidderRequest).data; + expect(requestBody.site).to.be.not.null; + expect(requestBody.site.publisher).to.be.not.null; + expect(requestBody.imp[0].tagid).to.be.not.null; + expect(requestBody.site.publisher.id).to.equal(baseBannerBidRequests[0].params.publisherId); + expect(requestBody.site.id).to.equal(baseBannerBidRequests[0].params.siteId); + expect(requestBody.imp[0].tagid).to.equal(baseBannerBidRequests[0].params.placementId); + }); + + it('includes the ad size in the bid request', function () { + const requestBody = testBuildRequests(baseBannerBidRequests, baseBidderRequest).data; + expect(requestBody.imp[0].banner.format[0].w).to.equal(300); + expect(requestBody.imp[0].banner.format[0].h).to.equal(250); + expect(requestBody.imp[0].banner.format[1].w).to.equal(300); + expect(requestBody.imp[0].banner.format[1].h).to.equal(600); + }); + + it('includes the detected referer in the bid request', function () { + const requestBody = testBuildRequests(baseBannerBidRequests, baseBidderRequest).data; + expect(requestBody.site.page).to.equal('https://www.example.com/test'); + }); + + it('sets the banner pos correctly if sent', function () { + let clonedBannerRequests = deepClone(baseBannerBidRequests); + clonedBannerRequests[0].mediaTypes.banner.pos = 1; + + const requestBody = testBuildRequests(clonedBannerRequests, baseBidderRequest).data; + expect(requestBody.imp[0].banner.pos).to.equal(1); + }); + + it('sets the banner expansion direction correctly if sent', function () { + let clonedBannerRequests = deepClone(baseBannerBidRequests); + const expdir = [1, 3] + clonedBannerRequests[0].params.banner = { + expdir: expdir + }; + + const requestBody = testBuildRequests(clonedBannerRequests, baseBidderRequest).data; + expect(requestBody.imp[0].banner.expdir).to.equal(expdir); + }); + + it('sets keywords properly if sent', function () { + let clonedBannerRequests = deepClone(baseBannerBidRequests); + + config.setConfig({ortb2: { + site: { + keywords: 'highViewability, clothing, holiday shopping' + } + }}); + const requestBody = testBuildRequests(clonedBannerRequests, baseBidderRequest).data; + config.resetConfig(); + expect(requestBody.ext.ttdprebid.keywords).to.deep.equal(['highViewability', 'clothing', 'holiday shopping']); + }); + + it('sets ext properly', function () { + let clonedBannerRequests = deepClone(baseBannerBidRequests); + + const requestBody = testBuildRequests(clonedBannerRequests, baseBidderRequest).data; + expect(requestBody.ext.ttdprebid.pbjs).to.equal('$prebid.version$'); + }); + + it('adds gdpr consent info to the request', function () { + let consentString = 'BON3G4EON3G4EAAABAENAA____ABl____A'; + let clonedBidderRequest = deepClone(baseBidderRequest); + clonedBidderRequest.gdprConsent = { + consentString: consentString, + gdprApplies: true + }; + + const requestBody = testBuildRequests(baseBannerBidRequests, clonedBidderRequest).data; + expect(requestBody.user.ext.consent).to.equal(consentString); + expect(requestBody.regs.ext.gdpr).to.equal(1); + }); + + it('adds usp consent info to the request', function () { + let consentString = 'BON3G4EON3G4EAAABAENAA____ABl____A'; + let clonedBidderRequest = deepClone(baseBidderRequest); + clonedBidderRequest.uspConsent = consentString; + + const requestBody = testBuildRequests(baseBannerBidRequests, clonedBidderRequest).data; + expect(requestBody.regs.ext.us_privacy).to.equal(consentString); + }); + + it('adds coppa consent info to the request', function () { + let clonedBidderRequest = deepClone(baseBidderRequest); + + config.setConfig({coppa: true}); + const requestBody = testBuildRequests(baseBannerBidRequests, clonedBidderRequest).data; + config.resetConfig(); + expect(requestBody.regs.coppa).to.equal(1); + }); + + it('adds schain info to the request', function () { + const schain = { + 'ver': '1.0', + 'complete': 1, + 'nodes': [{ + 'asi': 'indirectseller.com', + 'sid': '00001', + 'hp': 1 + }, { + 'asi': 'indirectseller-2.com', + 'sid': '00002', + 'hp': 1 + }] + }; + let clonedBannerBidRequests = deepClone(baseBannerBidRequests); + clonedBannerBidRequests[0].schain = schain; + + const requestBody = testBuildRequests(clonedBannerBidRequests, baseBidderRequest).data; + expect(requestBody.source.ext.schain).to.deep.equal(schain); + }); + + it('adds unified ID info to the request', function () { + const TDID = '00000000-0000-0000-0000-000000000000'; + let clonedBannerRequests = deepClone(baseBannerBidRequests); + clonedBannerRequests[0].userId = { + tdid: TDID + }; + + const requestBody = testBuildRequests(clonedBannerRequests, baseBidderRequest).data; + expect(requestBody.user.buyeruid).to.equal(TDID); + }); + + it('adds unified ID and UID2 info to user.ext.eids in the request', function () { + const TDID = '00000000-0000-0000-0000-000000000000'; + const UID2 = '99999999-9999-9999-9999-999999999999'; + let clonedBannerRequests = deepClone(baseBannerBidRequests); + clonedBannerRequests[0].userId = { + tdid: TDID, + uid2: { + id: UID2 + } + }; + const expectedEids = [ + { + source: 'adserver.org', + uids: [ + { + atype: 1, + ext: { + rtiPartner: 'TDID' + }, + id: TDID + } + ] + }, + { + source: 'uidapi.com', + uids: [ + { + atype: 3, + id: UID2 + } + ] + } + ]; + + const requestBody = testBuildRequests(clonedBannerRequests, baseBidderRequest).data; + expect(requestBody.user.ext.eids).to.deep.equal(expectedEids); + }); + + it('adds first party site data to the request', function () { + let clonedBidderRequest = deepClone(baseBidderRequest); + + config.setConfig({ortb2: { + site: { + name: 'example', + domain: 'page.example.com', + cat: ['IAB2'], + sectioncat: ['IAB2-2'], + pagecat: ['IAB2-2'], + page: 'https://page.example.com/here.html', + ref: 'https://ref.example.com', + keywords: 'power tools, drills' + } + }}); + const requestBody = testBuildRequests(baseBannerBidRequests, clonedBidderRequest).data; + config.resetConfig(); + expect(requestBody.site.name).to.equal('example'); + expect(requestBody.site.domain).to.equal('page.example.com'); + expect(requestBody.site.cat[0]).to.equal('IAB2'); + expect(requestBody.site.sectioncat[0]).to.equal('IAB2-2'); + expect(requestBody.site.pagecat[0]).to.equal('IAB2-2'); + expect(requestBody.site.page).to.equal('https://page.example.com/here.html'); + expect(requestBody.site.ref).to.equal('https://ref.example.com'); + expect(requestBody.site.keywords).to.equal('power tools, drills'); + }); + }); + + describe('buildRequests-banner-multiple', function () { + const baseBannerMultipleBidRequests = [{ + 'bidder': 'ttd', + 'params': { + 'supplySourceId': 'supplier', + 'publisherId': '13144370', + 'placementId': 'bottom' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 250], [300, 600]] + } + }, + 'sizes': [[300, 250], [300, 600]], + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': '8651474f-58b1-4368-b812-84f8c937a099', + 'bidId': 'small', + 'bidderRequestId': '18084284054531', + 'auctionId': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'src': 'client', + 'bidRequestsCount': 1 + }, { + 'bidder': 'ttd', + 'params': { + 'publisherId': '13144370', + 'placementId': 'top', + 'siteId': 'testSite123' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[728, 90]] + } + }, + 'sizes': [[728, 90]], + 'adUnitCode': 'div-gpt-ad-91515710-0', + 'transactionId': '825c1228-ca8c-4657-b40f-2df500621527', + 'bidId': 'large', + 'bidderRequestId': '18084284054531', + 'auctionId': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'src': 'client', + 'bidRequestsCount': 1 + }]; + + const baseBidderRequest = { + 'bidderCode': 'ttd', + 'auctionId': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'bidderRequestId': '18084284054531', + 'auctionStart': 1540945362095, + 'timeout': 3000, + 'refererInfo': { + 'referer': 'https://www.test.com', + 'reachedTop': true, + 'numIframes': 0, + 'stack': [ + 'https://www.test.com' + ] + }, + 'start': 1540945362099, + 'doneCbCallCount': 0 + }; + + it('sends multiple impressions', function () { + const requestBody = testBuildRequests(baseBannerMultipleBidRequests, baseBidderRequest).data; + expect(requestBody.imp.length).to.equal(2); + }); + + it('sends the right tag ids for each ad unit', function () { + const requestBody = testBuildRequests(baseBannerMultipleBidRequests, baseBidderRequest).data; + requestBody.imp.forEach(imp => { + if (imp.id === 'small') { + expect(imp.tagid).to.equal('bottom'); + } else if (imp.id === 'large') { + expect(imp.tagid).to.equal('top'); + } else { + assert.fail('no matching impression id found'); + } + }); + }); + + it('sends the sizes for each ad unit', function () { + const requestBody = testBuildRequests(baseBannerMultipleBidRequests, baseBidderRequest).data; + requestBody.imp.forEach(imp => { + if (imp.id === 'small') { + expect(imp.banner.format[0].w).to.equal(300); + expect(imp.banner.format[0].h).to.equal(250); + expect(imp.banner.format[1].w).to.equal(300); + expect(imp.banner.format[1].h).to.equal(600); + } else if (imp.id === 'large') { + expect(imp.banner.format[0].w).to.equal(728); + expect(imp.banner.format[0].h).to.equal(90); + } else { + assert.fail('no matching impression id found'); + } + }); + }); + }); + + describe('buildRequests-display-video-multiformat', function () { + const baseMultiformatBidRequests = [{ + 'bidder': 'ttd', + 'params': { + 'supplySourceId': 'supplier', + 'publisherId': '13144370', + 'placementId': '1gaa015' + }, + 'mediaTypes': { + 'video': { + 'playerSize': [640, 480], + 'api': [1, 3], + 'mimes': ['video/mp4'], + 'protocols': [2, 3, 5, 6], + 'minduration': 5, + 'maxduration': 30 + }, + 'banner': { + 'sizes': [[300, 250], [300, 600]] + } + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': '8651474f-58b1-4368-b812-84f8c937a099', + 'bidId': '243310435309b5', + 'bidderRequestId': '18084284054531', + 'auctionId': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'src': 'client', + 'bidRequestsCount': 1 + }]; + + const baseBidderRequest = { + 'bidderCode': 'ttd', + 'auctionId': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'bidderRequestId': '18084284054531', + 'auctionStart': 1540945362095, + 'timeout': 3000, + 'refererInfo': { + 'referer': 'https://www.example.com/test', + 'reachedTop': true, + 'numIframes': 0, + 'stack': [ + 'https://www.example.com/test' + ] + }, + 'start': 1540945362099, + 'doneCbCallCount': 0 + }; + + it('includes the video ad size in the bid request', function () { + const requestBody = testBuildRequests(baseMultiformatBidRequests, baseBidderRequest).data; + expect(requestBody.imp[0].video.w).to.equal(640); + expect(requestBody.imp[0].video.h).to.equal(480); + }); + + it('includes the banner ad size in the bid request', function () { + const requestBody = testBuildRequests(baseMultiformatBidRequests, baseBidderRequest).data; + expect(requestBody.imp[0].banner.format[0].w).to.equal(300); + expect(requestBody.imp[0].banner.format[0].h).to.equal(250); + expect(requestBody.imp[0].banner.format[1].w).to.equal(300); + expect(requestBody.imp[0].banner.format[1].h).to.equal(600); + }); + }); + + describe('buildRequests-video', function () { + const baseVideoBidRequests = [{ + 'bidder': 'ttd', + 'params': { + 'supplySourceId': 'supplier', + 'publisherId': '13144370', + 'placementId': '1gaa015' + }, + 'mediaTypes': { + 'video': { + 'playerSize': [640, 480], + 'api': [1, 3], + 'mimes': ['video/mp4'], + 'protocols': [2, 3, 5, 6], + 'minduration': 5, + 'maxduration': 30 + } + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': '8651474f-58b1-4368-b812-84f8c937a099', + 'bidId': '243310435309b5', + 'bidderRequestId': '18084284054531', + 'auctionId': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'src': 'client', + 'bidRequestsCount': 1 + }]; + + const baseBidderRequest = { + 'bidderCode': 'ttd', + 'auctionId': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'bidderRequestId': '18084284054531', + 'auctionStart': 1540945362095, + 'timeout': 3000, + 'refererInfo': { + 'referer': 'https://www.example.com/test', + 'reachedTop': true, + 'numIframes': 0, + 'stack': [ + 'https://www.example.com/test' + ] + }, + 'start': 1540945362099, + 'doneCbCallCount': 0 + }; + + it('includes the ad size in the bid request', function () { + const requestBody = testBuildRequests(baseVideoBidRequests, baseBidderRequest).data; + expect(requestBody.imp[0].video.w).to.equal(640); + expect(requestBody.imp[0].video.h).to.equal(480); + }); + + it('includes the mimes in the bid request', function () { + const requestBody = testBuildRequests(baseVideoBidRequests, baseBidderRequest).data; + expect(requestBody.imp[0].video.mimes[0]).to.equal('video/mp4'); + }); + + it('includes the min and max duration in the bid request', function () { + const requestBody = testBuildRequests(baseVideoBidRequests, baseBidderRequest).data; + expect(requestBody.imp[0].video.minduration).to.equal(5); + expect(requestBody.imp[0].video.maxduration).to.equal(30); + }); + + it('sets the minduration to 0 if missing', function () { + let clonedVideoRequests = deepClone(baseVideoBidRequests); + delete clonedVideoRequests[0].mediaTypes.video.minduration + + const requestBody = testBuildRequests(clonedVideoRequests, baseBidderRequest).data; + expect(requestBody.imp[0].video.minduration).to.equal(0); + }); + + it('includes the api frameworks in the bid request', function () { + const requestBody = testBuildRequests(baseVideoBidRequests, baseBidderRequest).data; + expect(requestBody.imp[0].video.api[0]).to.equal(1); + expect(requestBody.imp[0].video.api[1]).to.equal(3); + }); + + it('includes the protocols in the bid request', function () { + const requestBody = testBuildRequests(baseVideoBidRequests, baseBidderRequest).data; + expect(requestBody.imp[0].video.protocols[0]).to.equal(2); + expect(requestBody.imp[0].video.protocols[1]).to.equal(3); + expect(requestBody.imp[0].video.protocols[2]).to.equal(5); + expect(requestBody.imp[0].video.protocols[3]).to.equal(6); + }); + + it('sets skip correctly if sent', function () { + let clonedVideoRequests = deepClone(baseVideoBidRequests); + clonedVideoRequests[0].mediaTypes.video.skip = 1; + clonedVideoRequests[0].mediaTypes.video.skipmin = 5; + clonedVideoRequests[0].mediaTypes.video.skipafter = 10; + + const requestBody = testBuildRequests(clonedVideoRequests, baseBidderRequest).data; + expect(requestBody.imp[0].video.skip).to.equal(1); + expect(requestBody.imp[0].video.skipmin).to.equal(5); + expect(requestBody.imp[0].video.skipafter).to.equal(10); + }); + + it('sets bitrate correctly if sent', function () { + let clonedVideoRequests = deepClone(baseVideoBidRequests); + clonedVideoRequests[0].mediaTypes.video.minbitrate = 100; + clonedVideoRequests[0].mediaTypes.video.maxbitrate = 500; + + const requestBody = testBuildRequests(clonedVideoRequests, baseBidderRequest).data; + expect(requestBody.imp[0].video.minbitrate).to.equal(100); + expect(requestBody.imp[0].video.maxbitrate).to.equal(500); + }); + + it('sets pos correctly if sent', function () { + let clonedVideoRequests = deepClone(baseVideoBidRequests); + clonedVideoRequests[0].mediaTypes.video.pos = 1; + + const requestBody = testBuildRequests(clonedVideoRequests, baseBidderRequest).data; + expect(requestBody.imp[0].video.pos).to.equal(1); + }); + + it('sets playbackmethod correctly if sent', function () { + let clonedVideoRequests = deepClone(baseVideoBidRequests); + clonedVideoRequests[0].mediaTypes.video.playbackmethod = [1]; + + const requestBody = testBuildRequests(clonedVideoRequests, baseBidderRequest).data; + expect(requestBody.imp[0].video.playbackmethod[0]).to.equal(1); + }); + + it('sets startdelay correctly if sent', function () { + let clonedVideoRequests = deepClone(baseVideoBidRequests); + clonedVideoRequests[0].mediaTypes.video.startdelay = -1; + + const requestBody = testBuildRequests(clonedVideoRequests, baseBidderRequest).data; + expect(requestBody.imp[0].video.startdelay).to.equal(-1); + }); + + it('sets placement correctly if sent', function () { + let clonedVideoRequests = deepClone(baseVideoBidRequests); + clonedVideoRequests[0].mediaTypes.video.placement = 3; + + const requestBody = testBuildRequests(clonedVideoRequests, baseBidderRequest).data; + expect(requestBody.imp[0].video.placement).to.equal(3); + }); + }); + + describe('interpretResponse-empty', function () { + it('should handle empty response', function () { + let result = spec.interpretResponse({}); + expect(result.length).to.equal(0); + }); + + it('should handle empty seatbid response', function () { + let response = { + body: { + 'id': '5e5c23a5ba71e78', + 'seatbid': [] + } + }; + let result = spec.interpretResponse(response); + expect(result.length).to.equal(0); + }); + }); + + describe('interpretResponse-simple-display', function () { + const incoming = { + body: { + 'id': '5e5c23a5ba71e78', + 'seatbid': [ + { + 'bid': [ + { + 'id': '6vmb3isptf', + 'crid': 'ttdscreative', + 'impid': '322add653672f68', + 'price': 1.22, + 'adm': '', + 'cat': [], + 'h': 90, + 'w': 728, + 'ttl': 60, + 'dealid': 'ttd-dealid-1', + 'adomain': ['advertiser.com'], + 'ext': { + 'mediatype': 1 + } + } + ], + 'seat': 'MOCK' + } + ], + 'cur': 'EUR', + 'bidid': '5e5c23a5ba71e78' + } + }; + + const serverRequest = { + 'method': 'POST', + 'url': 'https://direct.adsrvr.org/bid/bidder/supplier', + 'data': { + 'id': 'c47237df-c108-419f-9c2b-da513dc3c133', + 'imp': [ + { + 'id': '322add653672f68', + 'tagid': 'simple', + 'banner': { + 'w': 728, + 'h': 90, + 'format': [ + { + 'w': 728, + 'h': 90 + } + ] + } + } + ], + 'site': { + 'page': 'http://www.test.com', + 'publisher': { + 'id': '111' + } + }, + 'device': { + 'ua': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36', + 'dnt': 0, + 'language': 'en-US', + 'connectiontype': 0 + }, + 'user': {}, + 'at': 1, + 'cur': [ + 'USD' + ], + 'regs': {}, + 'ext': { + 'ttdprebid': { + 'ver': 'TTD-PREBID-2019.11.12', + 'pbjs': '2.31.0' + } + } + }, + 'options': { + 'withCredentials': true + } + }; + + const expectedBid = { + 'requestId': '322add653672f68', + 'cpm': 1.22, + 'width': 728, + 'height': 90, + 'creativeId': 'ttdscreative', + 'dealId': 'ttd-dealid-1', + 'currency': 'EUR', + 'netRevenue': true, + 'ttl': 60, + 'ad': '', + 'mediaType': 'banner', + 'meta': { + 'advertiserDomains': ['advertiser.com'] + } + }; + + it('should get the correct bid response', function () { + let result = spec.interpretResponse(incoming, serverRequest); + expect(result.length).to.equal(1); + expect(result[0]).to.deep.equal(expectedBid); + }); + }); + + describe('interpretResponse-multiple-display', function () { + const incoming = { + 'body': { + 'id': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'seatbid': [ + { + 'bid': [ + { + 'id': 'small', + 'impid': 'small', + 'price': 4.25, + 'adm': 'Default Test Ad Tag', + 'cid': 'campaignId132', + 'crid': 'creativeId999', + 'adomain': [ + 'http://foo' + ], + 'dealid': null, + 'w': 300, + 'h': 600, + 'cat': [], + 'ext': { + 'mediatype': 1 + } + }, + { + 'id': 'large', + 'impid': 'large', + 'price': 5.25, + 'adm': 'Default Test Ad Tag', + 'cid': 'campaignId132', + 'crid': 'creativeId222', + 'adomain': [ + 'http://foo2' + ], + 'dealid': null, + 'w': 728, + 'h': 90, + 'cat': [], + 'ext': { + 'mediatype': 1 + } + } + ], + 'seat': 'supplyVendorBuyerId132' + } + ], + 'cur': 'USD' + } + }; + + const expectedBids = [ + { + 'requestId': 'small', + 'cpm': 4.25, + 'width': 300, + 'height': 600, + 'creativeId': 'creativeId999', + 'currency': 'USD', + 'dealId': null, + 'netRevenue': true, + 'ttl': 360, + 'ad': 'Default Test Ad Tag', + 'mediaType': 'banner', + 'meta': { + 'advertiserDomains': ['http://foo'] + } + }, + { + 'requestId': 'large', + 'cpm': 5.25, + 'width': 728, + 'height': 90, + 'creativeId': 'creativeId222', + 'currency': 'USD', + 'dealId': null, + 'netRevenue': true, + 'ttl': 360, + 'ad': 'Default Test Ad Tag', + 'mediaType': 'banner', + 'meta': { + 'advertiserDomains': ['http://foo2'] + } + } + ]; + + const serverRequest = { + 'method': 'POST', + 'url': 'https://direct.adsrvr.org/bid/bidder/supplier', + 'data': { + 'id': 'c47237df-c108-419f-9c2b-da513dc3c133', + 'imp': [ + { + 'id': 'small', + 'tagid': 'test1', + 'banner': { + 'w': 300, + 'h': 600, + 'format': [ + { + 'w': 300, + 'h': 600 + } + ] + } + }, + { + 'id': 'large', + 'tagid': 'test2', + 'banner': { + 'w': 728, + 'h': 90, + 'format': [ + { + 'w': 728, + 'h': 90 + } + ] + } + } + ], + 'site': { + 'page': 'http://www.test.com', + 'publisher': { + 'id': '111' + } + }, + 'device': { + 'ua': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36', + 'dnt': 0, + 'language': 'en-US', + 'connectiontype': 0 + }, + 'user': {}, + 'at': 1, + 'cur': [ + 'USD' + ], + 'regs': {}, + 'ext': { + 'ttdprebid': { + 'ver': 'TTD-PREBID-2019.11.12', + 'pbjs': '2.31.0' + } + } + }, + 'options': { + 'withCredentials': true + } + }; + + it('should get the correct bid response', function () { + let result = spec.interpretResponse(incoming, serverRequest); + expect(result.length).to.equal(2); + expect(result).to.deep.equal(expectedBids); + }); + }); + + describe('interpretResponse-simple-video', function () { + const incoming = { + 'body': { + 'cur': 'USD', + 'seatbid': [ + { + 'bid': [ + { + 'crid': 'mokivv6m', + 'ext': { + 'advid': '7ieo6xk', + 'agid': '7q9n3s2', + 'deal': { + 'dealid': '7013542' + }, + 'imptrackers': [], + 'viewabilityvendors': [], + 'mediatype': 2 + }, + 'h': 480, + 'impid': '2eabb87dfbcae4', + 'nurl': 'https://insight.adsrvr.org/enduser/vast?iid=00000000-0000-0000-0000-000000000000&crid=v3pek2eh&wp=${AUCTION_PRICE}&aid=&wpc=&sfe=0&puid=&tdid=00000000-0000-0000-0000-000000000000&pid=&ag=&adv=&sig=AAAAAAAAAAAAAA.&cf=&fq=0&td_s=&rcats=&mcat=&mste=&mfld=4&mssi=&mfsi=&uhow=&agsa=&rgco=&rgre=&rgme=&rgci=&rgz=&svbttd=0&dt=&osf=&os=&br=&rlangs=en&mlang=en&svpid=&did=&rcxt=&lat=&lon=&tmpc=&daid=&vp=0&osi=&osv=&dc=0&vcc=QAFIAVABiAECwAEDyAED0AED6AEG8AEBgAIDigIMCAIIBQgDCAYICwgMmgIECAEIAqACA6gCAsACAA..&sv=noop&pidi=&advi=&cmpi=&agi=&cridi=&svi=&cmp=&skip=1&c=&dur=&crrelr=', + 'price': 13.6, + 'ttl': 500, + 'w': 600 + } + ], + 'seat': 'supplyVendorBuyerId132' + } + ] + }, + 'headers': {} + }; + + const serverRequest = { + 'method': 'POST', + 'url': 'https://direct.bid.adsrvr.org/bid/bidder/supplier', + 'data': { + 'id': 'e94ec12d-ae1d-4ed7-abd1-eb3198ce3b63', + 'imp': [ + { + 'id': '2eabb87dfbcae4', + 'tagid': 'video', + 'video': { + 'api': [ + 1, + 3 + ], + 'mimes': [ + 'video/mp4' + ], + 'minduration': 5, + 'maxduration': 30, + 'w': 640, + 'h': 480, + 'protocols': [ + 2, + 3, + 5, + 6 + ] + } + } + ], + 'site': { + 'page': 'http://www.test.com', + 'publisher': { + 'id': '111' + } + }, + 'device': { + 'ua': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36', + 'dnt': 0, + 'language': 'en-US', + 'connectiontype': 0 + }, + 'user': {}, + 'at': 1, + 'cur': [ + 'USD' + ], + 'regs': {}, + 'ext': { + 'ttdprebid': { + 'ver': 'TTD-PREBID-2019.11.12', + 'pbjs': '3.10.0' + } + } + }, + 'options': { + 'withCredentials': true + } + }; + + const expectedBid = + { + 'requestId': '2eabb87dfbcae4', + 'cpm': 13.6, + 'creativeId': 'mokivv6m', + 'dealId': null, + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 500, + 'width': 640, + 'height': 480, + 'mediaType': 'video', + 'vastUrl': 'https://insight.adsrvr.org/enduser/vast?iid=00000000-0000-0000-0000-000000000000&crid=v3pek2eh&wp=13.6&aid=&wpc=&sfe=0&puid=&tdid=00000000-0000-0000-0000-000000000000&pid=&ag=&adv=&sig=AAAAAAAAAAAAAA.&cf=&fq=0&td_s=&rcats=&mcat=&mste=&mfld=4&mssi=&mfsi=&uhow=&agsa=&rgco=&rgre=&rgme=&rgci=&rgz=&svbttd=0&dt=&osf=&os=&br=&rlangs=en&mlang=en&svpid=&did=&rcxt=&lat=&lon=&tmpc=&daid=&vp=0&osi=&osv=&dc=0&vcc=QAFIAVABiAECwAEDyAED0AED6AEG8AEBgAIDigIMCAIIBQgDCAYICwgMmgIECAEIAqACA6gCAsACAA..&sv=noop&pidi=&advi=&cmpi=&agi=&cridi=&svi=&cmp=&skip=1&c=&dur=&crrelr=', + 'meta': {} + }; + + it('should get the correct bid response if nurl is returned', function () { + let result = spec.interpretResponse(incoming, serverRequest); + expect(result.length).to.equal(1); + expect(result[0]).to.deep.equal(expectedBid); + }); + + it('should get the correct bid response if adm is returned', function () { + const vastXml = "2.0574840600:00:30 \"Click ]]>"; + let admIncoming = deepClone(incoming); + delete admIncoming.body.seatbid[0].bid[0].nurl; + admIncoming.body.seatbid[0].bid[0].adm = vastXml; + + let vastXmlExpectedBid = deepClone(expectedBid); + delete vastXmlExpectedBid.vastUrl; + vastXmlExpectedBid.vastXml = vastXml; + + let result = spec.interpretResponse(admIncoming, serverRequest); + expect(result.length).to.equal(1); + expect(result[0]).to.deep.equal(vastXmlExpectedBid); + }); + }); + + describe('interpretResponse-display-and-video', function () { + const incoming = { + 'body': { + 'id': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'seatbid': [ + { + 'bid': [ + { + 'id': 'small', + 'impid': 'small', + 'price': 4.25, + 'adm': 'Default Test Ad Tag', + 'cid': 'campaignId132', + 'crid': 'creativeId999', + 'adomain': [ + 'http://foo' + ], + 'dealid': null, + 'w': 300, + 'h': 600, + 'cat': [], + 'ext': { + 'mediatype': 1 + } + }, + { + 'crid': 'mokivv6m', + 'ext': { + 'advid': '7ieo6xk', + 'agid': '7q9n3s2', + 'deal': { + 'dealid': '7013542' + }, + 'imptrackers': [], + 'viewabilityvendors': [], + 'mediatype': 2 + }, + 'h': 480, + 'impid': '2eabb87dfbcae4', + 'nurl': 'https://insight.adsrvr.org/enduser/vast?iid=00000000-0000-0000-0000-000000000000&crid=v3pek2eh&wp=${AUCTION_PRICE}&aid=&wpc=&sfe=0&puid=&tdid=00000000-0000-0000-0000-000000000000&pid=&ag=&adv=&sig=AAAAAAAAAAAAAA.&cf=&fq=0&td_s=&rcats=&mcat=&mste=&mfld=4&mssi=&mfsi=&uhow=&agsa=&rgco=&rgre=&rgme=&rgci=&rgz=&svbttd=0&dt=&osf=&os=&br=&rlangs=en&mlang=en&svpid=&did=&rcxt=&lat=&lon=&tmpc=&daid=&vp=0&osi=&osv=&dc=0&vcc=QAFIAVABiAECwAEDyAED0AED6AEG8AEBgAIDigIMCAIIBQgDCAYICwgMmgIECAEIAqACA6gCAsACAA..&sv=noop&pidi=&advi=&cmpi=&agi=&cridi=&svi=&cmp=&skip=1&c=&dur=&crrelr=', + 'price': 13.6, + 'ttl': 500, + 'w': 600 + } + ], + 'seat': 'supplyVendorBuyerId132' + } + ], + 'cur': 'USD' + } + }; + + const expectedBids = [ + { + 'requestId': 'small', + 'cpm': 4.25, + 'width': 300, + 'height': 600, + 'creativeId': 'creativeId999', + 'currency': 'USD', + 'dealId': null, + 'netRevenue': true, + 'ttl': 360, + 'ad': 'Default Test Ad Tag', + 'mediaType': 'banner', + 'meta': { + 'advertiserDomains': ['http://foo'] + } + }, + { + 'requestId': '2eabb87dfbcae4', + 'cpm': 13.6, + 'creativeId': 'mokivv6m', + 'dealId': null, + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 500, + 'width': 640, + 'height': 480, + 'mediaType': 'video', + 'vastUrl': 'https://insight.adsrvr.org/enduser/vast?iid=00000000-0000-0000-0000-000000000000&crid=v3pek2eh&wp=13.6&aid=&wpc=&sfe=0&puid=&tdid=00000000-0000-0000-0000-000000000000&pid=&ag=&adv=&sig=AAAAAAAAAAAAAA.&cf=&fq=0&td_s=&rcats=&mcat=&mste=&mfld=4&mssi=&mfsi=&uhow=&agsa=&rgco=&rgre=&rgme=&rgci=&rgz=&svbttd=0&dt=&osf=&os=&br=&rlangs=en&mlang=en&svpid=&did=&rcxt=&lat=&lon=&tmpc=&daid=&vp=0&osi=&osv=&dc=0&vcc=QAFIAVABiAECwAEDyAED0AED6AEG8AEBgAIDigIMCAIIBQgDCAYICwgMmgIECAEIAqACA6gCAsACAA..&sv=noop&pidi=&advi=&cmpi=&agi=&cridi=&svi=&cmp=&skip=1&c=&dur=&crrelr=', + 'meta': {} + } + ]; + + const serverRequest = { + 'method': 'POST', + 'url': 'https://direct.adsrvr.org/bid/bidder/supplier', + 'data': { + 'id': 'c47237df-c108-419f-9c2b-da513dc3c133', + 'imp': [ + { + 'id': 'small', + 'tagid': 'test1', + 'banner': { + 'w': 300, + 'h': 600, + 'format': [ + { + 'w': 300, + 'h': 600 + } + ] + }, + }, + { + 'id': '2eabb87dfbcae4', + 'tagid': 'video', + 'video': { + 'api': [ + 1, + 3 + ], + 'mimes': [ + 'video/mp4' + ], + 'minduration': 5, + 'maxduration': 30, + 'w': 640, + 'h': 480, + 'protocols': [ + 2, + 3, + 5, + 6 + ] + } + } + ], + 'site': { + 'page': 'http://www.test.com', + 'publisher': { + 'id': '111' + } + }, + 'device': { + 'ua': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36', + 'dnt': 0, + 'language': 'en-US', + 'connectiontype': 0 + }, + 'user': {}, + 'at': 1, + 'cur': [ + 'USD' + ], + 'regs': {}, + 'ext': { + 'ttdprebid': { + 'ver': 'TTD-PREBID-2019.11.12', + 'pbjs': '2.31.0' + } + } + }, + 'options': { + 'withCredentials': true + } + }; + + it('should get the correct bid response', function () { + let result = spec.interpretResponse(incoming, serverRequest); + expect(result.length).to.equal(2); + expect(result).to.deep.equal(expectedBids); + }); + }); +}); diff --git a/test/spec/modules/undertoneBidAdapter_spec.js b/test/spec/modules/undertoneBidAdapter_spec.js index 56217fe3561..c24f63c0b99 100644 --- a/test/spec/modules/undertoneBidAdapter_spec.js +++ b/test/spec/modules/undertoneBidAdapter_spec.js @@ -1,5 +1,7 @@ -import { expect } from 'chai'; -import { spec } from 'modules/undertoneBidAdapter.js'; +import {expect} from 'chai'; +import {spec} from 'modules/undertoneBidAdapter.js'; +import {BANNER, VIDEO} from '../../../src/mediaTypes'; +import {deepClone} from '../../../src/utils'; const URL = 'https://hb.undertone.com/hb'; const BIDDER_CODE = 'undertone'; @@ -276,6 +278,57 @@ describe('Undertone Adapter', () => { sandbox.restore(); }); + describe('getFloor', function () { + it('should send 0 floor when getFloor is undefined', function() { + const request = spec.buildRequests(videoBidReq, bidderReq); + const bidReq = JSON.parse(request.data)['x-ut-hb-params'][0]; + expect(bidReq.mediaType).to.deep.equal(VIDEO); + expect(bidReq.bidfloor).to.deep.equal(0); + }); + it('should send mocked floor when defined on video media-type', function() { + const clonedVideoBidReqArr = deepClone(videoBidReq); + const mockedFloorResponse = { + currency: 'USD', + floor: 2.3 + }; + clonedVideoBidReqArr[1].getFloor = () => mockedFloorResponse; + + const request = spec.buildRequests(clonedVideoBidReqArr, bidderReq); + const bidReq1 = JSON.parse(request.data)['x-ut-hb-params'][0]; + const bidReq2 = JSON.parse(request.data)['x-ut-hb-params'][1]; + expect(bidReq1.mediaType).to.deep.equal(VIDEO); + expect(bidReq1.bidfloor).to.deep.equal(0); + + expect(bidReq2.mediaType).to.deep.equal(VIDEO); + expect(bidReq2.bidfloor).to.deep.equal(mockedFloorResponse.floor); + }); + it('should send mocked floor on banner media-type', function() { + const clonedValidBidReqArr = [deepClone(validBidReq)]; + const mockedFloorResponse = { + currency: 'USD', + floor: 2.3 + }; + clonedValidBidReqArr[0].getFloor = () => mockedFloorResponse; + + const request = spec.buildRequests(clonedValidBidReqArr, bidderReq); + const bidReq = JSON.parse(request.data)['x-ut-hb-params'][0]; + expect(bidReq.mediaType).to.deep.equal(BANNER); + expect(bidReq.bidfloor).to.deep.equal(mockedFloorResponse.floor); + }); + it('should send 0 floor on invalid currency', function() { + const clonedValidBidReqArr = [deepClone(validBidReq)]; + const mockedFloorResponse = { + currency: 'EUR', + floor: 2.3 + }; + clonedValidBidReqArr[0].getFloor = () => mockedFloorResponse; + + const request = spec.buildRequests(clonedValidBidReqArr, bidderReq); + const bidReq = JSON.parse(request.data)['x-ut-hb-params'][0]; + expect(bidReq.mediaType).to.deep.equal(BANNER); + expect(bidReq.bidfloor).to.deep.equal(0); + }); + }); describe('supply chain', function () { it('should send supply chain if found on first bid', function () { const request = spec.buildRequests(supplyChainedBidReqs, bidderReq); diff --git a/test/spec/modules/userId_spec.js b/test/spec/modules/userId_spec.js index 0190bceca70..63107aa5107 100644 --- a/test/spec/modules/userId_spec.js +++ b/test/spec/modules/userId_spec.js @@ -14,7 +14,7 @@ import { import {createEidsArray} from 'modules/userId/eids.js'; import {config} from 'src/config.js'; import * as utils from 'src/utils.js'; -import events from 'src/events.js'; +import * as events from 'src/events.js'; import CONSTANTS from 'src/constants.json'; import {getGlobal} from 'src/prebidGlobal.js'; import { @@ -23,7 +23,7 @@ import { setConsentConfig } from 'modules/consentManagement.js'; import {server} from 'test/mocks/xhr.js'; -import find from 'core-js-pure/features/array/find.js'; +import {find} from 'src/polyfill.js'; import {unifiedIdSubmodule} from 'modules/unifiedIdSystem.js'; import {britepoolIdSubmodule} from 'modules/britepoolIdSystem.js'; import {id5IdSubmodule} from 'modules/id5IdSystem.js'; @@ -36,7 +36,7 @@ import {nextrollIdSubmodule} from 'modules/nextrollIdSystem.js'; import {intentIqIdSubmodule} from 'modules/intentIqIdSystem.js'; import {zeotapIdPlusSubmodule} from 'modules/zeotapIdPlusIdSystem.js'; import {sharedIdSystemSubmodule} from 'modules/sharedIdSystem.js'; -import {haloIdSubmodule} from 'modules/haloIdSystem.js'; +import {hadronIdSubmodule} from 'modules/hadronIdSystem.js'; import {pubProvidedIdSubmodule} from 'modules/pubProvidedIdSystem.js'; import {criteoIdSubmodule} from 'modules/criteoIdSystem.js'; import {mwOpenLinkIdSubModule} from 'modules/mwOpenLinkIdSystem.js'; @@ -49,6 +49,8 @@ import {flocIdSubmodule} from 'modules/flocIdSystem.js' import {amxIdSubmodule} from '../../../modules/amxIdSystem.js'; import {akamaiDAPIdSubmodule} from 'modules/akamaiDAPIdSystem.js' import {kinessoIdSubmodule} from 'modules/kinessoIdSystem.js' +import {adqueryIdSubmodule} from 'modules/adqueryIdSystem.js'; +import * as mockGpt from '../integration/faker/googletag.js'; let assert = require('chai').assert; let expect = require('chai').expect; @@ -114,15 +116,21 @@ describe('User ID', function () { describe('Decorate Ad Units', function () { beforeEach(function () { + // reset mockGpt so nothing else interferes + mockGpt.disable(); + mockGpt.enable(); coreStorage.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('pubcid_alt', 'altpubcid200000', (new Date(Date.now() + 5000).toUTCString())); sinon.spy(coreStorage, 'setCookie'); + sinon.stub(utils, 'logWarn'); }); afterEach(function () { + mockGpt.enable(); $$PREBID_GLOBAL$$.requestBids.removeAll(); config.resetConfig(); coreStorage.setCookie.restore(); + utils.logWarn.restore(); }); after(function () { @@ -321,6 +329,50 @@ describe('User ID', function () { expect((getGlobal()).getUserIdsAsEids()).to.deep.equal(createEidsArray((getGlobal()).getUserIds())); }); + it('should set googletag ppid correctly', function () { + let adUnits = [getAdUnitMock()]; + setSubmoduleRegistry([amxIdSubmodule, sharedIdSystemSubmodule, identityLinkSubmodule]); + init(config); + + config.setConfig({ + userSync: { + ppid: 'pubcid.org', + userIds: [ + { name: 'amxId', value: {'amxId': 'amx-id-value-amx-id-value-amx-id-value'} }, + { name: 'pubCommonId', value: {'pubcid': 'pubCommon-id-value-pubCommon-id-value'} }, + { name: 'identityLink', value: {'idl_env': 'identityLink-id-value-identityLink-id-value'} }, + ] + } + }); + // before ppid should not be set + expect(window.googletag._ppid).to.equal(undefined); + requestBidsHook(() => {}, {adUnits}); + // ppid should have been set without dashes and stuff + expect(window.googletag._ppid).to.equal('pubCommonidvaluepubCommonidvalue'); + }); + + it('should log a warning if PPID too big or small', function () { + let adUnits = [getAdUnitMock()]; + setSubmoduleRegistry([sharedIdSystemSubmodule]); + init(config); + + config.setConfig({ + userSync: { + ppid: 'pubcid.org', + userIds: [ + { name: 'pubCommonId', value: {'pubcid': 'pubcommonIdValue'} }, + ] + } + }); + // before ppid should not be set + expect(window.googletag._ppid).to.equal(undefined); + requestBidsHook(() => {}, {adUnits}); + // ppid should NOT have been set + expect(window.googletag._ppid).to.equal(undefined); + // a warning should have been emmited + expect(utils.logWarn.args[0][0]).to.exist.and.to.contain('User ID - Googletag Publisher Provided ID for pubcid.org is not between 32 and 150 characters - pubcommonIdValue'); + }); + it('pbjs.refreshUserIds refreshes', function() { let sandbox = sinon.createSandbox(); @@ -508,7 +560,7 @@ describe('User ID', function () { }); it('handles config with no usersync object', function () { - setSubmoduleRegistry([sharedIdSystemSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, mwOpenLinkIdSubModule, tapadIdSubmodule, uid2IdSubmodule, admixerIdSubmodule, deepintentDpesSubmodule, flocIdSubmodule, akamaiDAPIdSubmodule, amxIdSubmodule, kinessoIdSubmodule]); + setSubmoduleRegistry([sharedIdSystemSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, mwOpenLinkIdSubModule, tapadIdSubmodule, uid2IdSubmodule, admixerIdSubmodule, deepintentDpesSubmodule, flocIdSubmodule, akamaiDAPIdSubmodule, amxIdSubmodule, kinessoIdSubmodule, adqueryIdSubmodule]); init(config); config.setConfig({}); // usersync is undefined, and no logInfo message for 'User ID - usersync config updated' @@ -516,14 +568,14 @@ describe('User ID', function () { }); it('handles config with empty usersync object', function () { - setSubmoduleRegistry([sharedIdSystemSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, mwOpenLinkIdSubModule, tapadIdSubmodule, uid2IdSubmodule, admixerIdSubmodule, deepintentDpesSubmodule, flocIdSubmodule, akamaiDAPIdSubmodule, amxIdSubmodule, kinessoIdSubmodule]); + setSubmoduleRegistry([sharedIdSystemSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, mwOpenLinkIdSubModule, tapadIdSubmodule, uid2IdSubmodule, admixerIdSubmodule, deepintentDpesSubmodule, flocIdSubmodule, akamaiDAPIdSubmodule, amxIdSubmodule, kinessoIdSubmodule, adqueryIdSubmodule]); init(config); config.setConfig({userSync: {}}); expect(typeof utils.logInfo.args[0]).to.equal('undefined'); }); it('handles config with usersync and userIds that are empty objs', function () { - setSubmoduleRegistry([sharedIdSystemSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, nextrollIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, mwOpenLinkIdSubModule, tapadIdSubmodule, uid2IdSubmodule, admixerIdSubmodule, deepintentDpesSubmodule, dmdIdSubmodule, flocIdSubmodule, akamaiDAPIdSubmodule, amxIdSubmodule, kinessoIdSubmodule]); + setSubmoduleRegistry([sharedIdSystemSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, nextrollIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, mwOpenLinkIdSubModule, tapadIdSubmodule, uid2IdSubmodule, admixerIdSubmodule, deepintentDpesSubmodule, dmdIdSubmodule, flocIdSubmodule, akamaiDAPIdSubmodule, amxIdSubmodule, kinessoIdSubmodule, adqueryIdSubmodule]); init(config); config.setConfig({ userSync: { @@ -534,7 +586,7 @@ describe('User ID', function () { }); it('handles config with usersync and userIds with empty names or that dont match a submodule.name', function () { - setSubmoduleRegistry([sharedIdSystemSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, nextrollIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, mwOpenLinkIdSubModule, tapadIdSubmodule, uid2IdSubmodule, admixerIdSubmodule, deepintentDpesSubmodule, dmdIdSubmodule, flocIdSubmodule, akamaiDAPIdSubmodule, amxIdSubmodule, kinessoIdSubmodule]); + setSubmoduleRegistry([sharedIdSystemSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, nextrollIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, mwOpenLinkIdSubModule, tapadIdSubmodule, uid2IdSubmodule, admixerIdSubmodule, deepintentDpesSubmodule, dmdIdSubmodule, flocIdSubmodule, akamaiDAPIdSubmodule, amxIdSubmodule, kinessoIdSubmodule, adqueryIdSubmodule]); init(config); config.setConfig({ userSync: { @@ -551,7 +603,7 @@ describe('User ID', function () { }); it('config with 1 configurations should create 1 submodules', function () { - setSubmoduleRegistry([sharedIdSystemSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, nextrollIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, mwOpenLinkIdSubModule, tapadIdSubmodule, uid2IdSubmodule, admixerIdSubmodule, deepintentDpesSubmodule, flocIdSubmodule, akamaiDAPIdSubmodule, amxIdSubmodule, kinessoIdSubmodule]); + setSubmoduleRegistry([sharedIdSystemSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, nextrollIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, mwOpenLinkIdSubModule, tapadIdSubmodule, uid2IdSubmodule, admixerIdSubmodule, deepintentDpesSubmodule, flocIdSubmodule, akamaiDAPIdSubmodule, amxIdSubmodule, kinessoIdSubmodule, adqueryIdSubmodule]); init(config); config.setConfig(getConfigMock(['unifiedId', 'unifiedid', 'cookie'])); @@ -572,8 +624,8 @@ describe('User ID', function () { expect(utils.logInfo.args[0][0]).to.exist.and.to.contain('User ID - usersync config updated for 1 submodules'); }); - it('config with 23 configurations should result in 23 submodules add', function () { - setSubmoduleRegistry([sharedIdSystemSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, liveIntentIdSubmodule, britepoolIdSubmodule, netIdSubmodule, nextrollIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, mwOpenLinkIdSubModule, tapadIdSubmodule, uid2IdSubmodule, admixerIdSubmodule, deepintentDpesSubmodule, dmdIdSubmodule, flocIdSubmodule, akamaiDAPIdSubmodule, amxIdSubmodule, kinessoIdSubmodule]); + it('config with 24 configurations should result in 24 submodules add', function () { + setSubmoduleRegistry([sharedIdSystemSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, liveIntentIdSubmodule, britepoolIdSubmodule, netIdSubmodule, nextrollIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, hadronIdSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, mwOpenLinkIdSubModule, tapadIdSubmodule, uid2IdSubmodule, admixerIdSubmodule, deepintentDpesSubmodule, dmdIdSubmodule, flocIdSubmodule, akamaiDAPIdSubmodule, amxIdSubmodule, kinessoIdSubmodule, adqueryIdSubmodule]); init(config); config.setConfig({ userSync: { @@ -606,8 +658,8 @@ describe('User ID', function () { name: 'intentIqId', storage: {name: 'intentIqId', type: 'cookie'} }, { - name: 'haloId', - storage: {name: 'haloId', type: 'cookie'} + name: 'hadronId', + storage: {name: 'hadronId', type: 'cookie'} }, { name: 'zeotapIdPlus' }, { @@ -638,14 +690,17 @@ describe('User ID', function () { }, { name: 'kpuid', storage: {name: 'kpuid', type: 'cookie'} + }, { + name: 'qid', + storage: {name: 'qid', type: 'html5'} }] } }); - expect(utils.logInfo.args[0][0]).to.exist.and.to.contain('User ID - usersync config updated for 23 submodules'); + expect(utils.logInfo.args[0][0]).to.exist.and.to.contain('User ID - usersync config updated for 24 submodules'); }); it('config syncDelay updates module correctly', function () { - setSubmoduleRegistry([sharedIdSystemSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, nextrollIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, mwOpenLinkIdSubModule, tapadIdSubmodule, uid2IdSubmodule, admixerIdSubmodule, deepintentDpesSubmodule, dmdIdSubmodule, flocIdSubmodule, akamaiDAPIdSubmodule, amxIdSubmodule, kinessoIdSubmodule]); + setSubmoduleRegistry([sharedIdSystemSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, nextrollIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, hadronIdSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, mwOpenLinkIdSubModule, tapadIdSubmodule, uid2IdSubmodule, admixerIdSubmodule, deepintentDpesSubmodule, dmdIdSubmodule, flocIdSubmodule, akamaiDAPIdSubmodule, amxIdSubmodule, kinessoIdSubmodule, adqueryIdSubmodule]); init(config); config.setConfig({ @@ -661,7 +716,7 @@ describe('User ID', function () { }); it('config auctionDelay updates module correctly', function () { - setSubmoduleRegistry([sharedIdSystemSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, nextrollIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, mwOpenLinkIdSubModule, tapadIdSubmodule, uid2IdSubmodule, admixerIdSubmodule, deepintentDpesSubmodule, dmdIdSubmodule, flocIdSubmodule, akamaiDAPIdSubmodule, amxIdSubmodule, kinessoIdSubmodule]); + setSubmoduleRegistry([sharedIdSystemSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, nextrollIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, hadronIdSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, mwOpenLinkIdSubModule, tapadIdSubmodule, uid2IdSubmodule, admixerIdSubmodule, deepintentDpesSubmodule, dmdIdSubmodule, flocIdSubmodule, akamaiDAPIdSubmodule, amxIdSubmodule, kinessoIdSubmodule, adqueryIdSubmodule]); init(config); config.setConfig({ userSync: { @@ -676,7 +731,7 @@ describe('User ID', function () { }); it('config auctionDelay defaults to 0 if not a number', function () { - setSubmoduleRegistry([sharedIdSystemSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, nextrollIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, mwOpenLinkIdSubModule, tapadIdSubmodule, uid2IdSubmodule, admixerIdSubmodule, deepintentDpesSubmodule, dmdIdSubmodule, flocIdSubmodule, akamaiDAPIdSubmodule, amxIdSubmodule, kinessoIdSubmodule]); + setSubmoduleRegistry([sharedIdSystemSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, nextrollIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, hadronIdSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, mwOpenLinkIdSubModule, tapadIdSubmodule, uid2IdSubmodule, admixerIdSubmodule, deepintentDpesSubmodule, dmdIdSubmodule, flocIdSubmodule, akamaiDAPIdSubmodule, amxIdSubmodule, kinessoIdSubmodule, adqueryIdSubmodule]); init(config); config.setConfig({ userSync: { @@ -1584,28 +1639,28 @@ describe('User ID', function () { }, {adUnits}); }); - it('test hook from haloId html5', function (done) { + it('test hook from hadronId html5', function (done) { // simulate existing browser local storage values - localStorage.setItem('haloId', JSON.stringify({'haloId': 'random-ls-identifier'})); - localStorage.setItem('haloId_exp', ''); + localStorage.setItem('hadronId', JSON.stringify({'hadronId': 'random-ls-identifier'})); + localStorage.setItem('hadronId_exp', ''); - setSubmoduleRegistry([haloIdSubmodule]); + setSubmoduleRegistry([hadronIdSubmodule]); init(config); - config.setConfig(getConfigMock(['haloId', 'haloId', 'html5'])); + config.setConfig(getConfigMock(['hadronId', 'hadronId', 'html5'])); requestBidsHook(function () { adUnits.forEach(unit => { unit.bids.forEach(bid => { - expect(bid).to.have.deep.nested.property('userId.haloId'); - expect(bid.userId.haloId).to.equal('random-ls-identifier'); + expect(bid).to.have.deep.nested.property('userId.hadronId'); + expect(bid.userId.hadronId).to.equal('random-ls-identifier'); expect(bid.userIdAsEids[0]).to.deep.equal({ source: 'audigent.com', uids: [{id: 'random-ls-identifier', atype: 1}] }); }); }); - localStorage.removeItem('haloId'); - localStorage.removeItem('haloId_exp', ''); + localStorage.removeItem('hadronId'); + localStorage.removeItem('hadronId_exp', ''); done(); }, {adUnits}); }); @@ -1772,7 +1827,38 @@ describe('User ID', function () { }, {adUnits}); }); - it('test hook when pubCommonId, unifiedId, id5Id, identityLink, britepoolId, intentIqId, zeotapIdPlus, netId, haloId, Criteo, UID 2.0, admixerId, amxId, dmdId, kpuid and mwOpenLinkId have data to pass', function (done) { + it('test hook from qid html5', (done) => { + // simulate existing localStorage values + localStorage.setItem('qid', 'testqid'); + localStorage.setItem('qid_exp', ''); + + setSubmoduleRegistry([adqueryIdSubmodule]); + init(config); + config.setConfig(getConfigMock(['qid', 'qid', 'html5'])); + + requestBidsHook(() => { + adUnits.forEach((adUnit) => { + adUnit.bids.forEach((bid) => { + expect(bid).to.have.deep.nested.property('userId.qid'); + expect(bid.userId.qid).to.equal('testqid'); + expect(bid.userIdAsEids[0]).to.deep.equal({ + source: 'adquery.io', + uids: [{ + id: 'testqid', + atype: 1, + }] + }); + }); + }); + + // clear LS + localStorage.removeItem('qid'); + localStorage.removeItem('qid_exp'); + done(); + }, {adUnits}); + }); + + it('test hook when pubCommonId, unifiedId, id5Id, identityLink, britepoolId, intentIqId, zeotapIdPlus, netId, hadronId, Criteo, UID 2.0, admixerId, amxId, dmdId, kpuid, qid and mwOpenLinkId have data to pass', function (done) { coreStorage.setCookie('pubcid', 'testpubcid', (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('unifiedid', JSON.stringify({'TDID': 'testunifiedid'}), (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('id5id', JSON.stringify({'universal_uid': 'testid5id'}), (new Date(Date.now() + 5000).toUTCString())); @@ -1782,19 +1868,20 @@ describe('User ID', function () { coreStorage.setCookie('netId', JSON.stringify({'netId': 'testnetId'}), (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('intentIqId', 'testintentIqId', (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('IDP', btoa(JSON.stringify('zeotapId')), (new Date(Date.now() + 5000).toUTCString())); - coreStorage.setCookie('haloId', JSON.stringify({'haloId': 'testHaloId'}), (new Date(Date.now() + 5000).toUTCString())); + coreStorage.setCookie('hadronId', JSON.stringify({'hadronId': 'testHadronId'}), (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('storage_criteo', JSON.stringify({'criteoId': 'test_bidid'}), (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('mwol', JSON.stringify({eid: 'XX-YY-ZZ-123'}), (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('uid2id', 'Sample_AD_Token', (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('admixerId', 'testadmixerId', (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('deepintentId', 'testdeepintentId', (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('kpuid', 'KINESSO_ID', (new Date(Date.now() + 5000).toUTCString())); - // amxId only supports localStorage localStorage.setItem('amxId', 'test_amxid_id'); localStorage.setItem('amxId_exp', (new Date(Date.now() + 5000)).toUTCString()); - - setSubmoduleRegistry([sharedIdSystemSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, britepoolIdSubmodule, netIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, criteoIdSubmodule, mwOpenLinkIdSubModule, tapadIdSubmodule, uid2IdSubmodule, admixerIdSubmodule, deepintentDpesSubmodule, dmdIdSubmodule, amxIdSubmodule, kinessoIdSubmodule]); + // qid only supports localStorage + localStorage.setItem('qid', 'testqid'); + localStorage.setItem('qid_exp', (new Date(Date.now() + 5000)).toUTCString()); + setSubmoduleRegistry([sharedIdSystemSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, britepoolIdSubmodule, netIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, hadronIdSubmodule, criteoIdSubmodule, mwOpenLinkIdSubModule, tapadIdSubmodule, uid2IdSubmodule, admixerIdSubmodule, deepintentDpesSubmodule, dmdIdSubmodule, amxIdSubmodule, kinessoIdSubmodule, adqueryIdSubmodule]); init(config); config.setConfig(getConfigMock(['pubCommonId', 'pubcid', 'cookie'], ['unifiedId', 'unifiedid', 'cookie'], @@ -1805,7 +1892,7 @@ describe('User ID', function () { ['netId', 'netId', 'cookie'], ['intentIqId', 'intentIqId', 'cookie'], ['zeotapIdPlus', 'IDP', 'cookie'], - ['haloId', 'haloId', 'cookie'], + ['hadronId', 'hadronId', 'cookie'], ['criteo', 'storage_criteo', 'cookie'], ['mwOpenLinkId', 'mwol', 'cookie'], ['tapadId', 'tapad_id', 'cookie'], @@ -1813,7 +1900,8 @@ describe('User ID', function () { ['admixerId', 'admixerId', 'cookie'], ['amxId', 'amxId', 'html5'], ['deepintentId', 'deepintentId', 'cookie'], - ['kpuid', 'kpuid', 'cookie'])); + ['kpuid', 'kpuid', 'cookie'], + ['qid', 'qid', 'html5'])); requestBidsHook(function () { adUnits.forEach(unit => { @@ -1845,9 +1933,9 @@ describe('User ID', function () { // also check that zeotapIdPlus id data was copied to bid expect(bid).to.have.deep.nested.property('userId.IDP'); expect(bid.userId.IDP).to.equal('zeotapId'); - // also check that haloId id was copied to bid - expect(bid).to.have.deep.nested.property('userId.haloId'); - expect(bid.userId.haloId).to.equal('testHaloId'); + // also check that hadronId id was copied to bid + expect(bid).to.have.deep.nested.property('userId.hadronId'); + expect(bid.userId.hadronId).to.equal('testHadronId'); // also check that criteo id was copied to bid expect(bid).to.have.deep.nested.property('userId.criteoId'); expect(bid.userId.criteoId).to.equal('test_bidid'); @@ -1871,7 +1959,10 @@ describe('User ID', function () { expect(bid).to.have.deep.nested.property('userId.kpuid'); expect(bid.userId.kpuid).to.equal('KINESSO_ID'); - expect(bid.userIdAsEids.length).to.equal(17); + expect(bid).to.have.deep.nested.property('userId.qid'); + expect(bid.userId.qid).to.equal('testqid'); + + expect(bid.userIdAsEids.length).to.equal(18); }); }); coreStorage.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); @@ -1883,7 +1974,7 @@ describe('User ID', function () { coreStorage.setCookie('netId', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('intentIqId', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('IDP', '', EXPIRED_COOKIE_DATE); - coreStorage.setCookie('haloId', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('hadronId', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('storage_criteo', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('mwol', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('uid2id', '', EXPIRED_COOKIE_DATE); @@ -1892,11 +1983,13 @@ describe('User ID', function () { coreStorage.setCookie('kpuid', EXPIRED_COOKIE_DATE); localStorage.removeItem('amxId'); localStorage.removeItem('amxId_exp'); + localStorage.removeItem('qid'); + localStorage.removeItem('qid_exp'); done(); }, {adUnits}); }); - it('test hook when pubCommonId, unifiedId, id5Id, britepoolId, dmdId, intentIqId, zeotapIdPlus, criteo, netId, haloId, UID 2.0, admixerId, kpuid and mwOpenLinkId have their modules added before and after init', function (done) { + it('test hook when pubCommonId, unifiedId, id5Id, britepoolId, dmdId, intentIqId, zeotapIdPlus, criteo, netId, hadronId, UID 2.0, admixerId, kpuid and mwOpenLinkId have their modules added before and after init', function (done) { coreStorage.setCookie('pubcid', 'testpubcid', (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('unifiedid', JSON.stringify({'TDID': 'cookie-value-add-module-variations'}), new Date(Date.now() + 5000).toUTCString()); coreStorage.setCookie('id5id', JSON.stringify({'universal_uid': 'testid5id'}), (new Date(Date.now() + 5000).toUTCString())); @@ -1905,7 +1998,7 @@ describe('User ID', function () { coreStorage.setCookie('netId', JSON.stringify({'netId': 'testnetId'}), (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('intentIqId', 'testintentIqId', (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('IDP', btoa(JSON.stringify('zeotapId')), (new Date(Date.now() + 5000).toUTCString())); - coreStorage.setCookie('haloId', JSON.stringify({'haloId': 'testHaloId'}), (new Date(Date.now() + 5000).toUTCString())); + coreStorage.setCookie('hadronId', JSON.stringify({'hadronId': 'testHadronId'}), (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('dmdId', 'testdmdId', (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('storage_criteo', JSON.stringify({'criteoId': 'test_bidid'}), (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('mwol', JSON.stringify({eid: 'XX-YY-ZZ-123'}), (new Date(Date.now() + 5000).toUTCString())); @@ -1929,7 +2022,7 @@ describe('User ID', function () { attachIdSystem(netIdSubmodule); attachIdSystem(intentIqIdSubmodule); attachIdSystem(zeotapIdPlusSubmodule); - attachIdSystem(haloIdSubmodule); + attachIdSystem(hadronIdSubmodule); attachIdSystem(dmdIdSubmodule); attachIdSystem(criteoIdSubmodule); attachIdSystem(mwOpenLinkIdSubModule); @@ -1947,7 +2040,7 @@ describe('User ID', function () { ['netId', 'netId', 'cookie'], ['intentIqId', 'intentIqId', 'cookie'], ['zeotapIdPlus', 'IDP', 'cookie'], - ['haloId', 'haloId', 'cookie'], + ['hadronId', 'hadronId', 'cookie'], ['dmdId', 'dmdId', 'cookie'], ['criteo', 'storage_criteo', 'cookie'], ['mwOpenLinkId', 'mwol', 'cookie'], @@ -1985,9 +2078,9 @@ describe('User ID', function () { // also check that zeotapIdPlus id data was copied to bid expect(bid).to.have.deep.nested.property('userId.IDP'); expect(bid.userId.IDP).to.equal('zeotapId'); - // also check that haloId id data was copied to bid - expect(bid).to.have.deep.nested.property('userId.haloId'); - expect(bid.userId.haloId).to.equal('testHaloId'); + // also check that hadronId id data was copied to bid + expect(bid).to.have.deep.nested.property('userId.hadronId'); + expect(bid.userId.hadronId).to.equal('testHadronId'); // also check that dmdId id data was copied to bid expect(bid).to.have.deep.nested.property('userId.dmdId'); expect(bid.userId.dmdId).to.equal('testdmdId'); @@ -2023,7 +2116,7 @@ describe('User ID', function () { coreStorage.setCookie('netId', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('intentIqId', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('IDP', '', EXPIRED_COOKIE_DATE); - coreStorage.setCookie('haloId', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('hadronId', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('dmdId', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('storage_criteo', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('mwol', '', EXPIRED_COOKIE_DATE); @@ -2073,7 +2166,7 @@ describe('User ID', function () { coreStorage.setCookie('netId', JSON.stringify({'netId': 'testnetId'}), new Date(Date.now() + 5000).toUTCString()); coreStorage.setCookie('intentIqId', 'testintentIqId', (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('IDP', btoa(JSON.stringify('zeotapId')), (new Date(Date.now() + 5000).toUTCString())); - coreStorage.setCookie('haloId', JSON.stringify({'haloId': 'testHaloId'}), (new Date(Date.now() + 5000).toUTCString())); + coreStorage.setCookie('hadronId', JSON.stringify({'hadronId': 'testHadronId'}), (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('admixerId', 'testadmixerId', new Date(Date.now() + 5000).toUTCString()); coreStorage.setCookie('deepintentId', 'testdeepintentId', new Date(Date.now() + 5000).toUTCString()); coreStorage.setCookie('MOCKID', JSON.stringify({'MOCKID': '123456778'}), new Date(Date.now() + 5000).toUTCString()); @@ -2081,8 +2174,10 @@ describe('User ID', function () { localStorage.setItem('amxId', 'test_amxid_id'); localStorage.setItem('amxId_exp', new Date(Date.now() + 5000).toUTCString()) coreStorage.setCookie('kpuid', 'KINESSO_ID', (new Date(Date.now() + 5000).toUTCString())); + localStorage.setItem('qid', 'testqid'); + localStorage.setItem('qid_exp', new Date(Date.now() + 5000).toUTCString()) - setSubmoduleRegistry([sharedIdSystemSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, britepoolIdSubmodule, netIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, uid2IdSubmodule, admixerIdSubmodule, deepintentDpesSubmodule, dmdIdSubmodule, akamaiDAPIdSubmodule, amxIdSubmodule, kinessoIdSubmodule]); + setSubmoduleRegistry([sharedIdSystemSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, britepoolIdSubmodule, netIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, hadronIdSubmodule, uid2IdSubmodule, admixerIdSubmodule, deepintentDpesSubmodule, dmdIdSubmodule, akamaiDAPIdSubmodule, amxIdSubmodule, kinessoIdSubmodule, adqueryIdSubmodule]); init(config); config.setConfig({ @@ -2107,7 +2202,7 @@ describe('User ID', function () { }, { name: 'zeotapIdPlus' }, { - name: 'haloId', storage: {name: 'haloId', type: 'cookie'} + name: 'hadronId', storage: {name: 'hadronId', type: 'cookie'} }, { name: 'admixerId', storage: {name: 'admixerId', type: 'cookie'} }, { @@ -2120,6 +2215,8 @@ describe('User ID', function () { name: 'amxId', storage: {name: 'amxId', type: 'html5'} }, { name: 'kpuid', storage: {name: 'kpuid', type: 'cookie'} + }, { + name: 'qid', storage: {name: 'qid', type: 'html5'} }] } }); @@ -2171,9 +2268,9 @@ describe('User ID', function () { // also check that zeotapIdPlus id data was copied to bid expect(bid).to.have.deep.nested.property('userId.IDP'); expect(bid.userId.IDP).to.equal('zeotapId'); - // also check that haloId id data was copied to bid - expect(bid).to.have.deep.nested.property('userId.haloId'); - expect(bid.userId.haloId).to.equal('testHaloId'); + // also check that hadronId id data was copied to bid + expect(bid).to.have.deep.nested.property('userId.hadronId'); + expect(bid.userId.hadronId).to.equal('testHadronId'); expect(bid.userId.uid2).to.deep.equal({ id: 'Sample_AD_Token' }); @@ -2191,7 +2288,10 @@ describe('User ID', function () { expect(bid).to.have.deep.nested.property('userId.kpuid'); expect(bid.userId.kpuid).to.equal('KINESSO_ID'); - expect(bid.userIdAsEids.length).to.equal(15); + + expect(bid).to.have.deep.nested.property('userId.qid'); + expect(bid.userId.qid).to.equal('testqid'); + expect(bid.userIdAsEids.length).to.equal(16); }); }); coreStorage.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); @@ -2202,7 +2302,7 @@ describe('User ID', function () { coreStorage.setCookie('netId', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('intentIqId', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('IDP', '', EXPIRED_COOKIE_DATE); - coreStorage.setCookie('haloId', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('hadronId', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('dmdId', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('admixerId', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('deepintentId', '', EXPIRED_COOKIE_DATE); @@ -2553,5 +2653,97 @@ describe('User ID', function () { }); }); }); + + describe('handles config with ESP configuration in user sync object', function() { + describe('Call registerSignalSources to register signal sources with gtag', function () { + it('pbjs.registerSignalSources should be defined', () => { + expect(typeof (getGlobal()).registerSignalSources).to.equal('function'); + }); + }) + + describe('Call getEncryptedEidsForSource to get encrypted Eids for source', function() { + const signalSources = ['pubcid.org']; + + it('pbjs.getEncryptedEidsForSource should be defined', () => { + expect(typeof (getGlobal()).getEncryptedEidsForSource).to.equal('function'); + }); + + it('pbjs.getEncryptedEidsForSource should return the string without encryption if encryption is false', (done) => { + setSubmoduleRegistry([sharedIdSystemSubmodule]); + init(config); + config.setConfig({ + userSync: { + syncDelay: 0, + userIds: [ + { + 'name': 'sharedId', + 'storage': { + 'type': 'cookie', + 'name': '_pubcid', + 'expires': 365 + } + }, + { + 'name': 'pubcid.org' + } + ] + }, + }); + const encrypt = false; + (getGlobal()).getEncryptedEidsForSource(signalSources[0], encrypt).then((data) => { + let users = (getGlobal()).getUserIdsAsEids(); + expect(data).to.equal(users[0].uids[0].id); + done(); + }).catch(done); + }); + + it('pbjs.getEncryptedEidsForSource should return the string base64 encryption if encryption is true', (done) => { + const encrypt = true; + (getGlobal()).getEncryptedEidsForSource(signalSources[0], encrypt).then((result) => { + expect(result.startsWith('1||')).to.true; + done(); + }).catch(done); + }); + + it('pbjs.getEncryptedEidsForSource should return string if custom function is defined', (done) => { + const getCustomSignal = () => { + return '{"keywords":["tech","auto"]}'; + } + const expectedString = '1||eyJrZXl3b3JkcyI6WyJ0ZWNoIiwiYXV0byJdfQ=='; + const encrypt = false; + const source = 'pubmatic.com'; + (getGlobal()).getEncryptedEidsForSource(source, encrypt, getCustomSignal).then((result) => { + expect(result).to.equal(expectedString); + done(); + }).catch(done); + }); + + it('pbjs.getUserIdsAsEidBySource', () => { + const users = { + 'source': 'pubcid.org', + 'uids': [ + { + 'id': '11111', + 'atype': 1 + } + ] + } + setSubmoduleRegistry([sharedIdSystemSubmodule, amxIdSubmodule]); + init(config); + config.setConfig({ + userSync: { + syncDelay: 0, + userIds: [{ + name: 'pubCommonId', value: {'pubcid': '11111'} + }, { + name: 'amxId', value: {'amxId': 'amx-id-value-amx-id-value-amx-id-value'} + }] + } + }); + expect(typeof (getGlobal()).getUserIdsAsEidBySource).to.equal('function'); + expect((getGlobal()).getUserIdsAsEidBySource(signalSources[0])).to.deep.equal(users); + }); + }) + }); }) }); diff --git a/test/spec/modules/vibrantmediaBidAdapter_spec.js b/test/spec/modules/vibrantmediaBidAdapter_spec.js new file mode 100644 index 00000000000..c6ce7d52fb3 --- /dev/null +++ b/test/spec/modules/vibrantmediaBidAdapter_spec.js @@ -0,0 +1,1237 @@ +import {expect} from 'chai'; +import {spec} from 'modules/vibrantmediaBidAdapter.js'; +import {newBidder} from 'src/adapters/bidderFactory.js'; +import {BANNER, NATIVE, VIDEO} from 'src/mediaTypes.js'; +import {INSTREAM, OUTSTREAM} from 'src/video.js'; + +const EXPECTED_PREBID_SERVER_URL = 'https://prebid.intellitxt.com/prebid'; + +const BANNER_AD = + 'Test Banner Ad UnitHello!'; +const VIDEO_AD = 'Test Video Ad Unit' + + ''; + +const VALID_BANNER_BID_PARAMS = Object.freeze({ + member: '1234', + invCode: 'ABCD', + placementId: '10433394' +}); + +const VALID_VIDEO_BID_PARAMS = Object.freeze({ + member: '1234', + invCode: 'ABCD', + placementId: '10433394', + video: { + skippable: false, + playback_method: 'auto_play_sound_off' + } +}); + +const VALID_NATIVE_BID_PARAMS = VALID_BANNER_BID_PARAMS; + +const DEFAULT_BID_SIZES = [[300, 250], [600, 240]]; + +const VALID_CONSENT_STRING = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; + +const getValidBidderRequest = (bidRequests) => { + return Object.freeze({ + bidderCode: 'vibrantmedia', + auctionId: '1d1a030790a475', + bidderRequestId: '22edbae2733bf6', + timeout: 3000, + gdprConsent: { + consentString: VALID_CONSENT_STRING, + vendorData: {}, + gdprApplies: true, + }, + bids: bidRequests, + }); +}; + +describe('VibrantMediaBidAdapter', function () { + const adapter = newBidder(spec); + + describe('constants', function () { + expect(spec.code).to.equal('vibrantmedia'); + expect(spec.supportedMediaTypes).to.deep.equal([BANNER, NATIVE, VIDEO]); + }); + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('transformBidParams', function () { + it('transforms bid params correctly', function () { + expect(spec.transformBidParams(VALID_VIDEO_BID_PARAMS)).to.deep.equal(VALID_VIDEO_BID_PARAMS); + }); + }) + + let bidRequest; + + beforeEach(function () { + bidRequest = { + bidder: 'vibrantmedia', + params: { + // Filled in by individual tests + }, + mediaTypes: { + // Filled in by individual tests + }, + adUnitCode: 'test-div', + transactionId: '13579acef87623', + placementId: '7623587623857', + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475' + }; + }); + + describe('isBidRequestValid', function () { + describe('with banner bid requests', function () { + beforeEach(function () { + bidRequest.mediaTypes.banner = { + sizes: DEFAULT_BID_SIZES, + }; + }); + + it('should return true for a valid banner bid request', function () { + bidRequest.params = VALID_BANNER_BID_PARAMS; + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + + it('should return true for a valid banner bid request with a member id and inventory code', function () { + bidRequest.params = { + member: '1234', + invCode: 'ABCD', + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + + it('should return true for a valid banner bid request with a placement id', function () { + bidRequest.params = { + placementId: '10433394', + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + + it('should return false for a valid banner bid request but with a member id and no inventory code', function () { + bidRequest.params = { + member: '1234', + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return false for a valid banner bid request but with no member id and an inventory code', function () { + bidRequest.params = { + invCode: 'ABCD', + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return false for a valid banner bid request but with no supported media types', function () { + bidRequest.params = { + placementId: '10433394', + }; + delete bidRequest.mediaTypes.banner; + bidRequest.mediaTypes.unsupported = { + sizes: DEFAULT_BID_SIZES, + } + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return false for a valid banner bid request but with no params', function () { + bidRequest.params = {}; + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + }); + + describe('with video bid requests', function () { + describe('with sizes attribute', function () { + const validVideoMediaTypes = { + context: OUTSTREAM, + sizes: DEFAULT_BID_SIZES, + minduration: 1, + maxduration: 60, + skip: 0, + skipafter: 5, + playbackmethod: [2], + protocols: [1, 2, 3] + }; + + it('should return true for a valid video bid request', function () { + bidRequest.params = VALID_VIDEO_BID_PARAMS; + bidRequest.mediaTypes.video = validVideoMediaTypes; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + + it('should return false for an instream video bid request', function () { + bidRequest.params = VALID_VIDEO_BID_PARAMS; + bidRequest.mediaTypes.video = { + context: INSTREAM, + sizes: DEFAULT_BID_SIZES, + }; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return false for a video bid request with an unknown context', function () { + bidRequest.params = VALID_VIDEO_BID_PARAMS; + bidRequest.mediaTypes.video = { + context: 'fake', + sizes: DEFAULT_BID_SIZES, + }; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return false for a video bid request with no context', function () { + bidRequest.params = VALID_VIDEO_BID_PARAMS; + bidRequest.mediaTypes.video = { + sizes: DEFAULT_BID_SIZES, + }; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return true for a valid video bid request with a member id and inventory code', function () { + bidRequest.params = { + member: '1234', + invCode: 'ABCD', + }; + bidRequest.mediaTypes.video = validVideoMediaTypes; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + + it('should return true for a valid video bid request with a placement id', function () { + bidRequest.params = { + placementId: '10433394', + }; + bidRequest.mediaTypes.video = validVideoMediaTypes; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + + it('should return false for a valid video bid request but with a member id and no inventory code', function () { + bidRequest.params = { + member: '1234', + }; + bidRequest.mediaTypes.video = validVideoMediaTypes; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return false for a valid video bid request but with no member id and an inventory code', function () { + bidRequest.params = { + invCode: 'ABCD', + }; + bidRequest.mediaTypes.video = validVideoMediaTypes; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return false for a valid video bid request but with no params', function () { + bidRequest.params = {}; + bidRequest.mediaTypes.video = validVideoMediaTypes; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + }); + + describe('with playerSize attribute', function () { + const validVideoMediaTypes = { + context: OUTSTREAM, + playerSize: DEFAULT_BID_SIZES, + minduration: 1, + maxduration: 60, + skip: 0, + skipafter: 5, + playbackmethod: [2], + protocols: [1, 2, 3] + }; + + beforeEach(function () { + bidRequest.mediaTypes.video = { + // Filled in by individual tests + }; + }); + + it('should return true for a valid video bid request', function () { + bidRequest.params = VALID_VIDEO_BID_PARAMS; + bidRequest.mediaTypes.video = validVideoMediaTypes; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + + it('should return false for an instream video bid request', function () { + bidRequest.params = VALID_VIDEO_BID_PARAMS; + bidRequest.mediaTypes.video = { + context: INSTREAM, + playerSize: DEFAULT_BID_SIZES, + }; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return false for a video bid request with an unknown context', function () { + bidRequest.params = VALID_VIDEO_BID_PARAMS; + bidRequest.mediaTypes.video = { + context: 'fake', + playerSize: DEFAULT_BID_SIZES, + }; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return false for a video bid request with no context', function () { + bidRequest.params = VALID_VIDEO_BID_PARAMS; + bidRequest.mediaTypes.video = { + playerSize: DEFAULT_BID_SIZES, + }; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return true for a valid video bid request with a member id and inventory code', function () { + bidRequest.params = { + member: '1234', + invCode: 'ABCD', + }; + bidRequest.mediaTypes.video = validVideoMediaTypes; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + + it('should return true for a valid video bid request with a placement id', function () { + bidRequest.params = { + placementId: '10433394', + }; + bidRequest.mediaTypes.video = validVideoMediaTypes; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + + it('should return false for a valid video bid request but with a member id and no inventory code', function () { + bidRequest.params = { + member: '1234', + }; + bidRequest.mediaTypes.video = validVideoMediaTypes; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return false for a valid video bid request but with no member id and an inventory code', function () { + bidRequest.params = { + invCode: 'ABCD', + }; + bidRequest.mediaTypes.video = validVideoMediaTypes; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return false for a valid video bid request but with no params', function () { + bidRequest.params = {}; + bidRequest.mediaTypes.video = validVideoMediaTypes; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + }); + }); + + describe('with native bid requests', function () { + beforeEach(function () { + bidRequest.mediaTypes.native = { + image: { + required: true, + // Sizes is filled in by individual tests + }, + title: { + required: true + }, + sponsoredBy: { + required: true + }, + clickUrl: { + required: true + } + }; + }); + + it('should return true for a valid native bid request with a single size', function () { + bidRequest.params = VALID_NATIVE_BID_PARAMS; + bidRequest.mediaTypes.native.image.sizes = [300, 250]; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + + it('should return true for a valid native bid request with multiple sizes', function () { + bidRequest.params = VALID_NATIVE_BID_PARAMS; + bidRequest.mediaTypes.native.image.sizes = [[300, 250], [300, 600]]; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + + it('should return true for a valid native bid request with a member id and inventory code', function () { + bidRequest.params = { + member: '1234', + invCode: 'ABCD', + }; + bidRequest.mediaTypes.native.image.sizes = [300, 250]; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + + it('should return true for a valid native bid request with a placement id', function () { + bidRequest.params = { + placementId: '10433394', + }; + bidRequest.mediaTypes.native.image.sizes = [300, 250]; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + + it('should return false for a valid native bid request but with a member id and no inventory code', function () { + bidRequest.params = { + member: '1234', + }; + bidRequest.mediaTypes.native.image.sizes = [300, 250]; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return false for a valid native bid request but with no member id and an inventory code', function () { + bidRequest.params = { + invCode: 'ABCD', + }; + bidRequest.mediaTypes.native.image.sizes = [300, 250]; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return false for a valid native bid request but with no params', function () { + bidRequest.params = {}; + bidRequest.mediaTypes.native.image.sizes = [300, 250]; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return false for a native bid request with no image property', function () { + bidRequest.params = VALID_NATIVE_BID_PARAMS; + delete bidRequest.mediaTypes.native.image; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + }); + }); + + describe('buildRequests', function () { + let bidRequests; + + beforeEach(function () { + bidRequests = [bidRequest]; + + bidRequests[0].params = VALID_BANNER_BID_PARAMS; + bidRequests[0].mediaTypes.banner = { + sizes: DEFAULT_BID_SIZES, + }; + }); + + it('should use HTTP POST', function () { + const request = spec.buildRequests(bidRequests, {}); + expect(request.method).to.equal('POST'); + }); + + it('should use the correct prebid server URL', function () { + const request = spec.buildRequests(bidRequests, {}); + expect(request.url).to.equal(EXPECTED_PREBID_SERVER_URL); + }); + + it('should add the page URL to the server request', function () { + const request = spec.buildRequests(bidRequests, {}); + const payload = JSON.parse(request.data); + + expect(payload.url).to.exist; + expect(payload.url).to.be.a('string'); + }); + + it('should add GDPR consent to the server request, where present', function () { + const bidderRequest = { + bidderCode: 'vibrantmedia', + auctionId: '1d1a030790a475', + bidderRequestId: '22edbae2733bf6', + timeout: 3000, + gdprConsent: { + consentString: VALID_CONSENT_STRING, + gdprApplies: true, + }, + bids: bidRequests, + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + // TODO: Check that we should not be implementing withCredentials + // expect(request.options).to.deep.equal({withCredentials: true}); + + const payload = JSON.parse(request.data); + expect(payload.gdpr).to.exist; + expect(payload.gdpr.consentString).to.exist.and.to.equal(VALID_CONSENT_STRING); + expect(payload.gdpr.gdprApplies).to.exist.and.to.be.true; + }); + + it('should add USP consent to the server request, where present', function () { + const bidderRequest = { + bidderCode: 'vibrantmedia', + auctionId: '1d1a030790a475', + bidderRequestId: '22edbae2733bf6', + timeout: 3000, + uspConsent: { + cmpApi: 'iab', + timeout: 10000, + consentData: { + testDatum: true + } + }, + bids: bidRequests + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + // TODO: Check that we should not be implementing withCredentials + // expect(request.options).to.deep.equal({withCredentials: true}); + + const payload = JSON.parse(request.data); + expect(payload.usp).to.exist; + expect(payload.usp.cmpApi).to.exist.and.to.equal('iab'); + expect(payload.usp.timeout).to.exist.and.to.equal(10000); + expect(payload.usp.consentData).to.exist.and.to.deep.equal({ + testDatum: true + }); + }); + + it('should add GDPR and USP consent to the server request, where both present', function () { + const bidderRequest = { + bidderCode: 'vibrantmedia', + auctionId: '1d1a030790a475', + bidderRequestId: '22edbae2733bf6', + timeout: 3000, + gdprConsent: { + consentString: VALID_CONSENT_STRING, + gdprApplies: true, + }, + uspConsent: { + cmpApi: 'iab', + timeout: 10000, + consentData: { + testDatum: true + } + }, + bids: bidRequests, + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + // TODO: Check that we should not be implementing withCredentials + // expect(request.options).to.deep.equal({withCredentials: true}); + + const payload = JSON.parse(request.data); + + expect(payload.gdpr).to.exist; + expect(payload.gdpr.consentString).to.exist.and.to.equal(VALID_CONSENT_STRING); + expect(payload.gdpr.gdprApplies).to.exist.and.to.be.true; + + expect(payload.usp).to.exist; + expect(payload.usp.cmpApi).to.exist.and.to.equal('iab'); + expect(payload.usp.timeout).to.exist.and.to.equal(10000); + expect(payload.usp.consentData).to.exist.and.to.deep.equal({ + testDatum: true + }); + }); + + it('should add window dimensions to the server request', function () { + const request = spec.buildRequests(bidRequests, {}); + const payload = JSON.parse(request.data); + + expect(payload.window).to.exist; + expect(payload.window.width).to.equal(window.innerWidth); + expect(payload.window.height).to.equal(window.innerHeight); + }); + + it('should add the top-level sizes to the bid request, if present', function () { + bidRequest.params = VALID_BANNER_BID_PARAMS; + bidRequest.sizes = DEFAULT_BID_SIZES; + bidRequest.mediaTypes = { + banner: {}, + }; + + const request = spec.buildRequests(bidRequests, {}, true); + const payload = JSON.parse(request.data); + + expect(payload.biddata).to.exist; + expect(payload.biddata.length).to.equal(1); + expect(payload.biddata[0]).to.exist; + expect(payload.biddata[0].code).to.equal(bidRequest.adUnitCode); + expect(payload.biddata[0].id).to.equal(bidRequest.placementId); + expect(payload.biddata[0].bidder).to.equal(bidRequest.bidder); + expect(payload.biddata[0].sizes).to.deep.equal(DEFAULT_BID_SIZES); + expect(payload.biddata[0].mediaTypes).to.exist; + expect(payload.biddata[0].mediaTypes[BANNER]).to.exist; + expect(payload.biddata[0].mediaTypes[BANNER]).to.deep.equal({}); + }); + + it('should add the list of bids to the bid request, if present', function () { + const testBid = { + bidder: 'testBidder', + params: { + placement: '12345' + } + }; + + bidRequest.params = VALID_BANNER_BID_PARAMS; + bidRequest.bids = [testBid]; + bidRequest.mediaTypes = { + banner: {}, + }; + + // These will be present in the list of bids instead + delete bidRequest.bidId; + delete bidRequest.transactionId; + delete bidRequest.bidder; + + const request = spec.buildRequests(bidRequests, {}, true); + const payload = JSON.parse(request.data); + + expect(payload.biddata).to.exist; + expect(payload.biddata.length).to.equal(1); + expect(payload.biddata[0]).to.exist; + expect(payload.biddata[0].code).to.equal(bidRequest.adUnitCode); + expect(payload.biddata[0].id).to.equal(bidRequest.placementId); + expect(payload.biddata[0].bidder).to.equal(bidRequest.bidder); + expect(payload.biddata[0].bids.length).to.equal(1); + expect(payload.biddata[0].bids[0]).to.deep.equal(testBid); + expect(payload.biddata[0].mediaTypes).to.exist; + expect(payload.biddata[0].mediaTypes[BANNER]).to.exist; + expect(payload.biddata[0].mediaTypes[BANNER]).to.deep.equal({}); + }); + + it('should add the correct bid data to the server request for one bid request', function () { + bidRequest.params = VALID_BANNER_BID_PARAMS; + bidRequest.mediaTypes = { + banner: { + sizes: DEFAULT_BID_SIZES, + }, + }; + + const request = spec.buildRequests(bidRequests, {}, true); + const payload = JSON.parse(request.data); + + expect(payload.biddata).to.exist; + expect(payload.biddata.length).to.equal(1); + expect(payload.biddata[0]).to.exist; + expect(payload.biddata[0].code).to.equal(bidRequest.adUnitCode); + expect(payload.biddata[0].id).to.equal(bidRequest.placementId); + expect(payload.biddata[0].bidder).to.equal(bidRequest.bidder); + expect(payload.biddata[0].sizes).to.be.undefined; + expect(payload.biddata[0].mediaTypes).to.exist; + expect(payload.biddata[0].mediaTypes[BANNER]).to.exist; + expect(payload.biddata[0].mediaTypes[BANNER]).to.deep.equal({ + sizes: DEFAULT_BID_SIZES, + }); + }); + + it('should add the correct bid data to the server request for multiple bid requests', function () { + bidRequest.params = VALID_BANNER_BID_PARAMS; + bidRequest.mediaTypes = { + banner: { + sizes: DEFAULT_BID_SIZES, + }, + }; + const bid2 = { + bidder: 'vibrantmedia', + params: VALID_VIDEO_BID_PARAMS, + mediaTypes: { + video: { + context: OUTSTREAM, + sizes: DEFAULT_BID_SIZES, + } + }, + adUnitCode: 'video-div', + bidId: '30b31c1838de1f', + placementId: '135797531abcdef', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + }; + const bid3 = { + bidder: 'vibrantmedia', + params: VALID_NATIVE_BID_PARAMS, + mediaTypes: { + native: { + image: { + required: true, + sizes: [300, 250] + }, + title: { + required: true + }, + sponsoredBy: { + required: true + }, + clickUrl: { + required: true + } + } + }, + adUnitCode: 'native-div', + bidId: '30b31c1838de14', + placementId: '918273645abcdef', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + }; + + bidRequests.push(bid2, bid3); + + const request = spec.buildRequests(bidRequests, {}, true); + const payload = JSON.parse(request.data); + + expect(payload.biddata).to.exist; + expect(payload.biddata.length).to.equal(3); + expect(payload.biddata[0]).to.exist; + expect(payload.biddata[0].code).to.equal(bidRequest.adUnitCode); + expect(payload.biddata[0].id).to.equal(bidRequest.placementId); + expect(payload.biddata[0].bidder).to.equal(bidRequest.bidder); + expect(payload.biddata[0].mediaTypes).to.exist; + expect(payload.biddata[0].mediaTypes[BANNER]).to.exist; + expect(payload.biddata[0].mediaTypes[BANNER]).to.deep.equal({ + sizes: DEFAULT_BID_SIZES, + }); + expect(payload.biddata[1]).to.exist; + expect(payload.biddata[1].code).to.equal(bid2.adUnitCode); + expect(payload.biddata[1].id).to.equal(bid2.placementId); + expect(payload.biddata[1].bidder).to.equal(bid2.bidder); + expect(payload.biddata[1].mediaTypes).to.exist; + expect(payload.biddata[1].mediaTypes[VIDEO]).to.exist; + expect(payload.biddata[1].mediaTypes[VIDEO]).to.deep.equal({ + context: OUTSTREAM, + sizes: DEFAULT_BID_SIZES, + }); + expect(payload.biddata[2]).to.exist; + expect(payload.biddata[2].code).to.equal(bid3.adUnitCode); + expect(payload.biddata[2].id).to.equal(bid3.placementId); + expect(payload.biddata[2].bidder).to.equal(bid3.bidder); + expect(payload.biddata[2].mediaTypes[NATIVE]).to.exist; + expect(payload.biddata[2].mediaTypes[NATIVE]).to.deep.equal(bid3.mediaTypes.native); + }); + + it('should add the correct bid data to the bid request where a bid has multiple media types', function () { + bidRequest.params = VALID_VIDEO_BID_PARAMS; + bidRequest.mediaTypes = { + banner: { + sizes: DEFAULT_BID_SIZES, + }, + video: { + context: OUTSTREAM, + sizes: DEFAULT_BID_SIZES, + }, + native: { + image: { + required: true, + sizes: [300, 250] + }, + title: { + required: true + }, + sponsoredBy: { + required: true + }, + clickUrl: { + required: true + } + } + }; + bidRequest.adUnitCode = 'mixed-div'; + + const bid2 = { + bidder: 'vibrantmedia', + params: VALID_VIDEO_BID_PARAMS, + mediaTypes: { + video: { + context: OUTSTREAM, + sizes: DEFAULT_BID_SIZES, + } + }, + adUnitCode: 'video-div', + bidId: '30b31c1838de1a', + bidderRequestId: '22edbae2733bf6', + placementId: '293857832abfef', + auctionId: '1d1a030790a475', + }; + + bidRequests.push(bid2); + + const request = spec.buildRequests(bidRequests, {}, true); + const payload = JSON.parse(request.data); + + expect(payload.biddata).to.exist; + expect(payload.biddata.length).to.equal(2); + expect(payload.biddata[0]).to.exist; + expect(payload.biddata[0].code).to.equal(bidRequest.adUnitCode); + expect(payload.biddata[0].id).to.equal(bidRequest.placementId); + expect(payload.biddata[0].bidder).to.equal(bidRequest.bidder); + expect(payload.biddata[0].mediaTypes).to.exist; + expect(Object.keys(payload.biddata[0].mediaTypes).length).to.equal(3); + expect(payload.biddata[0].mediaTypes[BANNER]).to.exist; + expect(payload.biddata[0].mediaTypes[BANNER]).to.deep.equal({ + sizes: DEFAULT_BID_SIZES, + }); + expect(payload.biddata[0].mediaTypes[VIDEO]).to.exist; + expect(payload.biddata[0].mediaTypes[VIDEO]).to.deep.equal({ + context: OUTSTREAM, + sizes: DEFAULT_BID_SIZES, + }); + expect(payload.biddata[0].code).to.equal(bidRequest.adUnitCode); + expect(payload.biddata[0].id).to.equal(bidRequest.placementId); + expect(payload.biddata[0].bidder).to.equal(bidRequest.bidder); + expect(payload.biddata[0].mediaTypes[NATIVE]).to.exist; + expect(payload.biddata[0].mediaTypes[NATIVE]).to.deep.equal(bidRequest.mediaTypes.native); + expect(payload.biddata[1]).to.exist; + expect(payload.biddata[1].code).to.equal(bid2.adUnitCode); + expect(payload.biddata[1].id).to.equal(bid2.placementId); + expect(payload.biddata[1].bidder).to.equal(bid2.bidder); + expect(payload.biddata[1].mediaTypes).to.exist; + expect(Object.keys(payload.biddata[1].mediaTypes).length).to.equal(1); + expect(payload.biddata[1].mediaTypes[VIDEO]).to.exist; + expect(payload.biddata[1].mediaTypes[VIDEO]).to.deep.equal({ + context: OUTSTREAM, + sizes: DEFAULT_BID_SIZES, + }); + }); + }); + + describe('interpretResponse', function () { + it('returns a valid Prebid API response object for a banner Prebid Server response', function () { + const prebidServerResponse = { + body: [{ + mediaType: 'banner', + requestId: '12345', + cpm: 1, + currency: 'USD', + width: 640, + height: 240, + ad: BANNER_AD, + ttl: 300, + creativeId: '86f4aef9-2f17-421d-84db-5c9814bf4a79', + netRevenue: false, + meta: Object.freeze({ + advertiser: '105600', + width: 300, + height: 250, + isCustom: '1', + progressBar: false, + mpuSrc: '//images.intellitxt.com/a/105600/Genpact/genpact.jpg', + clickURL: '{{click}}' + }) + }] + }; + + const interpretedResponse = spec.interpretResponse(prebidServerResponse, {}); + + expect(interpretedResponse).to.be.a('array'); + expect(interpretedResponse.length).to.equal(1); + + const interpretedBid = interpretedResponse[0]; + + expect(interpretedBid.mediaType).to.equal('banner'); + expect(interpretedBid.requestId).to.equal('12345'); + expect(interpretedBid.cpm).to.equal(1); + expect(interpretedBid.currency).to.equal('USD'); + expect(interpretedBid.width).to.equal(640); + expect(interpretedBid.height).to.equal(240); + expect(interpretedBid.ad).to.equal(BANNER_AD); + expect(interpretedBid.ttl).to.equal(300); + expect(interpretedBid.creativeId).to.equal('86f4aef9-2f17-421d-84db-5c9814bf4a79'); + expect(interpretedBid.netRevenue).to.be.false; + expect(interpretedBid.meta).to.deep.equal(prebidServerResponse.body[0].meta); + expect(interpretedBid.renderer).to.be.undefined; + expect(interpretedBid.adResponse).to.deep.equal(prebidServerResponse); + }); + + it('returns a valid Prebid API response object for a video Prebid Server response', function () { + const prebidServerResponse = Object.freeze({ + body: [{ + mediaType: 'video', + requestId: '67890', + cpm: 2, + currency: 'USD', + width: 600, + height: 300, + ad: VIDEO_AD, + ttl: 300, + creativeId: '248e8e0c-2f17-421d-84db-5c9814bf4a79', + netRevenue: false, + meta: { + advertiser: '105600', + width: 300, + height: 250, + isCustom: '1', + progressBar: false, + mpuSrc: '//images.intellitxt.com/a/105600/Genpact/genpact.jpg', + clickURL: '{{click}}' + }, + vastUrl: 'https://www.example.com/myVastVideo' + }] + }); + + const interpretedResponse = spec.interpretResponse(prebidServerResponse, {}); + + expect(interpretedResponse).to.be.a('array'); + expect(interpretedResponse.length).to.equal(1); + + const interpretedBid = interpretedResponse[0]; + + expect(interpretedBid.mediaType).to.equal('video'); + expect(interpretedBid.requestId).to.equal('67890'); + expect(interpretedBid.cpm).to.equal(2); + expect(interpretedBid.currency).to.equal('USD'); + expect(interpretedBid.width).to.equal(600); + expect(interpretedBid.height).to.equal(300); + expect(interpretedBid.ad).to.equal(VIDEO_AD); + expect(interpretedBid.ttl).to.equal(300); + expect(interpretedBid.creativeId).to.equal('248e8e0c-2f17-421d-84db-5c9814bf4a79'); + expect(interpretedBid.netRevenue).to.be.false; + expect(interpretedBid.meta).to.deep.equal(prebidServerResponse.body[0].meta); + expect(interpretedBid.renderer).to.be.undefined; + expect(interpretedBid.adResponse).to.deep.equal(prebidServerResponse); + }); + + it('returns a valid Prebid API response object for a native Prebid Server response', function () { + const prebidServerResponse = Object.freeze({ + body: [{ + mediaType: 'native', + requestId: '13579', + cpm: 3, + currency: 'USD', + width: 240, + height: 300, + ad: 'https://www.example.com/native-display.html', + ttl: 300, + creativeId: 'd28e8e0c-2f17-421d-84db-5c9814bf4a81', + netRevenue: false, + meta: {}, + title: 'Test native ad bid for 13579', + sponsoredBy: 'Vibrant Media Ltd', + clickUrl: 'https://www.example.com/native-ct.html', + image: { + url: 'https://www.example.com/native-display.html', + width: 240, + height: 300 + } + }] + }); + + const interpretedResponse = spec.interpretResponse(prebidServerResponse, {}); + + expect(interpretedResponse).to.be.a('array'); + expect(interpretedResponse.length).to.equal(1); + + const interpretedBid = interpretedResponse[0]; + + expect(interpretedBid.mediaType).to.equal('native'); + expect(interpretedBid.requestId).to.equal('13579'); + expect(interpretedBid.cpm).to.equal(3); + expect(interpretedBid.currency).to.equal('USD'); + expect(interpretedBid.width).to.equal(240); + expect(interpretedBid.height).to.equal(300); + expect(interpretedBid.ad).to.equal('https://www.example.com/native-display.html'); + expect(interpretedBid.ttl).to.equal(300); + expect(interpretedBid.creativeId).to.equal('d28e8e0c-2f17-421d-84db-5c9814bf4a81'); + expect(interpretedBid.netRevenue).to.be.false; + expect(interpretedBid.meta).to.deep.equal(prebidServerResponse.body[0].meta); + expect(interpretedBid.renderer).to.be.undefined; + expect(interpretedBid.adResponse).to.deep.equal(prebidServerResponse); + }); + + it('returns a valid Prebid API response object for a multi-bid Prebid Server response', function () { + const prebidServerResponse = Object.freeze({ + body: [ + { + mediaType: 'banner', + requestId: '12345', + cpm: 3, + currency: 'USD', + width: 640, + height: 240, + ad: BANNER_AD, + ttl: 300, + creativeId: '86f4aef9-2f17-421d-84db-5c9814bf4a79', + netRevenue: false, + meta: { + advertiser: '105600', + width: 300, + height: 250, + isCustom: '1', + progressBar: false, + mpuSrc: '//images.intellitxt.com/a/105600/Genpact/genpact.jpg', + clickURL: '{{click}}' + } + }, + { + mediaType: 'video', + requestId: '67890', + cpm: 4, + currency: 'USD', + width: 300, + height: 300, + ad: VIDEO_AD, + ttl: 300, + creativeId: 'd28e8e0c-2f17-421d-84db-5c9814bf4a79', + netRevenue: false, + meta: { + advertiser: '105600', + width: 300, + height: 250, + isCustom: '1', + progressBar: false, + mpuSrc: '//images.intellitxt.com/a/105600/Genpact/genpact.jpg', + clickURL: '{{click}}' + }, + vastUrl: 'https://www.example.com/myVastVideo' + }, + { + mediaType: 'native', + requestId: '13579', + cpm: 5, + currency: 'USD', + width: 640, + height: 240, + ad: 'https://www.example.com/native-display.html', + ttl: 300, + creativeId: '888e8e0c-2f17-421d-84db-5c9814bf4a81', + netRevenue: false, + meta: {}, + title: 'Test native ad bid for 13579', + sponsoredBy: 'Vibrant Media Ltd', + clickUrl: 'https://www.example.com/native-ct.html', + image: { + url: 'https://www.example.com/native-display.html', + width: 640, + height: 240 + } + } + ] + }); + + const interpretedResponse = spec.interpretResponse(prebidServerResponse, {}); + + expect(interpretedResponse).to.be.a('array'); + expect(interpretedResponse.length).to.equal(3); + + const interpretedBannerBid = interpretedResponse[0]; + + expect(interpretedBannerBid.mediaType).to.equal('banner'); + expect(interpretedBannerBid.requestId).to.equal('12345'); + expect(interpretedBannerBid.cpm).to.equal(3); + expect(interpretedBannerBid.currency).to.equal('USD'); + expect(interpretedBannerBid.width).to.equal(640); + expect(interpretedBannerBid.height).to.equal(240); + expect(interpretedBannerBid.ad).to.equal(BANNER_AD); + expect(interpretedBannerBid.ttl).to.equal(300); + expect(interpretedBannerBid.creativeId).to.equal('86f4aef9-2f17-421d-84db-5c9814bf4a79'); + expect(interpretedBannerBid.netRevenue).to.be.false; + expect(interpretedBannerBid.meta).to.deep.equal(prebidServerResponse.body[0].meta); + expect(interpretedBannerBid.renderer).to.be.undefined; + expect(interpretedBannerBid.adResponse).to.deep.equal(prebidServerResponse); + + const interpretedVideoBid = interpretedResponse[1]; + + expect(interpretedVideoBid.mediaType).to.equal('video'); + expect(interpretedVideoBid.requestId).to.equal('67890'); + expect(interpretedVideoBid.cpm).to.equal(4); + expect(interpretedVideoBid.currency).to.equal('USD'); + expect(interpretedVideoBid.width).to.equal(300); + expect(interpretedVideoBid.height).to.equal(300); + expect(interpretedVideoBid.ad).to.equal(VIDEO_AD); + expect(interpretedVideoBid.ttl).to.equal(300); + expect(interpretedVideoBid.creativeId).to.equal('d28e8e0c-2f17-421d-84db-5c9814bf4a79'); + expect(interpretedVideoBid.netRevenue).to.be.false; + expect(interpretedVideoBid.meta).to.deep.equal(prebidServerResponse.body[1].meta); + expect(interpretedVideoBid.renderer).to.be.undefined; + expect(interpretedVideoBid.adResponse).to.deep.equal(prebidServerResponse); + + const interpretedNativeBid = interpretedResponse[2]; + + expect(interpretedNativeBid.mediaType).to.equal('native'); + expect(interpretedNativeBid.requestId).to.equal('13579'); + expect(interpretedNativeBid.cpm).to.equal(5); + expect(interpretedNativeBid.currency).to.equal('USD'); + expect(interpretedNativeBid.width).to.equal(640); + expect(interpretedNativeBid.height).to.equal(240); + expect(interpretedNativeBid.ad).to.equal('https://www.example.com/native-display.html'); + expect(interpretedNativeBid.ttl).to.equal(300); + expect(interpretedNativeBid.creativeId).to.equal('888e8e0c-2f17-421d-84db-5c9814bf4a81'); + expect(interpretedNativeBid.netRevenue).to.be.false; + expect(interpretedNativeBid.meta).to.deep.equal(prebidServerResponse.body[2].meta); + expect(interpretedNativeBid.renderer).to.be.undefined; + expect(interpretedNativeBid.adResponse).to.deep.equal(prebidServerResponse); + }); + }); + + describe('Flow tests', function () { + describe('For successive API calls to the public functions', function () { + it('should succeed with one media type per bid', function () { + const transformedBannerBidParams = spec.transformBidParams(VALID_BANNER_BID_PARAMS); + const transformedVideoBidParams = spec.transformBidParams(VALID_VIDEO_BID_PARAMS); + const transformedNativeBidParams = spec.transformBidParams(VALID_NATIVE_BID_PARAMS); + + const bannerBid = { + bidder: 'vibrantmedia', + params: transformedBannerBidParams, + mediaTypes: { + banner: { + sizes: DEFAULT_BID_SIZES, + }, + }, + adUnitCode: 'banner-div', + bidId: '30b31c1838de11', + bidderRequestId: '22edbae2733bf6', + placementId: '293857832abfef', + auctionId: '1d1a030790a475', + }; + const videoBid = { + bidder: 'vibrantmedia', + params: transformedVideoBidParams, + mediaTypes: { + video: { + context: OUTSTREAM, + sizes: DEFAULT_BID_SIZES, + }, + }, + adUnitCode: 'video-div', + bidId: '30b31c1838de15', + bidderRequestId: '22edbae2733bf6', + placementId: '293857832abfef', + auctionId: '1d1a030790a475', + }; + const nativeBid = { + bidder: 'vibrantmedia', + params: transformedNativeBidParams, + mediaTypes: { + native: { + image: { + required: true, + sizes: [300, 250] + }, + title: { + required: true + }, + sponsoredBy: { + required: true + }, + clickUrl: { + required: true + } + } + }, + adUnitCode: 'native-div', + bidId: '30b31c1838de12', + bidderRequestId: '22edbae2733bf6', + placementId: '293857832abfef', + auctionId: '1d1a030790a475', + }; + + expect(spec.isBidRequestValid(bannerBid)).to.be.true; + expect(spec.isBidRequestValid(videoBid)).to.be.true; + expect(spec.isBidRequestValid(nativeBid)).to.be.true; + + const bidRequests = [bannerBid, videoBid, nativeBid]; + const validBidderRequest = getValidBidderRequest(bidRequests); + const serverRequest = spec.buildRequests(bidRequests, validBidderRequest); + expect(serverRequest.method).to.equal('POST'); + expect(serverRequest.url).to.equal(EXPECTED_PREBID_SERVER_URL); + + const payload = JSON.parse(serverRequest.data); + expect(payload.biddata).to.exist; + expect(payload.biddata.length).to.equal(3); + expect(payload.biddata[0]).to.exist; + expect(payload.biddata[0].code).to.equal(bannerBid.adUnitCode); + expect(payload.biddata[0].id).to.equal(bannerBid.placementId); + expect(payload.biddata[0].bidder).to.equal(bannerBid.bidder); + expect(payload.biddata[0].mediaTypes[BANNER]).to.exist; + expect(payload.biddata[0].mediaTypes[BANNER]).to.deep.equal({ + sizes: DEFAULT_BID_SIZES, + }); + expect(payload.biddata[1]).to.exist; + expect(payload.biddata[1].code).to.equal(videoBid.adUnitCode); + expect(payload.biddata[1].id).to.equal(videoBid.placementId); + expect(payload.biddata[1].bidder).to.equal(videoBid.bidder); + expect(payload.biddata[1].mediaTypes[VIDEO]).to.exist; + expect(payload.biddata[1].mediaTypes[VIDEO]).to.deep.equal({ + context: OUTSTREAM, + sizes: DEFAULT_BID_SIZES + }); + expect(payload.biddata[2]).to.exist; + expect(payload.biddata[2].code).to.equal(nativeBid.adUnitCode); + expect(payload.biddata[2].id).to.equal(nativeBid.placementId); + expect(payload.biddata[2].bidder).to.equal(nativeBid.bidder); + expect(payload.biddata[2].mediaTypes[NATIVE]).to.exist; + expect(payload.biddata[2].mediaTypes[NATIVE]).to.deep.equal(nativeBid.mediaTypes.native); + + // From here, the API would call the Prebid Server and call interpretResponse, which is covered by tests elsewhere + }); + + it('should succeed with multiple media types for a single bid', function () { + const bidParams = spec.transformBidParams(VALID_VIDEO_BID_PARAMS); + const bid = { + bidder: 'vibrantmedia', + params: bidParams, + mediaTypes: { + banner: { + sizes: DEFAULT_BID_SIZES + }, + video: { + context: OUTSTREAM, + sizes: DEFAULT_BID_SIZES + }, + native: { + sizes: DEFAULT_BID_SIZES + } + }, + adUnitCode: 'test-div', + bidId: '30b31c1838de13', + bidderRequestId: '22edbae2733bf6', + placementId: '293857832abfef', + auctionId: '1d1a030790a475', + }; + + expect(spec.isBidRequestValid(bid)).to.be.true; + + const bidRequests = [bid]; + const validBidderRequest = getValidBidderRequest(bidRequests); + const serverRequest = spec.buildRequests(bidRequests, validBidderRequest); + expect(serverRequest.method).to.equal('POST'); + expect(serverRequest.url).to.equal(EXPECTED_PREBID_SERVER_URL); + + const payload = JSON.parse(serverRequest.data); + expect(payload.biddata).to.exist; + expect(payload.biddata.length).to.equal(1); + expect(payload.biddata[0]).to.exist; + expect(payload.biddata[0].code).to.equal(bid.adUnitCode); + expect(payload.biddata[0].id).to.equal(bid.placementId); + expect(payload.biddata[0].bidder).to.equal(bid.bidder); + expect(payload.biddata[0].mediaTypes[BANNER]).to.exist; + expect(payload.biddata[0].mediaTypes[BANNER]).to.deep.equal({ + sizes: DEFAULT_BID_SIZES, + }); + expect(payload.biddata[0].mediaTypes[VIDEO]).to.exist; + expect(payload.biddata[0].mediaTypes[VIDEO]).to.deep.equal({ + context: OUTSTREAM, + sizes: DEFAULT_BID_SIZES, + }); + expect(payload.biddata[0].mediaTypes[NATIVE]).to.exist; + expect(payload.biddata[0].mediaTypes[NATIVE]).to.deep.equal({ + sizes: DEFAULT_BID_SIZES, + }); + + // From here, the API would call the Prebid Server and call interpretResponse, which is covered by tests elsewhere + }); + }); + }); +}); diff --git a/test/spec/modules/vidazooBidAdapter_spec.js b/test/spec/modules/vidazooBidAdapter_spec.js index 35f510fd6ee..0b5dadce09f 100644 --- a/test/spec/modules/vidazooBidAdapter_spec.js +++ b/test/spec/modules/vidazooBidAdapter_spec.js @@ -37,7 +37,8 @@ const BID = { 'transactionId': 'c881914b-a3b5-4ecf-ad9c-1c2f37c6aabf', 'sizes': [[300, 250], [300, 600]], 'bidderRequestId': '1fdb5ff1b6eaa7', - 'requestId': 'b0777d85-d061-450e-9bc7-260dd54bbb7a' + 'requestId': 'b0777d85-d061-450e-9bc7-260dd54bbb7a', + 'schain': 'a0819c69-005b-41ed-af06-1be1e0aefefc' }; const BIDDER_REQUEST = { @@ -163,6 +164,7 @@ describe('VidazooBidAdapter', function () { uniqueDealId: `${hashUrl}_${Date.now().toString()}`, bidderVersion: adapter.version, prebidVersion: version, + schain: BID.schain, res: `${window.top.screen.width}x${window.top.screen.height}`, 'ext.param1': 'loremipsum', 'ext.param2': 'dolorsitamet', diff --git a/test/spec/modules/vidoomyBidAdapter_spec.js b/test/spec/modules/vidoomyBidAdapter_spec.js index 52f522bdcfc..8aa127faef2 100644 --- a/test/spec/modules/vidoomyBidAdapter_spec.js +++ b/test/spec/modules/vidoomyBidAdapter_spec.js @@ -16,7 +16,8 @@ describe('vidoomyBidAdapter', function() { 'bidder': 'vidoomy', 'params': { pid: '123123', - id: '123123' + id: '123123', + bidfloor: 0.5 }, 'adUnitCode': 'code', 'sizes': [[300, 250]] @@ -32,6 +33,11 @@ describe('vidoomyBidAdapter', function() { expect(spec.isBidRequestValid(bid)).to.equal(false); }); + it('should return false when bidfloor is invalid', function () { + bid.params.bidfloor = 'not a number'; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return false when id is empty', function () { bid.params.id = ''; expect(spec.isBidRequestValid(bid)).to.equal(false); diff --git a/test/spec/modules/viewability_spec.js b/test/spec/modules/viewability_spec.js new file mode 100644 index 00000000000..ab2753daf53 --- /dev/null +++ b/test/spec/modules/viewability_spec.js @@ -0,0 +1,280 @@ +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import * as utils from 'src/utils.js'; +import * as viewability from 'modules/viewability.js'; + +describe('viewability test', () => { + describe('start measurement', () => { + let sandbox; + let intersectionObserverStub; + let setTimeoutStub; + let observeCalled; + let unobserveCalled; + let ti = 1; + beforeEach(() => { + observeCalled = false; + unobserveCalled = false; + sandbox = sinon.sandbox.create(); + + let fakeIntersectionObserver = (stateChange, options) => { + return { + observe: (element) => { + observeCalled = true; + stateChange([{ isIntersecting: true }]); + }, + unobserve: (element) => { + unobserveCalled = true; + }, + }; + }; + + intersectionObserverStub = sandbox.stub(window, 'IntersectionObserver').callsFake(fakeIntersectionObserver); + setTimeoutStub = sandbox.stub(window, 'setTimeout').callsFake((callback, timeout) => { + callback(); + return ti++; + }); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should trigger appropriate callbacks', () => { + viewability.startMeasurement('0', {}, { method: 'img', value: 'http://my.tracker/123' }, { inViewThreshold: 0.5, timeInView: 1000 }); + + sinon.assert.called(intersectionObserverStub); + sinon.assert.called(setTimeoutStub); + expect(observeCalled).to.equal(true); + expect(unobserveCalled).to.equal(true); + }); + + it('should trigger img tracker', () => { + let triggerPixelSpy = sandbox.spy(utils, ['triggerPixel']); + viewability.startMeasurement('1', {}, { method: 'img', value: 'http://my.tracker/123' }, { inViewThreshold: 0.5, timeInView: 1000 }); + expect(triggerPixelSpy.callCount).to.equal(1); + }); + + it('should trigger js tracker', () => { + let insertHtmlIntoIframeSpy = sandbox.spy(utils, ['insertHtmlIntoIframe']); + viewability.startMeasurement('2', {}, { method: 'js', value: 'http://my.tracker/123.js' }, { inViewThreshold: 0.5, timeInView: 1000 }); + expect(insertHtmlIntoIframeSpy.callCount).to.equal(1); + }); + + it('should trigger callback tracker', () => { + let callbackFired = false; + viewability.startMeasurement('3', {}, { method: 'callback', value: () => { callbackFired = true; } }, { inViewThreshold: 0.5, timeInView: 1000 }); + expect(callbackFired).to.equal(true); + }); + + it('should check for vid uniqueness', () => { + let logWarnSpy = sandbox.spy(utils, 'logWarn'); + viewability.startMeasurement('4', {}, { method: 'js', value: 'http://my.tracker/123.js' }, { inViewThreshold: 0.5, timeInView: 1000 }); + expect(logWarnSpy.callCount).to.equal(0); + + viewability.startMeasurement('4', {}, { method: 'js', value: 'http://my.tracker/123.js' }, { inViewThreshold: 0.5, timeInView: 1000 }); + expect(logWarnSpy.callCount).to.equal(1); + expect(logWarnSpy.calledWith(`${viewability.MODULE_NAME}: must provide an unregistered vid`, '4')).to.equal(true); + }); + + it('should check for valid criteria', () => { + let logWarnSpy = sandbox.spy(utils, 'logWarn'); + viewability.startMeasurement('5', {}, { method: 'js', value: 'http://my.tracker/123.js' }, { timeInView: 1000 }); + expect(logWarnSpy.callCount).to.equal(1); + expect(logWarnSpy.calledWith(`${viewability.MODULE_NAME}: missing criteria`, { timeInView: 1000 })).to.equal(true); + }); + + it('should check for valid tracker', () => { + let logWarnSpy = sandbox.spy(utils, 'logWarn'); + viewability.startMeasurement('6', {}, { method: 'callback', value: 'string' }, { inViewThreshold: 0.5, timeInView: 1000 }); + expect(logWarnSpy.callCount).to.equal(1); + expect(logWarnSpy.calledWith(`${viewability.MODULE_NAME}: invalid tracker`, { method: 'callback', value: 'string' })).to.equal(true); + }); + + it('should check if element provided', () => { + let logWarnSpy = sandbox.spy(utils, 'logWarn'); + viewability.startMeasurement('7', undefined, { method: 'js', value: 'http://my.tracker/123.js' }, { timeInView: 1000 }); + expect(logWarnSpy.callCount).to.equal(1); + expect(logWarnSpy.calledWith(`${viewability.MODULE_NAME}: no html element provided`)).to.equal(true); + }); + }); + + describe('stop measurement', () => { + let sandbox; + let intersectionObserverStub; + let setTimeoutStub; + let clearTimeoutStub; + let observeCalled; + let unobserveCalled; + let stateChangeBackup; + let ti = 1; + beforeEach(() => { + observeCalled = false; + unobserveCalled = false; + sandbox = sinon.sandbox.create(); + + let fakeIntersectionObserver = (stateChange, options) => { + return { + observe: (element) => { + stateChangeBackup = stateChange; + observeCalled = true; + stateChange([{ isIntersecting: true }]); + }, + unobserve: (element) => { + unobserveCalled = true; + }, + }; + }; + + intersectionObserverStub = sandbox.stub(window, 'IntersectionObserver').callsFake(fakeIntersectionObserver); + setTimeoutStub = sandbox.stub(window, 'setTimeout').callsFake((callback, timeout) => { + // skipping the callback + return ti++; + }); + clearTimeoutStub = sandbox.stub(window, 'clearTimeout').callsFake((timeoutId) => { }); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should clear the timeout', () => { + viewability.startMeasurement('10', {}, { method: 'img', value: 'http://my.tracker/123' }, { inViewThreshold: 0.5, timeInView: 1000 }); + stateChangeBackup([{ isIntersecting: false }]); + sinon.assert.called(intersectionObserverStub); + sinon.assert.called(setTimeoutStub); + sinon.assert.called(clearTimeoutStub); + expect(observeCalled).to.equal(true); + }); + + it('should unobserve', () => { + viewability.startMeasurement('11', {}, { method: 'img', value: 'http://my.tracker/123' }, { inViewThreshold: 0.5, timeInView: 1000 }); + sinon.assert.called(intersectionObserverStub); + sinon.assert.called(setTimeoutStub); + expect(observeCalled).to.equal(true); + expect(unobserveCalled).to.equal(false); + + viewability.stopMeasurement('11'); + expect(unobserveCalled).to.equal(true); + sinon.assert.called(clearTimeoutStub); + }); + + it('should check for vid existence', () => { + let logWarnSpy = sandbox.spy(utils, 'logWarn'); + viewability.stopMeasurement('100'); + expect(logWarnSpy.callCount).to.equal(1); + expect(logWarnSpy.calledWith(`${viewability.MODULE_NAME}: must provide a registered vid`, '100')).to.equal(true); + }); + }); + + describe('handle creative messages', () => { + let sandbox; + let intersectionObserverStub; + let setTimeoutStub; + let observeCalled; + let unobserveCalled; + let ti = 1; + let getElementsByTagStub; + let getElementByIdStub; + + let fakeContentWindow = {}; + beforeEach(() => { + observeCalled = false; + unobserveCalled = false; + sandbox = sinon.sandbox.create(); + + let fakeIntersectionObserver = (stateChange, options) => { + return { + observe: (element) => { + observeCalled = true; + stateChange([{ isIntersecting: true }]); + }, + unobserve: (element) => { + unobserveCalled = true; + }, + }; + }; + + intersectionObserverStub = sandbox.stub(window, 'IntersectionObserver').callsFake(fakeIntersectionObserver); + setTimeoutStub = sandbox.stub(window, 'setTimeout').callsFake((callback, timeout) => { + callback(); + return ti++; + }); + + getElementsByTagStub = sandbox.stub(document, 'getElementsByTagName').callsFake((tagName) => { + return [{ + contentWindow: fakeContentWindow, + }]; + }); + getElementByIdStub = sandbox.stub(document, 'getElementById').callsFake((id) => { + return {}; + }); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should find element by contentWindow', () => { + let viewabilityRecord = { + vid: 1000, + tracker: { + value: 'http://my.tracker/123', + method: 'img', + }, + criteria: { inViewThreshold: 0.5, timeInView: 1000 }, + message: 'Prebid Viewability', + action: 'startMeasurement', + }; + let data = JSON.stringify(viewabilityRecord); + + viewability.receiveMessage({ + data: data, + source: fakeContentWindow, + }); + + sinon.assert.called(getElementsByTagStub); + sinon.assert.called(intersectionObserverStub); + sinon.assert.called(setTimeoutStub); + expect(observeCalled).to.equal(true); + expect(unobserveCalled).to.equal(true); + }); + + it('should find element by id', () => { + let viewabilityRecord = { + vid: 1001, + tracker: { + value: 'http://my.tracker/123', + method: 'img', + }, + criteria: { inViewThreshold: 0.5, timeInView: 1000 }, + message: 'Prebid Viewability', + action: 'startMeasurement', + elementId: '1', + }; + let data = JSON.stringify(viewabilityRecord); + viewability.receiveMessage({ + data: data, + }); + + sinon.assert.called(getElementByIdStub); + sinon.assert.called(intersectionObserverStub); + sinon.assert.called(setTimeoutStub); + expect(observeCalled).to.equal(true); + expect(unobserveCalled).to.equal(true); + }); + + it('should stop measurement', () => { + let viewabilityRecord = { + vid: 1001, + message: 'Prebid Viewability', + action: 'stopMeasurement', + }; + let data = JSON.stringify(viewabilityRecord); + viewability.receiveMessage({ + data: data, + }); + + expect(unobserveCalled).to.equal(true); + }); + }); +}); diff --git a/test/spec/modules/visxBidAdapter_spec.js b/test/spec/modules/visxBidAdapter_spec.js index f0c3007b4c3..4aaaf996f58 100755 --- a/test/spec/modules/visxBidAdapter_spec.js +++ b/test/spec/modules/visxBidAdapter_spec.js @@ -3,6 +3,7 @@ import { spec } from 'modules/visxBidAdapter.js'; import { config } from 'src/config.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; import * as utils from 'src/utils.js'; +import { makeSlot } from '../integration/faker/googletag.js'; describe('VisxAdapter', function () { const adapter = newBidder(spec); @@ -17,7 +18,7 @@ describe('VisxAdapter', function () { let bid = { 'bidder': 'visx', 'params': { - 'uid': '903536' + 'uid': 903536 }, 'adUnitCode': 'adunit-code', 'sizes': [[300, 250], [300, 600]], @@ -39,6 +40,15 @@ describe('VisxAdapter', function () { expect(spec.isBidRequestValid(bid)).to.equal(false); }); + it('should return false when uid can not be parsed as number', function () { + let bid = Object.assign({}, bid); + delete bid.params; + bid.params = { + 'uid': 'sdvsdv' + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('it should fail on invalid video bid', function () { let videoBid = Object.assign({}, bid); videoBid.mediaTypes = { @@ -101,7 +111,7 @@ describe('VisxAdapter', function () { { 'bidder': 'visx', 'params': { - 'uid': 903535 + 'uid': '903535' }, 'adUnitCode': 'adunit-code-2', 'sizes': [[728, 90], [300, 250]], @@ -145,17 +155,17 @@ describe('VisxAdapter', function () { const expectedFullImps = [{ 'id': '30b31c1838de1e', 'banner': {'format': [{'w': 300, 'h': 250}, {'w': 300, 'h': 600}]}, - 'ext': {'bidder': {'uid': 903535}} + 'ext': {'bidder': {'uid': 903535, 'adslotExists': false}} }, { 'id': '3150ccb55da321', 'banner': {'format': [{'w': 728, 'h': 90}, {'w': 300, 'h': 250}]}, - 'ext': {'bidder': {'uid': 903535}} + 'ext': {'bidder': {'uid': 903535, 'adslotExists': false}} }, { 'id': '42dbe3a7168a6a', 'banner': {'format': [{'w': 300, 'h': 250}, {'w': 300, 'h': 600}]}, - 'ext': {'bidder': {'uid': 903536}} + 'ext': {'bidder': {'uid': 903536, 'adslotExists': false}} }, { 'id': '39a4e3a7168a6a', @@ -452,7 +462,122 @@ describe('VisxAdapter', function () { 'imp': [{ 'id': '39aff3a7169a6a', 'banner': {'format': [{'w': 300, 'h': 250}, {'w': 300, 'h': 600}]}, - 'ext': {'bidder': {'uid': 903538}} + 'ext': {'bidder': {'uid': 903538, 'adslotExists': false}} + }], + 'tmax': 3000, + 'cur': ['EUR'], + 'source': { + 'ext': { + 'wrapperType': 'Prebid_js', + 'wrapperVersion': '$prebid.version$' + } + }, + 'site': {'page': referrer} + }); + }); + }); + + describe('buildRequests (check ad slot exists)', function () { + function parseRequest(url) { + const res = {}; + (url.split('?')[1] || '').split('&').forEach((it) => { + const couple = it.split('='); + res[couple[0]] = decodeURIComponent(couple[1]); + }); + return res; + } + const bidderRequest = { + timeout: 3000, + refererInfo: { + referer: 'https://example.com' + } + }; + const referrer = bidderRequest.refererInfo.referer; + const bidRequests = [ + { + 'bidder': 'visx', + 'params': { + 'uid': 903535 + }, + 'adUnitCode': 'visx-adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }, + { + 'bidder': 'visx', + 'params': { + 'uid': 903535 + }, + 'adUnitCode': 'visx-adunit-code-2', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + } + ]; + let sandbox; + let documentStub; + + before(function() { + sandbox = sinon.sandbox.create(); + documentStub = sandbox.stub(document, 'getElementById'); + documentStub.withArgs('visx-adunit-code-1').returns({ + id: 'visx-adunit-code-1' + }); + documentStub.withArgs('visx-adunit-element-2').returns({ + id: 'visx-adunit-element-2' + }); + }); + + after(function() { + sandbox.restore(); + }); + + it('should find ad slot by ad unit code as element id', function () { + const request = spec.buildRequests([bidRequests[0]], bidderRequest); + const payload = parseRequest(request.url); + expect(payload).to.be.an('object'); + expect(payload).to.have.property('auids', '903535'); + + const postData = request.data; + expect(postData).to.be.an('object'); + expect(postData).to.deep.equal({ + 'id': '22edbae2733bf6', + 'imp': [{ + 'id': '30b31c1838de1e', + 'banner': {'format': [{'w': 300, 'h': 250}, {'w': 300, 'h': 600}]}, + 'ext': {'bidder': {'uid': 903535, 'adslotExists': true}} + }], + 'tmax': 3000, + 'cur': ['EUR'], + 'source': { + 'ext': { + 'wrapperType': 'Prebid_js', + 'wrapperVersion': '$prebid.version$' + } + }, + 'site': {'page': referrer} + }); + }); + + it('should find ad slot by ad unit code as adUnitPath', function () { + makeSlot({code: 'visx-adunit-code-2', divId: 'visx-adunit-element-2'}); + + const request = spec.buildRequests([bidRequests[1]], bidderRequest); + const payload = parseRequest(request.url); + expect(payload).to.be.an('object'); + expect(payload).to.have.property('auids', '903535'); + + const postData = request.data; + expect(postData).to.be.an('object'); + expect(postData).to.deep.equal({ + 'id': '22edbae2733bf6', + 'imp': [{ + 'id': '30b31c1838de1e', + 'banner': {'format': [{'w': 300, 'h': 250}, {'w': 300, 'h': 600}]}, + 'ext': {'bidder': {'uid': 903535, 'adslotExists': true}} }], 'tmax': 3000, 'cur': ['EUR'], @@ -1147,9 +1272,10 @@ describe('VisxAdapter', function () { }); it('onTimeout', function () { - const data = { timeout: 3000, bidId: '23423', params: { uid: 1 } }; + const data = [{ timeout: 3000, adUnitCode: 'adunit-code-1', auctionId: '1cbd2feafe5e8b', bidder: 'visx', bidId: '23423', params: [{ uid: '1' }] }]; + const expectedData = [{ ...data[0], params: [{ uid: 1 }] }]; spec.onTimeout(data); - expect(utils.triggerPixel.calledOnceWith('https://t.visx.net/track/bid_timeout//' + JSON.stringify(data))).to.equal(true); + expect(utils.triggerPixel.calledOnceWith('https://t.visx.net/track/bid_timeout//' + JSON.stringify(expectedData))).to.equal(true); }); }); diff --git a/test/spec/modules/weboramaRtdProvider_spec.js b/test/spec/modules/weboramaRtdProvider_spec.js index 155f26990a7..0f0af4efe2f 100644 --- a/test/spec/modules/weboramaRtdProvider_spec.js +++ b/test/spec/modules/weboramaRtdProvider_spec.js @@ -1,13 +1,27 @@ -import { setBigseaContextualProfile, weboramaSubmodule } from 'modules/weboramaRtdProvider.js'; -import { server } from 'test/mocks/xhr.js'; -import {config} from 'src/config.js'; - -const responseHeader = {'Content-Type': 'application/json'}; - -// TODO fix it - -describe('weboramaRtdProvider', function() { - describe('weboramaSubmodule', function() { +import { + weboramaSubmodule +} from 'modules/weboramaRtdProvider.js'; +import { + server +} from 'test/mocks/xhr.js'; +import { + storage, + DEFAULT_LOCAL_STORAGE_USER_PROFILE_KEY +} from '../../../modules/weboramaRtdProvider.js'; +import { + config +} from 'src/config.js'; +import { + getGlobal +} from 'src/prebidGlobal.js'; +import 'src/prebid.js'; + +const responseHeader = { + 'Content-Type': 'application/json' +}; + +describe('weboramaRtdProvider', function () { + describe('weboramaSubmodule', function () { it('successfully instantiates and call contextual api', function () { const moduleConfig = { params: { @@ -18,271 +32,967 @@ describe('weboramaRtdProvider', function() { } }; - expect(weboramaSubmodule.init(moduleConfig)).to.equal(true); - - let request = server.requests[0]; - - expect(request.url).to.equal('https://ctx.weborama.com/api/profile?token=foo&url=https%3A%2F%2Fprebid.org&'); - expect(request.method).to.equal('GET') + expect(weboramaSubmodule.init(moduleConfig)).to.equal(true); }); - it('instantiate without token should fail', function () { + + it('instantiate without contextual token should fail', function () { const moduleConfig = { params: { weboCtxConf: {} } }; - expect(weboramaSubmodule.init(moduleConfig)).to.equal(false); + expect(weboramaSubmodule.init(moduleConfig)).to.equal(false); }); - }); - describe('Add Contextual Data', function() { - beforeEach(function() { - let conf = { - site: { - ext: { - data: { - inventory: ['value1'] - } - } - }, - user: { - ext: { - data: { - visitor: ['value2'] - } - } - }, - cur: ['USD'] - }; - - config.setConfig({ortb2: conf}); - }); - it('should set targeting and ortb2 if omit setTargeting', function() { + it('instantiate with empty weboUserData conf should return true', function () { const moduleConfig = { params: { - weboCtxConf: { - token: 'foo', - targetURL: 'https://prebid.org', - setOrtb2: true, - } + weboUserDataConf: {} } }; - const data = { - webo_ctx: ['foo', 'bar'], - webo_ds: ['baz'], - }; - const adUnitsCodes = ['adunit1', 'adunit2']; - weboramaSubmodule.init(moduleConfig); + expect(weboramaSubmodule.init(moduleConfig)).to.equal(true); + }); + }); - let request = server.requests[0]; - request.respond(200, responseHeader, JSON.stringify(data)); + describe('Handle Set Targeting', function () { + let sandbox; - const targeting = weboramaSubmodule.getTargetingData(adUnitsCodes, moduleConfig); + beforeEach(function () { + sandbox = sinon.sandbox.create(); - expect(targeting).to.deep.equal({ - 'adunit1': data, - 'adunit2': data, - }); + storage.removeDataFromLocalStorage('webo_wam2gam_entry'); - const ortb2 = config.getConfig('ortb2'); + getGlobal().setConfig({ + ortb2: undefined + }); + }); - expect(ortb2.site.ext.data.webo_ctx).to.deep.equal(data.webo_ctx); - expect(ortb2.site.ext.data.webo_ds).to.deep.equal(data.webo_ds); + afterEach(function () { + sandbox.restore(); }); - it('should set targeting and ortb2 with setTargeting=true', function() { - const moduleConfig = { - params: { - weboCtxConf: { - token: 'foo', - targetURL: 'https://prebid.org', - setTargeting: true, - setOrtb2: true, + describe('Add Contextual Data', function () { + it('should set gam targeting and send to bidders by default', function () { + let onDataResponse = {}; + const moduleConfig = { + params: { + weboCtxConf: { + token: 'foo', + targetURL: 'https://prebid.org', + onData: (data, site) => { + onDataResponse = { + data: data, + site: site, + }; + }, + } } - } - }; - const data = { - webo_ctx: ['foo', 'bar'], - webo_ds: ['baz'], - }; - const adUnitsCodes = ['adunit1', 'adunit2']; - weboramaSubmodule.init(moduleConfig); - - let request = server.requests[0]; - request.respond(200, responseHeader, JSON.stringify(data)); - - const targeting = weboramaSubmodule.getTargetingData(adUnitsCodes, moduleConfig); - - expect(targeting).to.deep.equal({ - 'adunit1': data, - 'adunit2': data, + }; + const data = { + webo_ctx: ['foo', 'bar'], + webo_ds: ['baz'], + }; + const adUnitsCodes = ['adunit1', 'adunit2']; + const reqBidsConfigObj = { + adUnits: [{ + bids: [{ + bidder: 'smartadserver' + }, { + bidder: 'pubmatic' + }, { + bidder: 'appnexus' + }, { + bidder: 'rubicon' + }, { + bidder: 'other' + }] + }] + }; + + const onDoneSpy = sinon.spy(); + + expect(weboramaSubmodule.init(moduleConfig)).to.be.true; + weboramaSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, moduleConfig); + + let request = server.requests[0]; + + expect(request.method).to.equal('GET'); + expect(request.url).to.equal('https://ctx.weborama.com/api/profile?token=foo&url=https%3A%2F%2Fprebid.org&'); + expect(request.withCredentials).to.be.false; + + request.respond(200, responseHeader, JSON.stringify(data)); + + expect(onDoneSpy.calledOnce).to.be.true; + + const targeting = weboramaSubmodule.getTargetingData(adUnitsCodes, moduleConfig); + + expect(targeting).to.deep.equal({ + 'adunit1': data, + 'adunit2': data, + }); + + expect(reqBidsConfigObj.adUnits[0].bids.length).to.equal(5); + expect(reqBidsConfigObj.adUnits[0].bids[0].params.target).to.equal('webo_ctx=foo;webo_ctx=bar;webo_ds=baz'); + expect(reqBidsConfigObj.adUnits[0].bids[1].params.dctr).to.equal('webo_ctx=foo,bar|webo_ds=baz'); + expect(reqBidsConfigObj.adUnits[0].bids[2].params.keywords).to.deep.equal(data); + expect(reqBidsConfigObj.adUnits[0].bids[3].params).to.deep.equal({ + inventory: data + }); + expect(reqBidsConfigObj.adUnits[0].bids[4].ortb2).to.deep.equal({ + site: { + ext: { + data: data + }, + } + }); + expect(getGlobal().getConfig('ortb2')).to.deep.equal({ + site: { + ext: { + data: data + }, + } + }); + expect(onDataResponse).to.deep.equal({ + data: data, + site: true, + }); }); - const ortb2 = config.getConfig('ortb2'); - - expect(ortb2.site.ext.data.webo_ctx).to.deep.equal(data.webo_ctx); - expect(ortb2.site.ext.data.webo_ds).to.deep.equal(data.webo_ds); - }); - it('should set targeting and ortb2 only webo_ctx with setTargeting=true', function() { - const moduleConfig = { - params: { - weboCtxConf: { - token: 'foo', - targetURL: 'https://prebid.org', - setTargeting: true, - setOrtb2: true, + it('should set gam targeting but not send to bidders with setPrebidTargeting=true/sendToBidders=false', function () { + const moduleConfig = { + params: { + weboCtxConf: { + token: 'foo', + targetURL: 'https://prebid.org', + setPrebidTargeting: true, + sendToBidders: false, + } } - } - }; - const data = { - webo_ctx: ['foo', 'bar'], - }; - - const adUnitsCodes = ['adunit1', 'adunit2']; - weboramaSubmodule.init(moduleConfig); - - let request = server.requests[0]; - request.respond(200, responseHeader, JSON.stringify(data)); - - const targeting = weboramaSubmodule.getTargetingData(adUnitsCodes, moduleConfig); - - expect(targeting).to.deep.equal({ - 'adunit1': data, - 'adunit2': data, + }; + const data = { + webo_ctx: ['foo', 'bar'], + webo_ds: ['baz'], + }; + const adUnitsCodes = ['adunit1', 'adunit2']; + const reqBidsConfigObj = { + adUnits: [{ + bids: [{ + bidder: 'smartadserver', + params: { + target: 'foo=bar' + } + }, { + bidder: 'pubmatic', + params: { + dctr: 'foo=bar' + } + }, { + bidder: 'appnexus', + params: { + keywords: { + foo: ['bar'] + } + } + }, { + bidder: 'rubicon', + params: { + inventory: { + foo: 'bar', + }, + visitor: { + baz: 'bam', + } + } + }, { + bidder: 'other', + }] + }] + }; + const onDoneSpy = sinon.spy(); + + expect(weboramaSubmodule.init(moduleConfig)).to.be.true; + weboramaSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, moduleConfig); + + let request = server.requests[0]; + + expect(request.method).to.equal('GET'); + expect(request.url).to.equal('https://ctx.weborama.com/api/profile?token=foo&url=https%3A%2F%2Fprebid.org&'); + expect(request.withCredentials).to.be.false; + + request.respond(200, responseHeader, JSON.stringify(data)); + + expect(onDoneSpy.calledOnce).to.be.true; + + const targeting = weboramaSubmodule.getTargetingData(adUnitsCodes, moduleConfig); + + expect(targeting).to.deep.equal({ + 'adunit1': data, + 'adunit2': data, + }); + + expect(reqBidsConfigObj.adUnits[0].bids.length).to.equal(5); + expect(reqBidsConfigObj.adUnits[0].bids[0].params.target).to.equal('foo=bar'); + expect(reqBidsConfigObj.adUnits[0].bids[1].params.dctr).to.equal('foo=bar'); + expect(reqBidsConfigObj.adUnits[0].bids[2].params.keywords).to.deep.equal({ + foo: ['bar'] + }); + expect(reqBidsConfigObj.adUnits[0].bids[3].params).to.deep.equal({ + inventory: { + foo: 'bar', + }, + visitor: { + baz: 'bam', + } + }); + expect(reqBidsConfigObj.adUnits[0].bids[4].ortb2).to.be.undefined; + expect(getGlobal().getConfig('ortb2')).to.be.undefined; }); - const ortb2 = config.getConfig('ortb2'); + it('should set gam targeting but not send to bidders with (submodule override) setPrebidTargeting=true/(global) sendToBidders=false', function () { + let onDataResponse = {}; + const moduleConfig = { + params: { + setPrebidTargeting: false, + sendToBidders: false, + onData: (data, site) => { + onDataResponse = { + data: data, + site: site, + }; + }, + weboCtxConf: { + token: 'foo', + targetURL: 'https://prebid.org', + setPrebidTargeting: true, // submodule parameter will override module parameter + } + } + }; + const data = { + webo_ctx: ['foo', 'bar'], + webo_ds: ['baz'], + }; + const adUnitsCodes = ['adunit1', 'adunit2']; + const reqBidsConfigObj = { + adUnits: [{ + bids: [{ + bidder: 'smartadserver', + params: { + target: 'foo=bar' + } + }] + }] + }; + const onDoneSpy = sinon.spy(); + + expect(weboramaSubmodule.init(moduleConfig)).to.be.true; + weboramaSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, moduleConfig); + + let request = server.requests[0]; + + expect(request.method).to.equal('GET'); + expect(request.url).to.equal('https://ctx.weborama.com/api/profile?token=foo&url=https%3A%2F%2Fprebid.org&'); + expect(request.withCredentials).to.be.false; + + request.respond(200, responseHeader, JSON.stringify(data)); + + expect(onDoneSpy.calledOnce).to.be.true; + + const targeting = weboramaSubmodule.getTargetingData(adUnitsCodes, moduleConfig); + + expect(targeting).to.deep.equal({ + 'adunit1': data, + 'adunit2': data, + }); + + expect(reqBidsConfigObj.adUnits[0].bids.length).to.equal(1); + expect(getGlobal().getConfig('ortb2')).to.be.undefined; + + expect(onDataResponse).to.deep.equal({ + data: data, + site: true, + }); + }); - expect(ortb2.site.ext.data.webo_ctx).to.deep.equal(data.webo_ctx); - expect(ortb2.site.ext.data).to.not.have.property('webo_ds'); - }); - it('should set only targeting and not ortb2 with setTargeting=true and setOrtb2=false', function() { - const moduleConfig = { - params: { - weboCtxConf: { - token: 'foo', - targetURL: 'https://prebid.org', - setTargeting: true, - setOrtb2: false, + it('should not set gam targeting with setPrebidTargeting=false but send to bidders', function () { + const moduleConfig = { + params: { + weboCtxConf: { + token: 'foo', + targetURL: 'https://prebid.org', + setPrebidTargeting: false, + } } + }; + const data = { + webo_ctx: ['foo', 'bar'], + webo_ds: ['baz'], + }; + const adUnitsCodes = ['adunit1', 'adunit2']; + const reqBidsConfigObj = { + adUnits: [{ + bids: [{ + bidder: 'smartadserver', + params: { + target: 'foo=bar' + } + }, { + bidder: 'pubmatic', + params: { + dctr: 'foo=bar' + } + }, { + bidder: 'appnexus', + params: { + keywords: { + foo: ['bar'] + } + } + }, { + bidder: 'rubicon', + params: { + inventory: { + foo: 'bar', + }, + visitor: { + baz: 'bam', + } + } + }, { + bidder: 'other', + }] + }] } - }; - const data = { - webo_ctx: ['foo', 'bar'], - }; - - const adUnitsCodes = ['adunit1', 'adunit2']; - weboramaSubmodule.init(moduleConfig); - - let request = server.requests[0]; - request.respond(200, responseHeader, JSON.stringify(data)); + const onDoneSpy = sinon.spy(); - const targeting = weboramaSubmodule.getTargetingData(adUnitsCodes, moduleConfig); + expect(weboramaSubmodule.init(moduleConfig)).to.be.true; + weboramaSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, moduleConfig); - expect(targeting).to.deep.equal({ - 'adunit1': data, - 'adunit2': data, - }); + let request = server.requests[0]; - const ortb2 = config.getConfig('ortb2'); + expect(request.method).to.equal('GET'); + expect(request.url).to.equal('https://ctx.weborama.com/api/profile?token=foo&url=https%3A%2F%2Fprebid.org&'); + expect(request.withCredentials).to.be.false; - expect(ortb2.site.ext.data).to.not.have.property('webo_ctx'); - expect(ortb2.site.ext.data).to.not.have.property('webo_ds'); - }); - it('should set only targeting and not ortb2 with setTargeting=true and omit setOrtb2', function() { - const moduleConfig = { - params: { - weboCtxConf: { - token: 'foo', - targetURL: 'https://prebid.org', - setTargeting: true, - } - } - }; - const data = { - webo_ctx: ['foo', 'bar'], - }; + request.respond(200, responseHeader, JSON.stringify(data)); - const adUnitsCodes = ['adunit1', 'adunit2']; - weboramaSubmodule.init(moduleConfig); + expect(onDoneSpy.calledOnce).to.be.true; - let request = server.requests[0]; - request.respond(200, responseHeader, JSON.stringify(data)); + const targeting = weboramaSubmodule.getTargetingData(adUnitsCodes, moduleConfig); - const targeting = weboramaSubmodule.getTargetingData(adUnitsCodes, moduleConfig); + expect(targeting).to.deep.equal({}); - expect(targeting).to.deep.equal({ - 'adunit1': data, - 'adunit2': data, + expect(reqBidsConfigObj.adUnits[0].bids.length).to.equal(5); + expect(reqBidsConfigObj.adUnits[0].bids[0].params.target).to.equal('foo=bar;webo_ctx=foo;webo_ctx=bar;webo_ds=baz'); + expect(reqBidsConfigObj.adUnits[0].bids[1].params.dctr).to.equal('foo=bar|webo_ctx=foo,bar|webo_ds=baz'); + expect(reqBidsConfigObj.adUnits[0].bids[2].params.keywords).to.deep.equal({ + foo: ['bar'], + webo_ctx: ['foo', 'bar'], + webo_ds: ['baz'], + }); + expect(reqBidsConfigObj.adUnits[0].bids[3].params).to.deep.equal({ + inventory: { + foo: 'bar', + webo_ctx: ['foo', 'bar'], + webo_ds: ['baz'], + }, + visitor: { + baz: 'bam', + } + }); + expect(reqBidsConfigObj.adUnits[0].bids[4].ortb2).to.deep.equal({ + site: { + ext: { + data: data + }, + } + }); + expect(getGlobal().getConfig('ortb2')).to.deep.equal({ + site: { + ext: { + data: data + }, + } + }); }); - const ortb2 = config.getConfig('ortb2'); - - expect(ortb2.site.ext.data).to.not.have.property('webo_ctx'); - expect(ortb2.site.ext.data).to.not.have.property('webo_ds'); + it('should use default profile in case of api error', function () { + const defaultProfile = { + webo_ctx: ['baz'], + }; + let onDataResponse = {}; + const moduleConfig = { + params: { + weboCtxConf: { + token: 'foo', + targetURL: 'https://prebid.org', + setPrebidTargeting: true, + defaultProfile: defaultProfile, + onData: (data, site) => { + onDataResponse = { + data: data, + site: site, + }; + }, + } + } + }; + + const adUnitsCodes = ['adunit1', 'adunit2']; + const reqBidsConfigObj = { + adUnits: [{ + bids: [{ + bidder: 'smartadserver' + }, { + bidder: 'pubmatic' + }, { + bidder: 'appnexus' + }, { + bidder: 'rubicon' + }, { + bidder: 'other' + }] + }] + }; + const onDoneSpy = sinon.spy(); + + expect(weboramaSubmodule.init(moduleConfig)).to.be.true; + weboramaSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, moduleConfig); + + let request = server.requests[0]; + + expect(request.method).to.equal('GET'); + expect(request.url).to.equal('https://ctx.weborama.com/api/profile?token=foo&url=https%3A%2F%2Fprebid.org&'); + expect(request.withCredentials).to.be.false; + + request.respond(500, responseHeader); + + expect(onDoneSpy.calledOnce).to.be.true; + + const targeting = weboramaSubmodule.getTargetingData(adUnitsCodes, moduleConfig); + + expect(targeting).to.deep.equal({ + 'adunit1': defaultProfile, + 'adunit2': defaultProfile, + }); + + expect(reqBidsConfigObj.adUnits[0].bids.length).to.equal(5); + expect(reqBidsConfigObj.adUnits[0].bids[0].params.target).to.equal('webo_ctx=baz'); + expect(reqBidsConfigObj.adUnits[0].bids[1].params.dctr).to.equal('webo_ctx=baz'); + expect(reqBidsConfigObj.adUnits[0].bids[2].params.keywords).to.deep.equal(defaultProfile); + expect(reqBidsConfigObj.adUnits[0].bids[3].params).to.deep.equal({ + inventory: defaultProfile + }); + expect(reqBidsConfigObj.adUnits[0].bids[4].ortb2).to.deep.equal({ + site: { + ext: { + data: defaultProfile + }, + } + }); + expect(getGlobal().getConfig('ortb2')).to.deep.equal({ + site: { + ext: { + data: defaultProfile + }, + } + }); + expect(onDataResponse).to.deep.equal({ + data: defaultProfile, + site: true, + }); + }); }); - it('should set only ortb2 with setTargeting=false', function() { - const moduleConfig = { - params: { - weboCtxConf: { - token: 'foo', - targetURL: 'https://prebid.org', - setTargeting: false, - setOrtb2: true, + describe('Add user-centric data (WAM2GAM)', function () { + it('should set gam targeting from local storage and send to bidders by default', function () { + let onDataResponse = {}; + const moduleConfig = { + params: { + weboUserDataConf: { + accountID: 12345, + onData: (data, site) => { + onDataResponse = { + data: data, + site: site, + }; + }, + } } - } - }; - const data = { - webo_ctx: ['foo', 'bar'], - }; - const adUnitsCodes = ['adunit1', 'adunit2']; - weboramaSubmodule.init(moduleConfig); - - let request = server.requests[0]; - request.respond(200, responseHeader, JSON.stringify(data)); - - const targeting = weboramaSubmodule.getTargetingData(adUnitsCodes, moduleConfig); - - expect(targeting).to.deep.equal({}); - - const ortb2 = config.getConfig('ortb2'); - - expect(ortb2.site.ext.data.webo_ctx).to.deep.equal(data.webo_ctx); - expect(ortb2.site.ext.data).to.not.have.property('webo_ds'); - }); - it('should use default profile in case of api error', function() { - const defaultProfile = { - webo_ctx: ['baz'], - }; - const moduleConfig = { - params: { - weboCtxConf: { - token: 'foo', - targetURL: 'https://prebid.org', - setTargeting: true, - defaultProfile: defaultProfile, + }; + const data = { + webo_cs: ['foo', 'bar'], + webo_audiences: ['baz'], + }; + + const entry = { + targeting: data, + }; + + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'getDataFromLocalStorage') + .withArgs(DEFAULT_LOCAL_STORAGE_USER_PROFILE_KEY) + .returns(JSON.stringify(entry)); + + const adUnitsCodes = ['adunit1', 'adunit2']; + const reqBidsConfigObj = { + adUnits: [{ + bids: [{ + bidder: 'smartadserver' + }, { + bidder: 'pubmatic' + }, { + bidder: 'appnexus' + }, { + bidder: 'rubicon' + }, { + bidder: 'other' + }] + }] + }; + const onDoneSpy = sinon.spy(); + + expect(weboramaSubmodule.init(moduleConfig)).to.be.true; + weboramaSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, moduleConfig); + + expect(onDoneSpy.calledOnce).to.be.true; + + const targeting = weboramaSubmodule.getTargetingData(adUnitsCodes, moduleConfig); + + expect(targeting).to.deep.equal({ + 'adunit1': data, + 'adunit2': data, + }); + + expect(reqBidsConfigObj.adUnits[0].bids.length).to.equal(5); + expect(reqBidsConfigObj.adUnits[0].bids[0].params.target).to.equal('webo_cs=foo;webo_cs=bar;webo_audiences=baz'); + expect(reqBidsConfigObj.adUnits[0].bids[1].params.dctr).to.equal('webo_cs=foo,bar|webo_audiences=baz'); + expect(reqBidsConfigObj.adUnits[0].bids[2].params.keywords).to.deep.equal(data); + expect(reqBidsConfigObj.adUnits[0].bids[3].params).to.deep.equal({ + visitor: data + }); + expect(reqBidsConfigObj.adUnits[0].bids[4].ortb2).to.deep.equal({ + user: { + ext: { + data: data + }, } - } - }; - - const adUnitsCodes = ['adunit1', 'adunit2']; - weboramaSubmodule.init(moduleConfig); + }); + expect(getGlobal().getConfig('ortb2')).to.deep.equal({ + user: { + ext: { + data: data + }, + } + }); + expect(onDataResponse).to.deep.equal({ + data: data, + site: false, + }); + }); - let request = server.requests[0]; - request.respond(500, responseHeader); + it('should set gam targeting but not send to bidders with setPrebidTargeting=true/sendToBidders=false', function () { + const moduleConfig = { + params: { + weboUserDataConf: { + setPrebidTargeting: true, + sendToBidders: false + } + } + }; + const data = { + webo_cs: ['foo', 'bar'], + webo_audiences: ['baz'], + }; + + const entry = { + targeting: data, + }; + + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'getDataFromLocalStorage') + .withArgs(DEFAULT_LOCAL_STORAGE_USER_PROFILE_KEY) + .returns(JSON.stringify(entry)); + + const adUnitsCodes = ['adunit1', 'adunit2']; + const reqBidsConfigObj = { + adUnits: [{ + bids: [{ + bidder: 'smartadserver', + params: { + target: 'foo=bar' + } + }, { + bidder: 'pubmatic', + params: { + dctr: 'foo=bar' + } + }, { + bidder: 'appnexus', + params: { + keywords: { + foo: ['bar'] + } + } + }, { + bidder: 'rubicon', + params: { + inventory: { + foo: 'bar' + }, + visitor: { + baz: 'bam' + } + } + }, { + bidder: 'other' + }] + }] + }; + const onDoneSpy = sinon.spy(); + + expect(weboramaSubmodule.init(moduleConfig)).to.be.true; + weboramaSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, moduleConfig); + + expect(onDoneSpy.calledOnce).to.be.true; + + const targeting = weboramaSubmodule.getTargetingData(adUnitsCodes, moduleConfig); + + expect(targeting).to.deep.equal({ + 'adunit1': data, + 'adunit2': data, + }); + + expect(reqBidsConfigObj.adUnits[0].bids.length).to.equal(5); + expect(reqBidsConfigObj.adUnits[0].bids[0].params.target).to.equal('foo=bar'); + expect(reqBidsConfigObj.adUnits[0].bids[1].params.dctr).to.equal('foo=bar'); + expect(reqBidsConfigObj.adUnits[0].bids[2].params.keywords).to.deep.equal({ + foo: ['bar'] + }); + expect(reqBidsConfigObj.adUnits[0].bids[3].params).to.deep.equal({ + inventory: { + foo: 'bar' + }, + visitor: { + baz: 'bam' + } + }); + expect(reqBidsConfigObj.adUnits[0].bids[4].ortb2).to.be.undefined; + expect(getGlobal().getConfig('ortb2')).to.be.undefined; + }); - const targeting = weboramaSubmodule.getTargetingData(adUnitsCodes, moduleConfig); + it('should set gam targeting but not send to bidders with (submodule override) setPrebidTargeting=true/(global) sendToBidders=false', function () { + let onDataResponse = {}; + const moduleConfig = { + params: { + setPrebidTargeting: false, + sendToBidders: false, + onData: (data, site) => { + onDataResponse = { + data: data, + site: site, + }; + }, + weboUserDataConf: { + setPrebidTargeting: true, // submodule parameter will override module parameter + } + } + }; + const data = { + webo_cs: ['foo', 'bar'], + webo_audiences: ['baz'], + }; + + const entry = { + targeting: data, + }; + + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'getDataFromLocalStorage') + .withArgs(DEFAULT_LOCAL_STORAGE_USER_PROFILE_KEY) + .returns(JSON.stringify(entry)); + + const adUnitsCodes = ['adunit1', 'adunit2']; + const reqBidsConfigObj = { + adUnits: [{ + bids: [{ + bidder: 'smartadserver', + params: { + target: 'foo=bar' + } + }] + }] + }; + const onDoneSpy = sinon.spy(); + + expect(weboramaSubmodule.init(moduleConfig)).to.be.true; + weboramaSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, moduleConfig); + + expect(onDoneSpy.calledOnce).to.be.true; + + const targeting = weboramaSubmodule.getTargetingData(adUnitsCodes, moduleConfig); + + expect(targeting).to.deep.equal({ + 'adunit1': data, + 'adunit2': data, + }); + + expect(reqBidsConfigObj.adUnits[0].bids.length).to.equal(1); + expect(reqBidsConfigObj.adUnits[0].bids[0].params.target).to.equal('foo=bar'); + expect(getGlobal().getConfig('ortb2')).to.be.undefined; + expect(onDataResponse).to.deep.equal({ + data: data, + site: false, + }); + }); - expect(targeting).to.deep.equal({ - 'adunit1': defaultProfile, - 'adunit2': defaultProfile, + it('should not set gam targeting with setPrebidTargeting=false but send to bidders', function () { + const moduleConfig = { + params: { + weboUserDataConf: { + setPrebidTargeting: false, + } + } + }; + const data = { + webo_cs: ['foo', 'bar'], + webo_audiences: ['baz'], + }; + + const entry = { + targeting: data, + }; + + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'getDataFromLocalStorage') + .withArgs(DEFAULT_LOCAL_STORAGE_USER_PROFILE_KEY) + .returns(JSON.stringify(entry)); + + const adUnitsCodes = ['adunit1', 'adunit2']; + const reqBidsConfigObj = { + adUnits: [{ + bids: [{ + bidder: 'smartadserver', + params: { + target: 'foo=bar' + } + }, { + bidder: 'pubmatic', + params: { + dctr: 'foo=bar' + } + }, { + bidder: 'appnexus', + params: { + keywords: { + foo: ['bar'] + } + } + }, { + bidder: 'rubicon', + params: { + inventory: { + foo: 'bar', + }, + visitor: { + baz: 'bam', + } + } + }, { + bidder: 'other' + }] + }] + }; + const onDoneSpy = sinon.spy(); + + expect(weboramaSubmodule.init(moduleConfig)).to.be.true; + weboramaSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, moduleConfig); + + expect(onDoneSpy.calledOnce).to.be.true; + + const targeting = weboramaSubmodule.getTargetingData(adUnitsCodes, moduleConfig); + + expect(targeting).to.deep.equal({}); + + expect(reqBidsConfigObj.adUnits[0].bids.length).to.equal(5); + expect(reqBidsConfigObj.adUnits[0].bids[0].params.target).to.equal('foo=bar;webo_cs=foo;webo_cs=bar;webo_audiences=baz'); + expect(reqBidsConfigObj.adUnits[0].bids[1].params.dctr).to.equal('foo=bar|webo_cs=foo,bar|webo_audiences=baz'); + expect(reqBidsConfigObj.adUnits[0].bids[2].params.keywords).to.deep.equal({ + foo: ['bar'], + webo_cs: ['foo', 'bar'], + webo_audiences: ['baz'], + }); + expect(reqBidsConfigObj.adUnits[0].bids[3].params).to.deep.equal({ + inventory: { + foo: 'bar', + }, + visitor: { + baz: 'bam', + webo_cs: ['foo', 'bar'], + webo_audiences: ['baz'], + } + }); + expect(reqBidsConfigObj.adUnits[0].bids[4].ortb2).to.deep.equal({ + user: { + ext: { + data: data + }, + } + }); + expect(getGlobal().getConfig('ortb2')).to.deep.equal({ + user: { + ext: { + data: data + }, + } + }); }); - const ortb2 = config.getConfig('ortb2'); + it('should use default profile in case of nothing on local storage', function () { + const defaultProfile = { + webo_audiences: ['baz'] + }; + const moduleConfig = { + params: { + weboUserDataConf: { + setPrebidTargeting: true, + defaultProfile: defaultProfile, + } + } + }; + + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + + const adUnitsCodes = ['adunit1', 'adunit2']; + const reqBidsConfigObj = { + adUnits: [{ + bids: [{ + bidder: 'smartadserver' + }, { + bidder: 'pubmatic' + }, { + bidder: 'appnexus' + }, { + bidder: 'rubicon' + }, { + bidder: 'other' + }] + }] + }; + const onDoneSpy = sinon.spy(); + + expect(weboramaSubmodule.init(moduleConfig)).to.be.true; + weboramaSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, moduleConfig); + + expect(onDoneSpy.calledOnce).to.be.true; + + const targeting = weboramaSubmodule.getTargetingData(adUnitsCodes, moduleConfig); + + expect(targeting).to.deep.equal({ + 'adunit1': defaultProfile, + 'adunit2': defaultProfile, + }); + + expect(reqBidsConfigObj.adUnits[0].bids.length).to.equal(5); + expect(reqBidsConfigObj.adUnits[0].bids[0].params.target).to.equal('webo_audiences=baz'); + expect(reqBidsConfigObj.adUnits[0].bids[1].params.dctr).to.equal('webo_audiences=baz'); + expect(reqBidsConfigObj.adUnits[0].bids[2].params.keywords).to.deep.equal(defaultProfile); + expect(reqBidsConfigObj.adUnits[0].bids[3].params).to.deep.equal({ + visitor: defaultProfile + }); + expect(reqBidsConfigObj.adUnits[0].bids[4].ortb2).to.deep.equal({ + user: { + ext: { + data: defaultProfile + }, + } + }); + expect(getGlobal().getConfig('ortb2')).to.deep.equal({ + user: { + ext: { + data: defaultProfile + }, + } + }); + }); - expect(ortb2.site.ext.data).to.not.have.property('webo_ctx'); - expect(ortb2.site.ext.data).to.not.have.property('webo_ds'); + it('should use default profile if cant read from local storage', function () { + const defaultProfile = { + webo_audiences: ['baz'] + }; + let onDataResponse = {}; + const moduleConfig = { + params: { + weboUserDataConf: { + setPrebidTargeting: true, + defaultProfile: defaultProfile, + onData: (data, site) => { + onDataResponse = { + data: data, + site: site, + }; + }, + } + } + }; + + sandbox.stub(storage, 'localStorageIsEnabled').returns(false); + + const adUnitsCodes = ['adunit1', 'adunit2']; + const reqBidsConfigObj = { + adUnits: [{ + bids: [{ + bidder: 'smartadserver' + }, { + bidder: 'pubmatic' + }, { + bidder: 'appnexus' + }, { + bidder: 'rubicon' + }, { + bidder: 'other' + }] + }] + }; + const onDoneSpy = sinon.spy(); + + expect(weboramaSubmodule.init(moduleConfig)).to.be.true; + weboramaSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, moduleConfig); + + expect(onDoneSpy.calledOnce).to.be.true; + + const targeting = weboramaSubmodule.getTargetingData(adUnitsCodes, moduleConfig); + + expect(targeting).to.deep.equal({ + 'adunit1': defaultProfile, + 'adunit2': defaultProfile, + }); + + expect(reqBidsConfigObj.adUnits[0].bids.length).to.equal(5); + expect(reqBidsConfigObj.adUnits[0].bids[0].params.target).to.equal('webo_audiences=baz'); + expect(reqBidsConfigObj.adUnits[0].bids[1].params.dctr).to.equal('webo_audiences=baz'); + expect(reqBidsConfigObj.adUnits[0].bids[2].params.keywords).to.deep.equal(defaultProfile); + expect(reqBidsConfigObj.adUnits[0].bids[3].params).to.deep.equal({ + visitor: defaultProfile + }); + expect(reqBidsConfigObj.adUnits[0].bids[4].ortb2).to.deep.equal({ + user: { + ext: { + data: defaultProfile + }, + } + }); + expect(getGlobal().getConfig('ortb2')).to.deep.equal({ + user: { + ext: { + data: defaultProfile + }, + } + }); + expect(onDataResponse).to.deep.equal({ + data: defaultProfile, + site: false, + }); + }); }); }); }); diff --git a/test/spec/modules/welectBidAdapter_spec.js b/test/spec/modules/welectBidAdapter_spec.js new file mode 100644 index 00000000000..2f2af35eaec --- /dev/null +++ b/test/spec/modules/welectBidAdapter_spec.js @@ -0,0 +1,211 @@ +import { expect } from 'chai'; +import { spec as adapter } from 'modules/welectBidAdapter.js'; + +describe('WelectAdapter', function () { + describe('Check methods existance', function () { + it('exists and is a function', function () { + expect(adapter.isBidRequestValid).to.exist.and.to.be.a('function'); + }); + it('exists and is a function', function () { + expect(adapter.buildRequests).to.exist.and.to.be.a('function'); + }); + it('exists and is a function', function () { + expect(adapter.interpretResponse).to.exist.and.to.be.a('function'); + }); + }); + + describe('Check method isBidRequestValid return', function () { + let bid = { + bidder: 'welect', + params: { + placementId: 'exampleAlias', + domain: 'www.welect.de' + }, + sizes: [[640, 360]], + mediaTypes: { + video: { + context: 'instream' + } + }, + }; + let bid2 = { + bidder: 'welect', + params: { + domain: 'www.welect.de' + }, + mediaTypes: { + video: { + context: 'instream', + playerSize: [640, 360] + } + }, + }; + + it('should be true', function () { + expect(adapter.isBidRequestValid(bid)).to.be.true; + }); + + it('should be false because the placementId is missing', function () { + expect(adapter.isBidRequestValid(bid2)).to.be.false; + }); + }); + + describe('Check buildRequests method', function () { + // Bids to be formatted + let bid1 = { + bidder: 'welect', + params: { + placementId: 'exampleAlias' + }, + sizes: [[640, 360]], + mediaTypes: { + video: { + context: 'instream' + } + }, + bidId: 'abdc' + }; + let bid2 = { + bidder: 'welect', + params: { + placementId: 'exampleAlias', + domain: 'www.welect2.de' + }, + sizes: [[640, 360]], + mediaTypes: { + video: { + context: 'instream' + } + }, + bidId: 'abdc', + gdprConsent: { + gdprApplies: 1, + gdprConsent: 'some_string' + } + }; + + let data1 = { + bid_id: 'abdc', + width: 640, + height: 360 + } + + let data2 = { + bid_id: 'abdc', + width: 640, + height: 360, + gdpr_consent: { + gdprApplies: 1, + tcString: 'some_string' + } + } + + // Formatted requets + let request1 = { + method: 'POST', + url: 'https://www.welect.de/api/v2/preflight/exampleAlias', + data: data1, + options: { + contentType: 'application/json', + withCredentials: false, + crossOrigin: true, + } + }; + + let request2 = { + method: 'POST', + url: 'https://www.welect2.de/api/v2/preflight/exampleAlias', + data: data2, + options: { + contentType: 'application/json', + withCredentials: false, + crossOrigin: true, + } + } + + it('defaults to www.welect.de, without gdpr object', function () { + expect(adapter.buildRequests([bid1])).to.deep.equal([request1]); + }) + + it('must return the right formatted requests, with gdpr object', function () { + expect(adapter.buildRequests([bid2])).to.deep.equal([request2]); + }); + }); + + describe('Check interpretResponse method return', function () { + // invalid server response + let unavailableResponse = { + body: { + available: false + } + }; + + let availableResponse = { + body: { + available: true, + bidResponse: { + ad: { + video: 'some vast url' + }, + meta: { + advertiserDomains: [], + }, + cpm: 17, + creativeId: 'svmpreview', + currency: 'EUR', + netRevenue: true, + requestId: 'some bid id', + ttl: 120, + vastUrl: 'some vast url', + height: 640, + width: 320 + } + } + } + // bid Request + let bid = { + data: { + bid_id: 'some bid id', + width: 640, + height: 320, + gdpr_consent: { + gdprApplies: 1, + tcString: 'some_string' + } + }, + method: 'POST', + url: 'https://www.welect.de/api/v2/preflight/exampleAlias', + options: { + contentType: 'application/json', + withCredentials: false, + crossOrigin: true, + } + }; + // Formatted reponse + let result = { + ad: { + video: 'some vast url' + }, + meta: { + advertiserDomains: [] + }, + cpm: 17, + creativeId: 'svmpreview', + currency: 'EUR', + height: 640, + netRevenue: true, + requestId: 'some bid id', + ttl: 120, + vastUrl: 'some vast url', + width: 320 + } + + it('if response reflects unavailability, should be empty', function () { + expect(adapter.interpretResponse(unavailableResponse, bid)).to.deep.equal([]); + }); + + it('if response reflects availability, should equal result', function () { + expect(adapter.interpretResponse(availableResponse, bid)).to.deep.equal([result]) + }) + }); +}); diff --git a/test/spec/modules/widespaceBidAdapter_spec.js b/test/spec/modules/widespaceBidAdapter_spec.js index af8d505b4f4..0a0af1f1229 100644 --- a/test/spec/modules/widespaceBidAdapter_spec.js +++ b/test/spec/modules/widespaceBidAdapter_spec.js @@ -1,6 +1,6 @@ import {expect} from 'chai'; import {spec, storage} from 'modules/widespaceBidAdapter.js'; -import includes from 'core-js-pure/features/array/includes.js'; +import {includes} from 'src/polyfill.js'; describe('+widespaceAdatperTest', function () { // Dummy bid request diff --git a/test/spec/modules/yahoosspBidAdapter_spec.js b/test/spec/modules/yahoosspBidAdapter_spec.js index 5eb82b399cc..e301218741c 100644 --- a/test/spec/modules/yahoosspBidAdapter_spec.js +++ b/test/spec/modules/yahoosspBidAdapter_spec.js @@ -11,7 +11,7 @@ const DEFAULT_AD_UNIT_CODE = '/19968336/header-bid-tag-1'; const DEFAULT_AD_UNIT_TYPE = 'banner'; const DEFAULT_PARAMS_BID_OVERRIDE = {}; const DEFAULT_VIDEO_CONTEXT = 'instream'; -const ADAPTER_VERSION = '1.0.1'; +const ADAPTER_VERSION = '1.0.2'; const PREBID_VERSION = '$prebid.version$'; const INTEGRATION_METHOD = 'prebid.js'; @@ -177,6 +177,16 @@ describe('YahooSSP Bid Adapter:', () => { expect(obj).to.be.an('object'); }); + describe('Validate basic properties', () => { + it('should define the correct bidder code', () => { + expect(spec.code).to.equal('yahoossp') + }); + + it('should define the correct vendor ID', () => { + expect(spec.gvlid).to.equal(25) + }); + }); + describe('getUserSyncs()', () => { const IMAGE_PIXEL_URL = 'http://image-pixel.com/foo/bar?1234&baz=true'; const IFRAME_ONE_URL = 'http://image-iframe.com/foo/bar?1234&baz=true'; @@ -656,6 +666,33 @@ describe('YahooSSP Bid Adapter:', () => { const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; expect(data.imp[0].ext.data).to.deep.equal(validBidRequests[0].ortb2Imp.ext.data); }); + // adUnit.ortb2Imp.instl + it(`should allow adUnit.ortb2Imp.instl numeric boolean "1" to be added to the bid-request`, () => { + let { validBidRequests, bidderRequest } = generateBuildRequestMock({}) + validBidRequests[0].ortb2Imp = { + instl: 1 + }; + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + expect(data.imp[0].instl).to.deep.equal(validBidRequests[0].ortb2Imp.instl); + }); + + it(`should prevent adUnit.ortb2Imp.instl boolean "true" to be added to the bid-request`, () => { + let { validBidRequests, bidderRequest } = generateBuildRequestMock({}) + validBidRequests[0].ortb2Imp = { + instl: true + }; + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + expect(data.imp[0].instl).to.not.exist; + }); + + it(`should prevent adUnit.ortb2Imp.instl boolean "false" to be added to the bid-request`, () => { + let { validBidRequests, bidderRequest } = generateBuildRequestMock({}) + validBidRequests[0].ortb2Imp = { + instl: false + }; + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + expect(data.imp[0].instl).to.not.exist; + }); }); describe('e2etest mode support:', () => { @@ -1328,5 +1365,13 @@ describe('YahooSSP Bid Adapter:', () => { expect(response[0].ttl).to.equal(500); }); }); + + describe('Aliasing support', () => { + it('should return undefined as the bidder code value', () => { + const { serverResponse, bidderRequest } = generateResponseMock('banner'); + const response = spec.interpretResponse(serverResponse, {bidderRequest}); + expect(response[0].bidderCode).to.be.undefined; + }); + }); }); }); diff --git a/test/spec/modules/yieldmoBidAdapter_spec.js b/test/spec/modules/yieldmoBidAdapter_spec.js index 3eee9e44453..f72705a79ac 100644 --- a/test/spec/modules/yieldmoBidAdapter_spec.js +++ b/test/spec/modules/yieldmoBidAdapter_spec.js @@ -511,6 +511,7 @@ describe('YieldmoAdapter', function () { body: [{ callback_id: '21989fdbef550a', cpm: 3.45455, + publisherDealId: 'YMO_123', width: 300, height: 250, ad: '' + @@ -525,6 +526,7 @@ describe('YieldmoAdapter', function () { const newResponse = spec.interpretResponse(mockServerResponse()); expect(newResponse.length).to.be.equal(1); expect(newResponse[0]).to.deep.equal({ + dealId: 'YMO_123', requestId: '21989fdbef550a', cpm: 3.45455, width: 300, @@ -552,6 +554,7 @@ describe('YieldmoAdapter', function () { crid: 'dd65c0a7536aff', impid: '91ea8bba1', price: 1.5, + dealid: 'YMO_456' }, }, ]; @@ -574,6 +577,7 @@ describe('YieldmoAdapter', function () { const newResponse = spec.interpretResponse(response, bidRequest); expect(newResponse.length).to.be.equal(2); expect(newResponse[1]).to.deep.equal({ + dealId: 'YMO_456', cpm: 1.5, creativeId: 'dd65c0a7536aff', currency: 'USD', diff --git a/test/spec/modules/yieldoneBidAdapter_spec.js b/test/spec/modules/yieldoneBidAdapter_spec.js index 1b38bb94a8c..d452d78e147 100644 --- a/test/spec/modules/yieldoneBidAdapter_spec.js +++ b/test/spec/modules/yieldoneBidAdapter_spec.js @@ -366,6 +366,39 @@ describe('yieldoneBidAdapter', function() { expect(request[0].data.lr_env).to.equal('idl_env_sample'); }); }); + + describe('IMID', function () { + it('dont send IMID if undefined', function () { + const bidRequests = [ + { + params: {placementId: '0'}, + }, + { + params: {placementId: '1'}, + userId: {}, + }, + { + params: {placementId: '2'}, + userId: undefined, + }, + ]; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request[0].data).to.not.have.property('imuid'); + expect(request[1].data).to.not.have.property('imuid'); + expect(request[2].data).to.not.have.property('imuid'); + }); + + it('should send IMID if available', function () { + const bidRequests = [ + { + params: {placementId: '0'}, + userId: {imuid: 'imuid_sample'}, + }, + ]; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request[0].data.imuid).to.equal('imuid_sample'); + }); + }); }); describe('interpretResponse', function () { diff --git a/test/spec/modules/zeotapIdPlusIdSystem_spec.js b/test/spec/modules/zeotapIdPlusIdSystem_spec.js index 9de6fa843bc..98459a36d45 100644 --- a/test/spec/modules/zeotapIdPlusIdSystem_spec.js +++ b/test/spec/modules/zeotapIdPlusIdSystem_spec.js @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import find from 'core-js-pure/features/array/find.js'; +import {find} from 'src/polyfill.js'; import { config } from 'src/config.js'; import { init, requestBidsHook, setSubmoduleRegistry } from 'modules/userId/index.js'; import { storage, getStorage, zeotapIdPlusSubmodule } from 'modules/zeotapIdPlusIdSystem.js'; @@ -54,7 +54,7 @@ describe('Zeotap ID System', function() { it('when a stored Zeotap ID exists it is added to bids', function() { let store = getStorage(); expect(getStorageManagerSpy.calledOnce).to.be.true; - sinon.assert.calledWith(getStorageManagerSpy, 301, 'zeotapIdPlus'); + sinon.assert.calledWith(getStorageManagerSpy, {gvlid: 301, moduleName: 'zeotapIdPlus'}); }); }); diff --git a/test/spec/modules/zeta_global_sspAnalyticsAdapter_spec.js b/test/spec/modules/zeta_global_sspAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..15a1155f378 --- /dev/null +++ b/test/spec/modules/zeta_global_sspAnalyticsAdapter_spec.js @@ -0,0 +1,427 @@ +import zetaAnalyticsAdapter from 'modules/zeta_global_sspAnalyticsAdapter.js'; +import {config} from 'src/config'; +import CONSTANTS from 'src/constants.json'; +import {logError} from '../../../src/utils'; + +let utils = require('src/utils'); +let events = require('src/events'); + +const MOCK = { + STUB: { + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa' + }, + AUCTION_END: { + 'auctionId': '75e394d9-ccce-4978-9238-91e6a1ac88a1', + 'timestamp': 1638441234544, + 'auctionEnd': 1638441234784, + 'auctionStatus': 'completed', + 'adUnits': [ + { + 'code': '/19968336/header-bid-tag-0', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ] + } + }, + 'bids': [ + { + 'bidder': 'zeta_global_ssp', + 'params': { + 'sid': 111, + 'tags': { + 'shortname': 'prebid_analytics_event_test_shortname', + 'position': 'test_position' + } + } + }, + { + 'bidder': 'appnexus', + 'params': { + 'placementId': 13232385 + } + } + ], + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ], + 'transactionId': '6b29369c-0c2e-414e-be1f-5867aec18d83' + } + ], + 'adUnitCodes': [ + '/19968336/header-bid-tag-0' + ], + 'bidderRequests': [ + { + 'bidderCode': 'zeta_global_ssp', + 'auctionId': '75e394d9-ccce-4978-9238-91e6a1ac88a1', + 'bidderRequestId': '1207cb49191887', + 'bids': [ + { + 'bidder': 'zeta_global_ssp', + 'params': { + 'sid': 111, + 'tags': { + 'shortname': 'prebid_analytics_event_test_shortname', + 'position': 'test_position' + } + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ] + } + }, + 'adUnitCode': '/19968336/header-bid-tag-0', + 'transactionId': '6b29369c-0c2e-414e-be1f-5867aec18d83', + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ], + 'bidId': '206be9a13236af', + 'bidderRequestId': '1207cb49191887', + 'auctionId': '75e394d9-ccce-4978-9238-91e6a1ac88a1', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0 + } + ], + 'auctionStart': 1638441234544, + 'timeout': 400, + 'refererInfo': { + 'referer': 'http://test-zeta-ssp.net:63342/zeta-ssp/ssp/_dev/examples/page_banner.html', + 'reachedTop': true, + 'isAmp': false, + 'numIframes': 0, + 'stack': [ + 'http://test-zeta-ssp.net:63342/zeta-ssp/ssp/_dev/examples/page_banner.html' + ], + 'canonicalUrl': null + }, + 'start': 1638441234547 + }, + { + 'bidderCode': 'appnexus', + 'auctionId': '75e394d9-ccce-4978-9238-91e6a1ac88a1', + 'bidderRequestId': '32b97f0a935422', + 'bids': [ + { + 'bidder': 'appnexus', + 'params': { + 'placementId': 13232385 + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ] + } + }, + 'adUnitCode': '/19968336/header-bid-tag-0', + 'transactionId': '6b29369c-0c2e-414e-be1f-5867aec18d83', + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ], + 'bidId': '41badc0e164c758', + 'bidderRequestId': '32b97f0a935422', + 'auctionId': '75e394d9-ccce-4978-9238-91e6a1ac88a1', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0 + } + ], + 'auctionStart': 1638441234544, + 'timeout': 400, + 'refererInfo': { + 'referer': 'http://test-zeta-ssp.net:63342/zeta-ssp/ssp/_dev/examples/page_banner.html', + 'reachedTop': true, + 'isAmp': false, + 'numIframes': 0, + 'stack': [ + 'http://test-zeta-ssp.net:63342/zeta-ssp/ssp/_dev/examples/page_banner.html' + ], + 'canonicalUrl': null + }, + 'start': 1638441234550 + } + ], + 'noBids': [ + { + 'bidder': 'appnexus', + 'params': { + 'placementId': 13232385 + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ] + } + }, + 'adUnitCode': '/19968336/header-bid-tag-0', + 'transactionId': '6b29369c-0c2e-414e-be1f-5867aec18d83', + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ], + 'bidId': '41badc0e164c758', + 'bidderRequestId': '32b97f0a935422', + 'auctionId': '75e394d9-ccce-4978-9238-91e6a1ac88a1', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0 + } + ], + 'bidsReceived': [ + { + 'bidderCode': 'zeta_global_ssp', + 'width': 480, + 'height': 320, + 'statusMessage': 'Bid available', + 'adId': '5759bb3ef7be1e8', + 'requestId': '206be9a13236af', + 'mediaType': 'banner', + 'source': 'client', + 'cpm': 2.258302852806723, + 'currency': 'USD', + 'ad': 'test_ad', + 'ttl': 200, + 'creativeId': '456456456', + 'netRevenue': true, + 'meta': { + 'advertiserDomains': [ + 'viaplay.fi' + ] + }, + 'originalCpm': 2.258302852806723, + 'originalCurrency': 'USD', + 'auctionId': '75e394d9-ccce-4978-9238-91e6a1ac88a1', + 'responseTimestamp': 1638441234670, + 'requestTimestamp': 1638441234547, + 'bidder': 'zeta_global_ssp', + 'adUnitCode': '/19968336/header-bid-tag-0', + 'timeToRespond': 123, + 'pbLg': '2.00', + 'pbMg': '2.20', + 'pbHg': '2.25', + 'pbAg': '2.25', + 'pbDg': '2.25', + 'pbCg': '', + 'size': '480x320', + 'adserverTargeting': { + 'hb_bidder': 'zeta_global_ssp', + 'hb_adid': '5759bb3ef7be1e8', + 'hb_pb': '2.20', + 'hb_size': '480x320', + 'hb_source': 'client', + 'hb_format': 'banner', + 'hb_adomain': 'viaplay.fi' + } + } + ], + 'winningBids': [], + 'timeout': 400 + }, + AD_RENDER_SUCCEEDED: { + 'doc': { + 'location': { + 'href': 'http://test-zeta-ssp.net:63342/zeta-ssp/ssp/_dev/examples/page_banner.html', + 'protocol': 'http:', + 'host': 'localhost:63342', + 'hostname': 'localhost', + 'port': '63342', + 'pathname': '/zeta-ssp/ssp/_dev/examples/page_banner.html', + 'hash': '', + 'origin': 'http://test-zeta-ssp.net:63342', + 'ancestorOrigins': { + '0': 'http://test-zeta-ssp.net:63342' + } + } + }, + 'bid': { + 'bidderCode': 'zeta_global_ssp', + 'width': 480, + 'height': 320, + 'statusMessage': 'Bid available', + 'adId': '5759bb3ef7be1e8', + 'requestId': '206be9a13236af', + 'mediaType': 'banner', + 'source': 'client', + 'cpm': 2.258302852806723, + 'currency': 'USD', + 'ad': 'test_ad', + 'ttl': 200, + 'creativeId': '456456456', + 'netRevenue': true, + 'meta': { + 'advertiserDomains': [ + 'viaplay.fi' + ] + }, + 'originalCpm': 2.258302852806723, + 'originalCurrency': 'USD', + 'auctionId': '75e394d9-ccce-4978-9238-91e6a1ac88a1', + 'responseTimestamp': 1638441234670, + 'requestTimestamp': 1638441234547, + 'bidder': 'zeta_global_ssp', + 'adUnitCode': '/19968336/header-bid-tag-0', + 'timeToRespond': 123, + 'pbLg': '2.00', + 'pbMg': '2.20', + 'pbHg': '2.25', + 'pbAg': '2.25', + 'pbDg': '2.25', + 'pbCg': '', + 'size': '480x320', + 'adserverTargeting': { + 'hb_bidder': 'zeta_global_ssp', + 'hb_adid': '5759bb3ef7be1e8', + 'hb_pb': '2.20', + 'hb_size': '480x320', + 'hb_source': 'client', + 'hb_format': 'banner', + 'hb_adomain': 'viaplay.fi' + }, + 'status': 'rendered', + 'params': [ + { + 'sid': 111, + 'tags': { + 'shortname': 'prebid_analytics_event_test_shortname', + 'position': 'test_position' + } + } + ] + }, + 'adId': '5759bb3ef7be1e8' + } +} + +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); + sandbox.stub(events, 'getEvents').returns([]); + }); + + afterEach(function () { + sandbox.restore(); + config.resetConfig(); + }); + + it('should require publisherId', function () { + sandbox.stub(utils, 'logError'); + zetaAnalyticsAdapter.enableAnalytics({ + options: {} + }); + expect(utils.logError.called).to.equal(true); + }); + + describe('handle events', function() { + beforeEach(function() { + zetaAnalyticsAdapter.enableAnalytics({ + options: { + sid: 111 + } + }); + }); + + afterEach(function () { + zetaAnalyticsAdapter.disableAnalytics(); + }); + + it('events are sent', function() { + this.timeout(5000); + events.emit(CONSTANTS.EVENTS.AUCTION_INIT, MOCK.STUB); + events.emit(CONSTANTS.EVENTS.AUCTION_END, MOCK.AUCTION_END); + events.emit(CONSTANTS.EVENTS.BID_ADJUSTMENT, MOCK.STUB); + events.emit(CONSTANTS.EVENTS.BID_TIMEOUT, MOCK.STUB); + events.emit(CONSTANTS.EVENTS.BID_REQUESTED, MOCK.STUB); + events.emit(CONSTANTS.EVENTS.BID_RESPONSE, MOCK.STUB); + events.emit(CONSTANTS.EVENTS.NO_BID, MOCK.STUB); + events.emit(CONSTANTS.EVENTS.BID_WON, MOCK.STUB); + events.emit(CONSTANTS.EVENTS.BIDDER_DONE, MOCK.STUB); + events.emit(CONSTANTS.EVENTS.BIDDER_ERROR, MOCK.STUB); + events.emit(CONSTANTS.EVENTS.SET_TARGETING, MOCK.STUB); + events.emit(CONSTANTS.EVENTS.BEFORE_REQUEST_BIDS, MOCK.STUB); + events.emit(CONSTANTS.EVENTS.BEFORE_BIDDER_HTTP, MOCK.STUB); + events.emit(CONSTANTS.EVENTS.REQUEST_BIDS, MOCK.STUB); + events.emit(CONSTANTS.EVENTS.ADD_AD_UNITS, MOCK.STUB); + events.emit(CONSTANTS.EVENTS.AD_RENDER_FAILED, MOCK.STUB); + events.emit(CONSTANTS.EVENTS.AD_RENDER_SUCCEEDED, MOCK.AD_RENDER_SUCCEEDED); + events.emit(CONSTANTS.EVENTS.TCF2_ENFORCEMENT, MOCK.STUB); + events.emit(CONSTANTS.EVENTS.AUCTION_DEBUG, MOCK.STUB); + events.emit(CONSTANTS.EVENTS.BID_VIEWABLE, MOCK.STUB); + events.emit(CONSTANTS.EVENTS.STALE_RENDER, MOCK.STUB); + + expect(requests.length).to.equal(2); + expect(JSON.parse(requests[0].requestBody)).to.deep.equal(MOCK.AUCTION_END); + expect(JSON.parse(requests[1].requestBody)).to.deep.equal(MOCK.AD_RENDER_SUCCEEDED); + }); + }); +}); diff --git a/test/spec/modules/zeta_global_sspBidAdapter_spec.js b/test/spec/modules/zeta_global_sspBidAdapter_spec.js index dcb0183fb4c..20113a63994 100644 --- a/test/spec/modules/zeta_global_sspBidAdapter_spec.js +++ b/test/spec/modules/zeta_global_sspBidAdapter_spec.js @@ -1,4 +1,5 @@ import {spec} from '../../../modules/zeta_global_sspBidAdapter.js' +import {BANNER, VIDEO} from '../../../src/mediaTypes'; describe('Zeta Ssp Bid Adapter', function () { const eids = [ @@ -99,6 +100,13 @@ describe('Zeta Ssp Bid Adapter', function () { expect(payload.user.ext.eids).to.eql(eids); }); + it('Test contains ua and language', function () { + const request = spec.buildRequests(bannerRequest, bannerRequest[0]); + const payload = JSON.parse(request.data); + expect(payload.device.ua).to.not.be.empty; + expect(payload.device.language).to.not.be.empty; + }); + it('Test page and domain in site', function () { const request = spec.buildRequests(bannerRequest, bannerRequest[0]); const payload = JSON.parse(request.data); @@ -143,7 +151,22 @@ describe('Zeta Ssp Bid Adapter', function () { 'https://example2.com' ], h: 150, - w: 200 + w: 200, + ext: { + bidtype: 'video' + } + }, + { + id: 'auctionId3', + impid: 'impId3', + price: 0.2, + adm: '', + crid: 'creativeId3', + adomain: [ + 'https://example3.com' + ], + h: 400, + w: 300 } ] } @@ -159,6 +182,8 @@ describe('Zeta Ssp Bid Adapter', function () { const receivedBid1 = response.body.seatbid[0].bid[0]; expect(bid1).to.not.be.empty; expect(bid1.ad).to.equal(receivedBid1.adm); + expect(bid1.vastXml).to.be.undefined; + expect(bid1.mediaType).to.equal(BANNER); expect(bid1.cpm).to.equal(receivedBid1.price); expect(bid1.height).to.equal(receivedBid1.h); expect(bid1.width).to.equal(receivedBid1.w); @@ -169,11 +194,25 @@ describe('Zeta Ssp Bid Adapter', function () { const receivedBid2 = response.body.seatbid[0].bid[1]; expect(bid2).to.not.be.empty; expect(bid2.ad).to.equal(receivedBid2.adm); + expect(bid2.vastXml).to.equal(receivedBid2.adm); + expect(bid2.mediaType).to.equal(VIDEO); expect(bid2.cpm).to.equal(receivedBid2.price); expect(bid2.height).to.equal(receivedBid2.h); expect(bid2.width).to.equal(receivedBid2.w); expect(bid2.requestId).to.equal(receivedBid2.impid); expect(bid2.meta.advertiserDomains).to.equal(receivedBid2.adomain); + + const bid3 = bidResponse[2]; + const receivedBid3 = response.body.seatbid[0].bid[2]; + expect(bid3).to.not.be.empty; + expect(bid3.ad).to.equal(receivedBid3.adm); + expect(bid3.vastXml).to.equal(receivedBid3.adm); + expect(bid3.mediaType).to.equal(VIDEO); + expect(bid3.cpm).to.equal(receivedBid3.price); + expect(bid3.height).to.equal(receivedBid3.h); + expect(bid3.width).to.equal(receivedBid3.w); + expect(bid3.requestId).to.equal(receivedBid3.impid); + expect(bid3.meta.advertiserDomains).to.equal(receivedBid3.adomain); }); it('Different cases for user syncs', function () { diff --git a/test/spec/native_spec.js b/test/spec/native_spec.js index 0ffef30965b..66e11b9a472 100644 --- a/test/spec/native_spec.js +++ b/test/spec/native_spec.js @@ -1,10 +1,19 @@ import { expect } from 'chai'; -import { fireNativeTrackers, getNativeTargeting, nativeBidIsValid, getAssetMessage, getAllAssetsMessage } from 'src/native.js'; +import { + fireNativeTrackers, + getNativeTargeting, + nativeBidIsValid, + getAssetMessage, + getAllAssetsMessage, + decorateAdUnitsWithNativeParams +} from 'src/native.js'; import CONSTANTS from 'src/constants.json'; +import {stubAuctionIndex} from '../helpers/indexStub.js'; const utils = require('src/utils'); const bid = { adId: '123', + transactionId: 'au', native: { title: 'Native Creative', body: 'Cool description great stuff', @@ -32,6 +41,7 @@ const bid = { }; const bidWithUndefinedFields = { + transactionId: 'au', native: { title: 'Native Creative', body: undefined, @@ -52,6 +62,10 @@ describe('native.js', function () { let triggerPixelStub; let insertHtmlIntoIframeStub; + function deps(adUnit) { + return { index: stubAuctionIndex({ adUnits: [adUnit] }) }; + } + beforeEach(function () { triggerPixelStub = sinon.stub(utils, 'triggerPixel'); insertHtmlIntoIframeStub = sinon.stub(utils, 'insertHtmlIntoIframe'); @@ -71,7 +85,8 @@ describe('native.js', function () { }); it('sends placeholders for configured assets', function () { - const bidRequest = { + const adUnit = { + transactionId: 'au', nativeParams: { body: { sendId: true }, clickUrl: { sendId: true }, @@ -85,7 +100,7 @@ describe('native.js', function () { } } }; - const targeting = getNativeTargeting(bid, bidRequest); + const targeting = getNativeTargeting(bid, deps(adUnit)); expect(targeting[CONSTANTS.NATIVE_KEYS.title]).to.equal(bid.native.title); expect(targeting[CONSTANTS.NATIVE_KEYS.body]).to.equal('hb_native_body:123'); @@ -95,7 +110,8 @@ describe('native.js', function () { }); it('should only include native targeting keys with values', function () { - const bidRequest = { + const adUnit = { + transactionId: 'au', nativeParams: { body: { sendId: true }, clickUrl: { sendId: true }, @@ -110,7 +126,7 @@ describe('native.js', function () { } }; - const targeting = getNativeTargeting(bidWithUndefinedFields, bidRequest); + const targeting = getNativeTargeting(bidWithUndefinedFields, deps(adUnit)); expect(Object.keys(targeting)).to.deep.equal([ CONSTANTS.NATIVE_KEYS.title, @@ -121,7 +137,8 @@ describe('native.js', function () { }); it('should only include targeting that has sendTargetingKeys set to true', function () { - const bidRequest = { + const adUnit = { + transactionId: 'au', nativeParams: { image: { required: true, @@ -136,7 +153,7 @@ describe('native.js', function () { } }; - const targeting = getNativeTargeting(bid, bidRequest); + const targeting = getNativeTargeting(bid, deps(adUnit)); expect(Object.keys(targeting)).to.deep.equal([ CONSTANTS.NATIVE_KEYS.title @@ -144,7 +161,8 @@ describe('native.js', function () { }); it('should only include targeting if sendTargetingKeys not set to false', function () { - const bidRequest = { + const adUnit = { + transactionId: 'au', nativeParams: { image: { required: true, @@ -181,7 +199,7 @@ describe('native.js', function () { } }; - const targeting = getNativeTargeting(bid, bidRequest); + const targeting = getNativeTargeting(bid, deps(adUnit)); expect(Object.keys(targeting)).to.deep.equal([ CONSTANTS.NATIVE_KEYS.title, @@ -193,7 +211,8 @@ describe('native.js', function () { }); it('should copy over rendererUrl to bid object and include it in targeting', function () { - const bidRequest = { + const adUnit = { + transactionId: 'au', nativeParams: { image: { required: true, @@ -209,7 +228,7 @@ describe('native.js', function () { } }; - const targeting = getNativeTargeting(bid, bidRequest); + const targeting = getNativeTargeting(bid, deps(adUnit)); expect(Object.keys(targeting)).to.deep.equal([ CONSTANTS.NATIVE_KEYS.title, @@ -227,7 +246,8 @@ describe('native.js', function () { }); it('should copy over adTemplate to bid object and include it in targeting', function () { - const bidRequest = { + const adUnit = { + transactionId: 'au', nativeParams: { image: { required: true, @@ -241,7 +261,7 @@ describe('native.js', function () { } }; - const targeting = getNativeTargeting(bid, bidRequest); + const targeting = getNativeTargeting(bid, deps(adUnit)); expect(Object.keys(targeting)).to.deep.equal([ CONSTANTS.NATIVE_KEYS.title, @@ -374,35 +394,33 @@ describe('native.js', function () { }); describe('validate native', function () { - let bidReq = [{ - bids: [{ - bidderCode: 'test_bidder', - bidId: 'test_bid_id', - mediaTypes: { - native: { - title: { - required: true, - }, - body: { - required: true, - }, - image: { - required: true, - sizes: [150, 50], - aspect_ratios: [150, 50] - }, - icon: { - required: true, - sizes: [50, 50] - }, - } + const adUnit = { + transactionId: 'test_adunit', + mediaTypes: { + native: { + title: { + required: true, + }, + body: { + required: true, + }, + image: { + required: true, + sizes: [150, 50], + aspect_ratios: [150, 50] + }, + icon: { + required: true, + sizes: [50, 50] + }, } - }] - }]; + } + } let validBid = { adId: 'abc123', requestId: 'test_bid_id', + transactionId: 'test_adunit', adUnitCode: '123/prebid_native_adunit', bidder: 'test_bidder', native: { @@ -428,6 +446,7 @@ describe('validate native', function () { let noIconDimBid = { adId: 'abc234', requestId: 'test_bid_id', + transactionId: 'test_adunit', adUnitCode: '123/prebid_native_adunit', bidder: 'test_bidder', native: { @@ -449,6 +468,7 @@ describe('validate native', function () { let noImgDimBid = { adId: 'abc345', requestId: 'test_bid_id', + transactionId: 'test_adunit', adUnitCode: '123/prebid_native_adunit', bidder: 'test_bidder', native: { @@ -472,11 +492,13 @@ describe('validate native', function () { afterEach(function () {}); it('should accept bid if no image sizes are defined', function () { - let result = nativeBidIsValid(validBid, bidReq); + decorateAdUnitsWithNativeParams([adUnit]); + const index = stubAuctionIndex({adUnits: [adUnit]}) + let result = nativeBidIsValid(validBid, {index}); expect(result).to.be.true; - result = nativeBidIsValid(noIconDimBid, bidReq); + result = nativeBidIsValid(noIconDimBid, {index}); expect(result).to.be.true; - result = nativeBidIsValid(noImgDimBid, bidReq); + result = nativeBidIsValid(noImgDimBid, {index}); expect(result).to.be.true; }); }); diff --git a/test/spec/sizeMapping_spec.js b/test/spec/sizeMapping_spec.js index a3c39a52441..c4efbddad6d 100644 --- a/test/spec/sizeMapping_spec.js +++ b/test/spec/sizeMapping_spec.js @@ -1,6 +1,6 @@ import { expect } from 'chai'; import { resolveStatus, setSizeConfig, sizeSupported } from 'src/sizeMapping.js'; -import includes from 'core-js-pure/features/array/includes.js'; +import {includes} from 'src/polyfill.js' let utils = require('src/utils'); let deepClone = utils.deepClone; diff --git a/test/spec/unit/core/adapterManager_spec.js b/test/spec/unit/core/adapterManager_spec.js index f414febcebe..88beaa88a67 100644 --- a/test/spec/unit/core/adapterManager_spec.js +++ b/test/spec/unit/core/adapterManager_spec.js @@ -1,5 +1,11 @@ import { expect } from 'chai'; -import adapterManager, { allS2SBidders, clientTestAdapters, gdprDataHandler, coppaDataHandler } from 'src/adapterManager.js'; +import adapterManager, { + gdprDataHandler, + coppaDataHandler, + _partitionBidders, + PARTITIONS, + getS2SBidderSet, _filterBidsForAdUnit +} from 'src/adapterManager.js'; import { getAdUnits, getServerTestingConfig, @@ -11,9 +17,9 @@ import * as utils from 'src/utils.js'; import { config } from 'src/config.js'; import { registerBidder } from 'src/adapters/bidderFactory.js'; import { setSizeConfig } from 'src/sizeMapping.js'; -import find from 'core-js-pure/features/array/find.js'; -import includes from 'core-js-pure/features/array/includes.js'; +import {find, includes} from 'src/polyfill.js'; import s2sTesting from 'modules/s2sTesting.js'; +import {hook} from '../../../../src/hook.js'; var events = require('../../../../src/events'); const CONFIG = { @@ -87,9 +93,14 @@ describe('adapterManager tests', function () { config.setConfig({s2sConfig: { enabled: false }}); }); + afterEach(() => { + s2sTesting.clientTestBidders.clear(); + }); + describe('callBids', function () { before(function () { config.setConfig({s2sConfig: { enabled: false }}); + hook.ready(); }); beforeEach(function () { @@ -700,10 +711,6 @@ describe('adapterManager tests', function () { prebidServerAdapterMock.callBids.reset(); }); - afterEach(function () { - allS2SBidders.length = 0; - }); - const bidRequests = [{ 'bidderCode': 'appnexus', 'auctionId': '1863e370099523', @@ -1304,9 +1311,6 @@ describe('adapterManager tests', function () { } beforeEach(function () { - allS2SBidders.length = 0; - clientTestAdapters.length = 0 - adapterManager.bidderRegistry['prebidServer'] = prebidServerAdapterMock; adapterManager.bidderRegistry['adequant'] = adequantAdapterMock; adapterManager.bidderRegistry['appnexus'] = appnexusAdapterMock; @@ -1661,13 +1665,32 @@ describe('adapterManager tests', function () { describe('makeBidRequests', function () { let adUnits; beforeEach(function () { - allS2SBidders.length = 0 adUnits = utils.deepClone(getAdUnits()).map(adUnit => { adUnit.bids = adUnit.bids.filter(bid => includes(['appnexus', 'rubicon'], bid.bidder)); return adUnit; }) }); + it('should add nativeParams to adUnits after BEFORE_REQUEST_BIDS', () => { + function beforeReqBids(adUnits) { + adUnits.forEach(adUnit => { + adUnit.mediaTypes.native = { + type: 'image', + } + }) + } + events.on(CONSTANTS.EVENTS.BEFORE_REQUEST_BIDS, beforeReqBids); + adapterManager.makeBidRequests( + adUnits, + Date.now(), + utils.getUniqueIdentifierStr(), + function callback() {}, + [] + ); + events.off(CONSTANTS.EVENTS.BEFORE_REQUEST_BIDS, beforeReqBids); + expect(adUnits.map((u) => u.nativeParams).some(i => i == null)).to.be.false; + }); + it('should make separate bidder request objects for each bidder', () => { adUnits = [utils.deepClone(getAdUnits()[0])]; @@ -1715,8 +1738,6 @@ describe('adapterManager tests', function () { let sandbox; beforeEach(function () { sandbox = sinon.sandbox.create(); - allS2SBidders.length = 0; - clientTestAdapters.length = 0; // always have matchMedia return true for us sandbox.stub(utils.getWindowTop(), 'matchMedia').callsFake(() => ({matches: true})); }); @@ -1892,7 +1913,7 @@ describe('adapterManager tests', function () { ['visitor-uk', 'desktop'] ); - // only one adUnit and one bid from that adUnit should make it through the applied labels above + // only one adUnit and one bid from that adUnit should make it through the applied labels above expect(bidRequests.length).to.equal(1); expect(bidRequests[0].bidderCode).to.equal('rubicon'); expect(bidRequests[0].bids.length).to.equal(1); @@ -1976,7 +1997,6 @@ describe('adapterManager tests', function () { describe('s2sTesting - testServerOnly', () => { beforeEach(() => { config.setConfig({ s2sConfig: getServerTestingConfig(CONFIG) }); - allS2SBidders.length = 0 s2sTesting.bidSource = {}; }); @@ -2107,7 +2127,6 @@ describe('adapterManager tests', function () { afterEach(() => { config.resetConfig() - allS2SBidders.length = 0; s2sTesting.bidSource = {}; }); @@ -2287,4 +2306,102 @@ describe('adapterManager tests', function () { ); }); }); + + describe('getS2SBidderSet', () => { + it('should always return the "null" bidder', () => { + expect([...getS2SBidderSet({bidders: []})]).to.eql([null]); + }); + + it('should not consider disabled s2s adapters', () => { + const actual = getS2SBidderSet([{enabled: false, bidders: ['A', 'B']}, {enabled: true, bidders: ['C']}]); + expect([...actual]).to.include.members(['C']); + expect([...actual]).not.to.include.members(['A', 'B']); + }); + + it('should accept both single config objects and an array of them', () => { + const conf = {enabled: true, bidders: ['A', 'B']}; + expect(getS2SBidderSet(conf)).to.eql(getS2SBidderSet([conf])); + }); + }); + + describe('separation of client and server bidders', () => { + let s2sBidders, getS2SBidders; + beforeEach(() => { + s2sBidders = null; + getS2SBidders = sinon.stub(); + getS2SBidders.callsFake(() => s2sBidders); + }) + + describe('partitionBidders', () => { + let adUnits; + + beforeEach(() => { + adUnits = [{ + bids: [{ + bidder: 'A' + }, { + bidder: 'B' + }] + }, { + bids: [{ + bidder: 'A', + }, { + bidder: 'C' + }] + }]; + }); + + function partition(adUnits, s2sConfigs) { + return _partitionBidders(adUnits, s2sConfigs, {getS2SBidders}) + } + + Object.entries({ + 'all client': { + s2s: [], + expected: { + [PARTITIONS.CLIENT]: ['A', 'B', 'C'], + [PARTITIONS.SERVER]: [] + } + }, + 'all server': { + s2s: ['A', 'B', 'C'], + expected: { + [PARTITIONS.CLIENT]: [], + [PARTITIONS.SERVER]: ['A', 'B', 'C'] + } + }, + 'mixed': { + s2s: ['B', 'C'], + expected: { + [PARTITIONS.CLIENT]: ['A'], + [PARTITIONS.SERVER]: ['B', 'C'] + } + } + }).forEach(([test, {s2s, expected}]) => { + it(`should partition ${test} requests`, () => { + s2sBidders = new Set(s2s); + const s2sConfig = {}; + expect(partition(adUnits, s2sConfig)).to.eql(expected); + sinon.assert.calledWith(getS2SBidders, sinon.match.same(s2sConfig)); + }); + }); + }); + + describe('filterBidsForAdUnit', () => { + function filterBids(bids, s2sConfig) { + return _filterBidsForAdUnit(bids, s2sConfig, {getS2SBidders}); + } + it('should not filter any bids when s2sConfig == null', () => { + const bids = ['untouched', 'data']; + expect(filterBids(bids)).to.eql(bids); + }); + + it('should remove bids that have bidder not present in s2sConfig', () => { + s2sBidders = new Set('A', 'B'); + const s2sConfig = {}; + expect(filterBids(['A', 'C', 'D'].map((code) => ({bidder: code})), s2sConfig)).to.eql([{bidder: 'A'}]); + sinon.assert.calledWith(getS2SBidders, sinon.match.same(s2sConfig)); + }) + }); + }); }); diff --git a/test/spec/unit/core/auctionIndex_spec.js b/test/spec/unit/core/auctionIndex_spec.js new file mode 100644 index 00000000000..f00e2cd281f --- /dev/null +++ b/test/spec/unit/core/auctionIndex_spec.js @@ -0,0 +1,129 @@ +import {AuctionIndex} from '../../../../src/auctionIndex.js'; + +describe('auction index', () => { + let index, auctions; + + function mockAuction(id, adUnits, bidderRequests) { + return { + getAuctionId() { return id }, + getAdUnits() { return adUnits; }, + getBidRequests() { return bidderRequests; } + } + } + + beforeEach(() => { + auctions = []; + index = new AuctionIndex(() => auctions); + }) + + describe('getAuction', () => { + beforeEach(() => { + auctions = [mockAuction('a1'), mockAuction('a2')]; + }); + + it('should find auctions by auctionId', () => { + expect(index.getAuction({auctionId: 'a1'})).to.equal(auctions[0]); + }); + + it('should return undef if auction is missing', () => { + expect(index.getAuction({auctionId: 'missing'})).to.be.undefined; + }); + + it('should return undef if no auctionId is provided', () => { + expect(index.getAuction({})).to.be.undefined; + }); + }); + + describe('getAdUnit', () => { + let adUnits; + + beforeEach(() => { + adUnits = [{transactionId: 'au1'}, {transactionId: 'au2'}]; + auctions = [ + mockAuction('a1', [adUnits[0], {}]), + mockAuction('a2', [adUnits[1]]) + ]; + }); + + it('should find adUnits by transactionId', () => { + expect(index.getAdUnit({transactionId: 'au2'})).to.equal(adUnits[1]); + }); + + it('should return undefined if adunit is missing', () => { + expect(index.getAdUnit({transactionId: 'missing'})).to.be.undefined; + }); + + it('should return undefined if no transactionId is provided', () => { + expect(index.getAdUnit({})).to.be.undefined; + }); + }); + + describe('getBidRequest', () => { + let bidRequests; + beforeEach(() => { + bidRequests = [{bidId: 'b1'}, {bidId: 'b2'}]; + auctions = [ + mockAuction('a1', [], [{bids: [bidRequests[0], {}]}]), + mockAuction('a2', [], [{bids: [bidRequests[1]]}]) + ] + }); + + it('should find bidRequests by requestId', () => { + expect(index.getBidRequest({requestId: 'b2'})).to.equal(bidRequests[1]); + }); + + it('should return undef if bidRequest is missing', () => { + expect(index.getBidRequest({requestId: 'missing'})).to.be.undefined; + }); + + it('should return undef if no requestId is provided', () => { + expect(index.getBidRequest({})).to.be.undefined; + }); + }); + + describe('getMediaTypes', () => { + let bidderRequests, mediaTypes, adUnits; + + beforeEach(() => { + mediaTypes = [{mockMT: '1'}, {mockMT: '2'}, {mockMT: '3'}, {mockMT: '4'}] + adUnits = [ + {transactionId: 'au1', mediaTypes: mediaTypes[0]}, + {transactionId: 'au2', mediaTypes: mediaTypes[1]} + ] + bidderRequests = [ + {bidderRequestId: 'ber1', bids: [{bidId: 'b1', mediaTypes: mediaTypes[2], transactionId: 'au1'}, {}]}, + {bidderRequestId: 'ber2', bids: [{bidId: 'b2', mediaTypes: mediaTypes[3], transactionId: 'au2'}]} + ] + auctions = [ + mockAuction('a1', [adUnits[0]], [bidderRequests[0], {}]), + mockAuction('a2', [adUnits[1]], [bidderRequests[1]]) + ] + }); + + it('should find mediaTypes by transactionId', () => { + expect(index.getMediaTypes({transactionId: 'au2'})).to.equal(mediaTypes[1]); + }); + + it('should find mediaTypes by requestId', () => { + expect(index.getMediaTypes({requestId: 'b1'})).to.equal(mediaTypes[2]); + }); + + it('should give precedence to request.mediaTypes over adUnit.mediaTypes', () => { + expect(index.getMediaTypes({requestId: 'b2', transactionId: 'au2'})).to.equal(mediaTypes[3]); + }); + + it('should return undef if requestId and transactionId do not match', () => { + expect(index.getMediaTypes({requestId: 'b1', transactionId: 'au2'})).to.be.undefined; + }); + + it('should return undef if no params are provided', () => { + expect(index.getMediaTypes({})).to.be.undefined; + }); + + ['requestId', 'transactionId'].forEach(param => { + it(`should return undef if ${param} is missing`, () => { + expect(index.getMediaTypes({[param]: 'missing'})).to.be.undefined; + }); + }) + }); +}); diff --git a/test/spec/unit/core/bidderFactory_spec.js b/test/spec/unit/core/bidderFactory_spec.js index 4dc79deaf85..067f9abe424 100644 --- a/test/spec/unit/core/bidderFactory_spec.js +++ b/test/spec/unit/core/bidderFactory_spec.js @@ -7,7 +7,10 @@ import * as utils from 'src/utils.js'; import { config } from 'src/config.js'; import { server } from 'test/mocks/xhr.js'; import CONSTANTS from 'src/constants.json'; -import events from 'src/events.js'; +import * as events from 'src/events.js'; +import {hook} from '../../../../src/hook.js'; +import {auctionManager} from '../../../../src/auctionManager.js'; +import {stubAuctionIndex} from '../../../helpers/indexStub.js'; const CODE = 'sampleBidder'; const MOCK_BIDS_REQUEST = { @@ -43,6 +46,10 @@ describe('bidders created by newBidder', function () { let addBidResponseStub; let doneStub; + before(() => { + hook.ready(); + }); + beforeEach(function () { spec = { code: CODE, @@ -59,17 +66,22 @@ describe('bidders created by newBidder', function () { describe('when the ajax response is irrelevant', function () { let ajaxStub; let getConfigSpy; + let aliasRegistryStub, aliasRegistry; beforeEach(function () { ajaxStub = sinon.stub(ajax, 'ajax'); addBidResponseStub.reset(); getConfigSpy = sinon.spy(config, 'getConfig'); doneStub.reset(); + aliasRegistry = {}; + aliasRegistryStub = sinon.stub(adapterManager, 'aliasRegistry'); + aliasRegistryStub.get(() => aliasRegistry); }); afterEach(function () { ajaxStub.restore(); getConfigSpy.restore(); + aliasRegistryStub.restore(); }); it('should let registerSyncs run with invalid alias and aliasSync enabled', function () { @@ -116,6 +128,7 @@ describe('bidders created by newBidder', function () { }); spec.code = 'aliasBidder'; const bidder = newBidder(spec); + aliasRegistry = {[spec.code]: CODE}; bidder.callBids({ bids: [] }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); expect(getConfigSpy.withArgs('userSync.filterSettings').calledOnce).to.equal(false); }); @@ -548,6 +561,30 @@ describe('bidders created by newBidder', function () { expect(logErrorSpy.calledOnce).to.equal(true); }); + + it('should require requestId from interpretResponse', () => { + const bidder = newBidder(spec); + const bid = { + 'ad': 'creative', + 'cpm': '1.99', + 'creativeId': 'some-id', + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 360 + }; + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: 'test.url.com', + data: {} + }); + spec.getUserSyncs.returns([]); + spec.interpretResponse.returns(bid); + + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + expect(addBidResponseStub.called).to.be.false; + }); }); describe('when the ajax call fails', function () { @@ -783,7 +820,7 @@ describe('registerBidder', function () { describe('validate bid response: ', function () { let spec; - let bidder; + let indexStub, adUnits, bidderRequests; let addBidResponseStub; let doneStub; let ajaxStub; @@ -824,25 +861,34 @@ describe('validate bid response: ', function () { callbacks.success('response body', { getResponseHeader: fakeResponse }); }); logErrorSpy = sinon.spy(utils, 'logError'); + indexStub = sinon.stub(auctionManager, 'index'); + adUnits = []; + bidderRequests = []; + indexStub.get(() => stubAuctionIndex({adUnits: adUnits, bidderRequests: bidderRequests})) }); afterEach(function () { ajaxStub.restore(); logErrorSpy.restore(); + indexStub.restore; }); it('should add native bids that do have required assets', function () { + adUnits = [{ + transactionId: 'au', + nativeParams: { + title: {'required': true}, + } + }] let bidRequest = { bids: [{ bidId: '1', auctionId: 'first-bid-id', adUnitCode: 'mock/placement', + transactionId: 'au', params: { param: 5 }, - nativeParams: { - title: {'required': true}, - }, mediaType: 'native', }] }; @@ -869,21 +915,24 @@ describe('validate bid response: ', function () { }); it('should not add native bids that do not have required assets', function () { + adUnits = [{ + transactionId: 'au', + nativeParams: { + title: {'required': true}, + }, + }]; let bidRequest = { bids: [{ bidId: '1', auctionId: 'first-bid-id', adUnitCode: 'mock/placement', + transactionId: 'au', params: { param: 5 }, - nativeParams: { - title: {'required': true}, - }, mediaType: 'native', }] }; - let bids1 = Object.assign({}, bids[0], { @@ -905,17 +954,21 @@ describe('validate bid response: ', function () { }); it('should add bid when renderer is present on outstream bids', function () { + adUnits = [{ + transactionId: 'au', + mediaTypes: { + video: {context: 'outstream'} + } + }] let bidRequest = { bids: [{ bidId: '1', auctionId: 'first-bid-id', + transactionId: 'au', adUnitCode: 'mock/placement', params: { param: 5 }, - mediaTypes: { - video: {context: 'outstream'} - } }] }; @@ -951,7 +1004,7 @@ describe('validate bid response: ', function () { sizes: [[300, 250]], }] }; - + bidderRequests = [bidRequest]; let bids1 = Object.assign({}, bids[0], { diff --git a/test/spec/unit/core/bidderSettings_spec.js b/test/spec/unit/core/bidderSettings_spec.js new file mode 100644 index 00000000000..ece18040d1e --- /dev/null +++ b/test/spec/unit/core/bidderSettings_spec.js @@ -0,0 +1,123 @@ +import {bidderSettings, ScopedSettings} from '../../../../src/bidderSettings.js'; +import {expect} from 'chai'; +import * as prebidGlobal from '../../../../src/prebidGlobal'; +import sinon from 'sinon'; + +describe('ScopedSettings', () => { + let data; + let settings; + + beforeEach(() => { + settings = new ScopedSettings(() => data, 'fallback'); + }); + + describe('get', () => { + it('should retrieve setting from scope', () => { + data = { + scope: {key: 'value'} + }; + expect(settings.get('scope', 'key')).to.equal('value'); + }); + + it('should fallback to fallback scope', () => { + data = { + fallback: { + key: 'value' + } + }; + expect(settings.get('scope', 'key')).to.equal('value'); + }); + + it('should retrieve from default scope if scope is null', () => { + data = { + fallback: { + key: 'value' + } + }; + + expect(settings.get(null, 'key')).to.equal('value'); + }); + + it('should not fall back if own setting has a falsy value', () => { + data = { + scope: { + key: false, + }, + fallback: { + key: true + } + } + expect(settings.get('scope', 'key')).to.equal(false); + }) + }); + + describe('getOwn', () => { + it('should not fall back to default scope', () => { + data = { + fallback: { + key: 'value' + } + }; + expect(settings.getOwn('missing', 'key')).to.be.undefined; + }); + + it('should use default if scope is null', () => { + data = { + fallback: { + key: 'value' + } + }; + expect(settings.getOwn(null, 'key')).to.equal('value'); + }); + }); + + describe('getScopes', () => { + it('should return all top-level keys except the default scope', () => { + data = { + fallback: {}, + scope1: {}, + scope2: {}, + }; + expect(settings.getScopes()).to.have.members(['scope1', 'scope2']); + }); + }); + + describe('settingsFor', () => { + it('should merge with default scope', () => { + data = { + fallback: { + dkey: 'value' + }, + scope: { + skey: 'value' + } + } + expect(settings.settingsFor('scope')).to.eql({ + dkey: 'value', + skey: 'value' + }) + }) + }); +}); + +describe('bidderSettings', () => { + let sandbox; + beforeEach(() => { + sandbox = sinon.sandbox.create(); + sandbox.stub(prebidGlobal, 'getGlobal').returns({ + bidderSettings: { + scope: { + key: 'value' + } + } + }); + }) + + afterEach(() => { + sandbox.restore(); + }) + + it('should fetch data from getGlobal().bidderSettings', () => { + expect(bidderSettings.get('scope', 'key')).to.equal('value'); + }) +}); diff --git a/test/spec/unit/core/consentHandler_spec.js b/test/spec/unit/core/consentHandler_spec.js new file mode 100644 index 00000000000..bee5a2d9522 --- /dev/null +++ b/test/spec/unit/core/consentHandler_spec.js @@ -0,0 +1,41 @@ +import {ConsentHandler} from '../../../../src/consentHandler.js'; + +describe('Consent data handler', () => { + let handler; + beforeEach(() => { + handler = new ConsentHandler(); + }) + + it('should be disabled, return null data on init', () => { + expect(handler.enabled).to.be.false; + expect(handler.getConsentData()).to.equal(null); + }) + + it('should resolve promise to null when disabled', () => { + return handler.promise.then((data) => { + expect(data).to.equal(null); + }); + }); + + it('should return data after setConsentData', () => { + const data = {consent: 'string'}; + handler.enable(); + handler.setConsentData(data); + expect(handler.getConsentData()).to.equal(data); + }); + + it('should resolve .promise to data after setConsentData', (done) => { + let actual = null; + const data = {consent: 'string'}; + handler.enable(); + handler.promise.then((d) => actual = d); + setTimeout(() => { + expect(actual).to.equal(null); + handler.setConsentData(data); + setTimeout(() => { + expect(actual).to.equal(data); + done(); + }) + }) + }); +}) diff --git a/test/spec/unit/core/storageManager_spec.js b/test/spec/unit/core/storageManager_spec.js index 5bb766217f5..74a3b3b023f 100644 --- a/test/spec/unit/core/storageManager_spec.js +++ b/test/spec/unit/core/storageManager_spec.js @@ -1,8 +1,19 @@ -import { resetData, getCoreStorageManager, storageCallbacks, getStorageManager } from 'src/storageManager.js'; +import { + resetData, + getCoreStorageManager, + storageCallbacks, + getStorageManager, + newStorageManager +} from 'src/storageManager.js'; import { config } from 'src/config.js'; import * as utils from 'src/utils.js'; +import {hook} from '../../../../src/hook.js'; describe('storage manager', function() { + before(() => { + hook.ready(); + }); + beforeEach(function() { resetData(); }); @@ -95,4 +106,62 @@ describe('storage manager', function() { expect(localStorage.getItem('unrelated')).to.be.eq('dummy'); }); }); + + describe('when bidderSettings.allowStorage is defined', () => { + const DENIED_BIDDER = 'denied-bidder'; + const DENY_KEY = 'storageAllowed'; + + const COOKIE = 'test-cookie'; + const LS_KEY = 'test-localstorage'; + + function mockBidderSettings() { + return { + get(bidder, key) { + if (bidder === DENIED_BIDDER && key === DENY_KEY) { + return false; + } else { + return undefined; + } + } + } + } + + Object.entries({ + disallowed: [DENIED_BIDDER, false], + allowed: ['allowed-bidder', true] + }).forEach(([test, [bidderCode, shouldWork]]) => { + describe(`for ${test} bidders`, () => { + let mgr; + + beforeEach(() => { + mgr = newStorageManager({bidderCode: bidderCode}, {bidderSettings: mockBidderSettings()}); + }) + + afterEach(() => { + mgr.setCookie(COOKIE, 'delete', new Date().toUTCString()); + mgr.removeDataFromLocalStorage(LS_KEY); + }) + + const testDesc = (desc) => `should ${shouldWork ? '' : 'not'} ${desc}`; + + it(testDesc('allow cookies'), () => { + mgr.setCookie(COOKIE, 'value'); + expect(mgr.getCookie(COOKIE)).to.equal(shouldWork ? 'value' : null); + }); + + it(testDesc('allow localStorage'), () => { + mgr.setDataInLocalStorage(LS_KEY, 'value'); + expect(mgr.getDataFromLocalStorage(LS_KEY)).to.equal(shouldWork ? 'value' : null); + }); + + it(testDesc('report localStorage as available'), () => { + expect(mgr.hasLocalStorage()).to.equal(shouldWork); + }); + + it(testDesc('report cookies as available'), () => { + expect(mgr.cookiesAreEnabled()).to.equal(shouldWork); + }); + }); + }); + }) }); diff --git a/test/spec/unit/core/targeting_spec.js b/test/spec/unit/core/targeting_spec.js index 1064d7c0f7d..53aa3b90fa8 100644 --- a/test/spec/unit/core/targeting_spec.js +++ b/test/spec/unit/core/targeting_spec.js @@ -5,6 +5,7 @@ import { createBidReceived } from 'test/fixtures/fixtures.js'; import CONSTANTS from 'src/constants.json'; import { auctionManager } from 'src/auctionManager.js'; import * as utils from 'src/utils.js'; +import {deepClone} from 'src/utils.js'; const bid1 = { 'bidderCode': 'rubicon', @@ -227,6 +228,8 @@ describe('targeting tests', function () { let sandbox; let enableSendAllBids = false; let useBidCache; + let bidCacheFilterFunction; + let undef; beforeEach(function() { sandbox = sinon.sandbox.create(); @@ -241,12 +244,16 @@ describe('targeting tests', function () { if (key === 'useBidCache') { return useBidCache; } + if (key === 'bidCacheFilterFunction') { + return bidCacheFilterFunction; + } return origGetConfig.apply(config, arguments); }); }); afterEach(function () { sandbox.restore(); + bidCacheFilterFunction = undef; }); describe('getAllTargeting', function () { @@ -461,6 +468,50 @@ describe('targeting tests', function () { }); }); + describe('targetingControls.allowZeroCpmBids', function () { + let bid4; + let bidderSettingsStorage; + + before(function() { + bidderSettingsStorage = $$PREBID_GLOBAL$$.bidderSettings; + }); + + beforeEach(function () { + bid4 = utils.deepClone(bid1); + bid4.adserverTargeting = { + hb_pb: '0.0', + hb_adid: '567891011', + hb_bidder: 'appnexus', + }; + bid4.bidder = bid4.bidderCode = 'appnexus'; + bid4.cpm = 0; + bidsReceived = [bid4]; + }); + + after(function() { + bidsReceived = [bid1, bid2, bid3]; + $$PREBID_GLOBAL$$.bidderSettings = bidderSettingsStorage; + }) + + it('targeting should not include a 0 cpm by default', function() { + bid4.adserverTargeting = {}; + const targeting = targetingInstance.getAllTargeting(['/123456/header-bid-tag-0']); + expect(targeting['/123456/header-bid-tag-0']).to.deep.equal({}); + }); + + it('targeting should allow a 0 cpm with targetingControls.allowZeroCpmBids set to true', function () { + $$PREBID_GLOBAL$$.bidderSettings = { + standard: { + allowZeroCpmBids: true + } + }; + + const targeting = targetingInstance.getAllTargeting(['/123456/header-bid-tag-0']); + expect(targeting['/123456/header-bid-tag-0']).to.include.all.keys('hb_pb', 'hb_bidder', 'hb_adid', 'hb_bidder_appnexus', 'hb_adid_appnexus', 'hb_pb_appnexus'); + expect(targeting['/123456/header-bid-tag-0']['hb_pb']).to.equal('0.0') + }); + }); + describe('targetingControls.allowTargetingKeys', function () { let bid4; @@ -501,6 +552,77 @@ describe('targeting tests', function () { }); }); + describe('targetingControls.addTargetingKeys', function () { + let winningBid = null; + + beforeEach(function () { + bidsReceived = [bid1, bid2, nativeBid1, nativeBid2].map(deepClone); + bidsReceived.forEach((bid) => { + bid.adserverTargeting[CONSTANTS.TARGETING_KEYS.SOURCE] = 'test-source'; + bid.adUnitCode = 'adunit'; + if (winningBid == null || bid.cpm > winningBid.cpm) { + winningBid = bid; + } + }); + enableSendAllBids = true; + }); + + const expandKey = function (key) { + const keys = new Set(); + if (winningBid.adserverTargeting[key] != null) { + keys.add(key); + } + bidsReceived + .filter((bid) => bid.adserverTargeting[key] != null) + .map((bid) => bid.bidderCode) + .forEach((code) => keys.add(`${key}_${code}`.substr(0, 20))); + return new Array(...keys); + } + + const targetingResult = function () { + return targetingInstance.getAllTargeting(['adunit'])['adunit']; + } + + it('should include added keys', function () { + config.setConfig({ + targetingControls: { + addTargetingKeys: ['SOURCE'] + } + }); + expect(targetingResult()).to.include.all.keys(...expandKey(CONSTANTS.TARGETING_KEYS.SOURCE)); + }); + + it('should keep default and native keys', function() { + config.setConfig({ + targetingControls: { + addTargetingKeys: ['SOURCE'] + } + }); + const defaultKeys = new Set(Object.values(CONSTANTS.DEFAULT_TARGETING_KEYS)); + Object.values(CONSTANTS.NATIVE_KEYS).forEach((k) => defaultKeys.add(k)); + + const expectedKeys = new Set(); + bidsReceived + .map((bid) => Object.keys(bid.adserverTargeting)) + .reduce((left, right) => left.concat(right), []) + .filter((key) => defaultKeys.has(key)) + .map(expandKey) + .reduce((left, right) => left.concat(right), []) + .forEach((k) => expectedKeys.add(k)); + expect(targetingResult()).to.include.all.keys(...expectedKeys); + }); + + it('should not be allowed together with allowTargetingKeys', function () { + config.setConfig({ + targetingControls: { + allowTargetingKeys: [CONSTANTS.TARGETING_KEYS.BIDDER], + addTargetingKeys: [CONSTANTS.TARGETING_KEYS.SOURCE] + } + }); + expect(targetingResult).to.throw(); + }); + }); + describe('targetingControls.allowSendAllBidsTargetingKeys', function () { let bid4; @@ -785,6 +907,93 @@ describe('targeting tests', function () { expect(bids[0].adId).to.equal('adid-2'); }); + it('should use bidCacheFilterFunction', function() { + auctionManagerStub.returns([ + createBidReceived({bidder: 'appnexus', cpm: 7, auctionId: 1, responseTimestamp: 100, adUnitCode: 'code-0', adId: 'adid-1', mediaType: 'banner'}), + createBidReceived({bidder: 'appnexus', cpm: 5, auctionId: 2, responseTimestamp: 102, adUnitCode: 'code-0', adId: 'adid-2', mediaType: 'banner'}), + createBidReceived({bidder: 'appnexus', cpm: 6, auctionId: 1, responseTimestamp: 101, adUnitCode: 'code-1', adId: 'adid-3', mediaType: 'banner'}), + createBidReceived({bidder: 'appnexus', cpm: 8, auctionId: 2, responseTimestamp: 103, adUnitCode: 'code-1', adId: 'adid-4', mediaType: 'banner'}), + createBidReceived({bidder: 'appnexus', cpm: 27, auctionId: 1, responseTimestamp: 100, adUnitCode: 'code-2', adId: 'adid-5', mediaType: 'video'}), + createBidReceived({bidder: 'appnexus', cpm: 25, auctionId: 2, responseTimestamp: 102, adUnitCode: 'code-2', adId: 'adid-6', mediaType: 'video'}), + createBidReceived({bidder: 'appnexus', cpm: 26, auctionId: 1, responseTimestamp: 101, adUnitCode: 'code-3', adId: 'adid-7', mediaType: 'video'}), + createBidReceived({bidder: 'appnexus', cpm: 28, auctionId: 2, responseTimestamp: 103, adUnitCode: 'code-3', adId: 'adid-8', mediaType: 'video'}), + ]); + + let adUnitCodes = ['code-0', 'code-1', 'code-2', 'code-3']; + targetingInstance.setLatestAuctionForAdUnit('code-0', 2); + targetingInstance.setLatestAuctionForAdUnit('code-1', 2); + targetingInstance.setLatestAuctionForAdUnit('code-2', 2); + targetingInstance.setLatestAuctionForAdUnit('code-3', 2); + + // Bid Caching On, No Filter Function + useBidCache = true; + bidCacheFilterFunction = undef; + let bids = targetingInstance.getWinningBids(adUnitCodes); + + expect(bids.length).to.equal(4); + expect(bids[0].adId).to.equal('adid-1'); + expect(bids[1].adId).to.equal('adid-4'); + expect(bids[2].adId).to.equal('adid-5'); + expect(bids[3].adId).to.equal('adid-8'); + + // Bid Caching Off, No Filter Function + useBidCache = false; + bidCacheFilterFunction = undef; + bids = targetingInstance.getWinningBids(adUnitCodes); + + expect(bids.length).to.equal(4); + expect(bids[0].adId).to.equal('adid-2'); + expect(bids[1].adId).to.equal('adid-4'); + expect(bids[2].adId).to.equal('adid-6'); + expect(bids[3].adId).to.equal('adid-8'); + + // Bid Caching On AGAIN, No Filter Function (should be same as first time) + useBidCache = true; + bidCacheFilterFunction = undef; + bids = targetingInstance.getWinningBids(adUnitCodes); + + expect(bids.length).to.equal(4); + expect(bids[0].adId).to.equal('adid-1'); + expect(bids[1].adId).to.equal('adid-4'); + expect(bids[2].adId).to.equal('adid-5'); + expect(bids[3].adId).to.equal('adid-8'); + + // Bid Caching On, with Filter Function to Exclude video + useBidCache = true; + let bcffCalled = 0; + bidCacheFilterFunction = bid => { + bcffCalled++; + return bid.mediaType !== 'video'; + } + bids = targetingInstance.getWinningBids(adUnitCodes); + + expect(bids.length).to.equal(4); + expect(bids[0].adId).to.equal('adid-1'); + expect(bids[1].adId).to.equal('adid-4'); + expect(bids[2].adId).to.equal('adid-6'); + expect(bids[3].adId).to.equal('adid-8'); + // filter function should have been called for each cached bid (4 times) + expect(bcffCalled).to.equal(4); + + // Bid Caching Off, with Filter Function to Exclude video + // - should not use cached bids or call the filter function + useBidCache = false; + bcffCalled = 0; + bidCacheFilterFunction = bid => { + bcffCalled++; + return bid.mediaType !== 'video'; + } + bids = targetingInstance.getWinningBids(adUnitCodes); + + expect(bids.length).to.equal(4); + expect(bids[0].adId).to.equal('adid-2'); + expect(bids[1].adId).to.equal('adid-4'); + expect(bids[2].adId).to.equal('adid-6'); + expect(bids[3].adId).to.equal('adid-8'); + // filter function should not have been called + expect(bcffCalled).to.equal(0); + }); + it('should not use rendered bid to get winning bid', function () { let bidsReceived = [ createBidReceived({bidder: 'appnexus', cpm: 8, auctionId: 1, responseTimestamp: 100, adUnitCode: 'code-0', adId: 'adid-1', status: 'rendered'}), diff --git a/test/spec/unit/pbjs_api_spec.js b/test/spec/unit/pbjs_api_spec.js index 962fae60db1..5302b3b8c74 100644 --- a/test/spec/unit/pbjs_api_spec.js +++ b/test/spec/unit/pbjs_api_spec.js @@ -15,7 +15,10 @@ import * as ajaxLib from 'src/ajax.js'; import * as auctionModule from 'src/auction.js'; import { registerBidder } from 'src/adapters/bidderFactory.js'; import { _sendAdToCreative } from 'src/secureCreatives.js'; -import find from 'core-js-pure/features/array/find.js'; +import {find} from 'src/polyfill.js'; +import {synchronizePromise} from '../../helpers/syncPromise.js'; +import 'src/prebid.js'; +import {hook} from '../../../src/hook.js'; var assert = require('chai').assert; var expect = require('chai').expect; @@ -190,13 +193,21 @@ window.apntag = { } describe('Unit: Prebid Module', function () { - let bidExpiryStub; + let bidExpiryStub, promiseSandbox; + + before(() => { + hook.ready(); + }); + beforeEach(function () { + promiseSandbox = sinon.createSandbox(); + synchronizePromise(promiseSandbox); bidExpiryStub = sinon.stub(filters, 'isBidNotExpired').callsFake(() => true); configObj.setConfig({ useBidCache: true }); }); afterEach(function() { + promiseSandbox.restore(); $$PREBID_GLOBAL$$.adUnits = []; bidExpiryStub.restore(); configObj.setConfig({ useBidCache: false }); @@ -422,6 +433,7 @@ describe('Unit: Prebid Module', function () { let bid; let auction; let ajaxStub; + let indexStub; let cbTimeout = 3000; let targeting; @@ -487,7 +499,8 @@ describe('Unit: Prebid Module', function () { ], 'bidId': '4d0a6829338a07', 'bidderRequestId': '331f3cf3f1d9c8', - 'auctionId': '20882439e3238c' + 'auctionId': '20882439e3238c', + 'transactionId': 'trdiv-gpt-ad-1460505748561-0', } ], 'auctionStart': 1505250713622, @@ -505,6 +518,7 @@ describe('Unit: Prebid Module', function () { let auctionManagerInstance = newAuctionManager(); targeting = newTargeting(auctionManagerInstance); let adUnits = [{ + transactionId: 'trdiv-gpt-ad-1460505748561-0', code: 'div-gpt-ad-1460505748561-0', sizes: [[300, 250], [300, 600]], bids: [{ @@ -516,6 +530,8 @@ describe('Unit: Prebid Module', function () { }]; let adUnitCodes = ['div-gpt-ad-1460505748561-0']; auction = auctionManagerInstance.createAuction({adUnits, adUnitCodes}); + indexStub = sinon.stub(auctionManager, 'index'); + indexStub.get(() => auctionManagerInstance.index); ajaxStub = sinon.stub(ajaxLib, 'ajaxBuilder').callsFake(function() { return function(url, callback) { const fakeResponse = sinon.stub(); @@ -527,6 +543,7 @@ describe('Unit: Prebid Module', function () { afterEach(function () { ajaxStub.restore(); + indexStub.restore(); }); it('should get correct ' + CONSTANTS.TARGETING_KEYS.PRICE_BUCKET + ' when using bid.cpm is between 0 to 5', function() { @@ -566,6 +583,7 @@ describe('Unit: Prebid Module', function () { let cbTimeout = 3000; let auctionManagerInstance; let targeting; + let indexStub; const bannerResponse = { 'version': '0.0.1', @@ -644,6 +662,7 @@ describe('Unit: Prebid Module', function () { } const adUnit = { + transactionId: `tr${code}`, code: code, sizes: [[300, 250], [300, 600]], bids: [{ @@ -656,22 +675,22 @@ describe('Unit: Prebid Module', function () { let _mediaTypes = {}; if (mediaTypes.indexOf('banner') !== -1) { - _mediaTypes['banner'] = { + Object.assign(_mediaTypes, { 'banner': {} - }; + }); } if (mediaTypes.indexOf('video') !== -1) { - _mediaTypes['video'] = { + Object.assign(_mediaTypes, { 'video': { context: 'instream', playerSize: [300, 250] } - }; + }); } if (mediaTypes.indexOf('native') !== -1) { - _mediaTypes['native'] = { + Object.assign(_mediaTypes, { 'native': {} - }; + }); } if (Object.keys(_mediaTypes).length > 0) { @@ -733,35 +752,41 @@ describe('Unit: Prebid Module', function () { before(function () { currentPriceBucket = configObj.getConfig('priceGranularity'); - sinon.stub(adapterManager, 'makeBidRequests').callsFake(() => ([{ - 'bidderCode': 'appnexus', - 'auctionId': '20882439e3238c', - 'bidderRequestId': '331f3cf3f1d9c8', - 'bids': [ - { - 'bidder': 'appnexus', - 'params': { - 'placementId': '10433394' - }, - 'adUnitCode': 'div-gpt-ad-1460505748561-0', - 'sizes': [ - [ - 300, - 250 + sinon.stub(adapterManager, 'makeBidRequests').callsFake(() => { + const br = { + 'bidderCode': 'appnexus', + 'auctionId': '20882439e3238c', + 'bidderRequestId': '331f3cf3f1d9c8', + 'bids': [ + { + 'bidder': 'appnexus', + 'params': { + 'placementId': '10433394' + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': 'trdiv-gpt-ad-1460505748561-0', + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] ], - [ - 300, - 600 - ] - ], - 'bidId': '4d0a6829338a07', - 'bidderRequestId': '331f3cf3f1d9c8', - 'auctionId': '20882439e3238c' - } - ], - 'auctionStart': 1505250713622, - 'timeout': 3000 - }])); + 'bidId': '4d0a6829338a07', + 'bidderRequestId': '331f3cf3f1d9c8', + 'auctionId': '20882439e3238c' + } + ], + 'auctionStart': 1505250713622, + 'timeout': 3000 + }; + const au = auction.getAdUnits().find((au) => au.transactionId === br.bids[0].transactionId); + br.bids[0].mediaTypes = Object.assign({}, au.mediaTypes); + return [br]; + }); }); after(function () { @@ -769,8 +794,14 @@ describe('Unit: Prebid Module', function () { adapterManager.makeBidRequests.restore(); }) + beforeEach(() => { + indexStub = sinon.stub(auctionManager, 'index'); + indexStub.get(() => auctionManagerInstance.index); + }); + afterEach(function () { ajaxStub.restore(); + indexStub.restore(); }); it('should get correct ' + CONSTANTS.TARGETING_KEYS.PRICE_BUCKET + ' with cpm between 0 - 5', function() { @@ -1002,12 +1033,7 @@ describe('Unit: Prebid Module', function () { adUnitCode: config.adUnitCodes[0], }; - const event = { - source: { postMessage: sinon.stub() }, - origin: 'origin.sf.com' - }; - - _sendAdToCreative(mockAdObject, event); + _sendAdToCreative(mockAdObject, sinon.stub()); expect(slots[0].spyGetSlotElementId.called).to.equal(false); expect(slots[1].spyGetSlotElementId.called).to.equal(true); @@ -1135,13 +1161,15 @@ describe('Unit: Prebid Module', function () { height: 0 } }, - getElementsByTagName: sinon.stub() + getElementsByTagName: sinon.stub(), + querySelector: sinon.stub() }; elStub = { insertBefore: sinon.stub() }; doc.getElementsByTagName.returns([elStub]); + doc.querySelector.returns(elStub); spyLogError = sinon.spy(utils, 'logError'); spyLogMessage = sinon.spy(utils, 'logMessage'); @@ -1598,6 +1626,7 @@ describe('Unit: Prebid Module', function () { let auctionArgs; beforeEach(function () { + auctionArgs = null; adUnitsBackup = auction.getAdUnits auctionManagerStub = sinon.stub(auctionManager, 'createAuction').callsFake(function() { auctionArgs = arguments[0]; @@ -1852,11 +1881,39 @@ describe('Unit: Prebid Module', function () { pos: 2 } } + }, + { + code: 'test6', + bids: [], + sizes: [300, 250], + mediaTypes: { + banner: { + sizes: [300, 250], + pos: 0 + } + } }]; $$PREBID_GLOBAL$$.requestBids({ adUnits: adUnit }); expect(auctionArgs.adUnits[0].mediaTypes.banner.pos).to.equal(2); + expect(auctionArgs.adUnits[1].mediaTypes.banner.pos).to.equal(0); + }); + + it(`should allow no bids if 'ortb2Imp' is specified`, () => { + const adUnit = { + code: 'test', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + ortb2Imp: {} + }; + $$PREBID_GLOBAL$$.requestBids({ + adUnits: [adUnit] + }); + sinon.assert.match(auctionArgs.adUnits[0], adUnit); }); }); @@ -1993,7 +2050,7 @@ describe('Unit: Prebid Module', function () { }); expect(auctionArgs.adUnits.length).to.equal(1); expect(auctionArgs.adUnits[1]).to.not.exist; - assert.ok(logErrorSpy.calledWith("Detected adUnit.code 'bad-ad-unit-2' did not have 'adUnit.bids' defined or 'adUnit.bids' is not an array. Removing adUnit from auction.")); + assert.ok(logErrorSpy.calledWith("adUnit.code 'bad-ad-unit-2' has no 'adUnit.bids' and no 'adUnit.ortb2Imp'. Removing adUnit from auction")); }); }); }); diff --git a/test/spec/unit/secureCreatives_spec.js b/test/spec/unit/secureCreatives_spec.js index cee416bd1be..e3dc21ffd92 100644 --- a/test/spec/unit/secureCreatives_spec.js +++ b/test/spec/unit/secureCreatives_spec.js @@ -1,20 +1,60 @@ import { - _sendAdToCreative, receiveMessage + _sendAdToCreative, getReplier, receiveMessage } from 'src/secureCreatives.js'; +import * as secureCreatives from 'src/secureCreatives.js'; import * as utils from 'src/utils.js'; import {getAdUnits, getBidRequests, getBidResponses} from 'test/fixtures/fixtures.js'; import {auctionManager} from 'src/auctionManager.js'; import * as auctionModule from 'src/auction.js'; import * as native from 'src/native.js'; import {fireNativeTrackers, getAllAssetsMessage} from 'src/native.js'; -import events from 'src/events.js'; +import * as events from 'src/events.js'; import { config as configObj } from 'src/config.js'; +import 'src/prebid.js'; import { expect } from 'chai'; var CONSTANTS = require('src/constants.json'); describe('secureCreatives', () => { + function makeEvent(ev) { + return Object.assign({origin: 'mock-origin', ports: []}, ev) + } + + describe('getReplier', () => { + it('should use source.postMessage if no MessagePort is available', () => { + const ev = { + ports: [], + source: { + postMessage: sinon.spy() + }, + origin: 'mock-origin' + }; + getReplier(ev)('test'); + sinon.assert.calledWith(ev.source.postMessage, JSON.stringify('test')); + }); + + it('should use MesagePort.postMessage if available', () => { + const ev = { + ports: [{ + postMessage: sinon.spy() + }] + } + getReplier(ev)('test'); + sinon.assert.calledWith(ev.ports[0].postMessage, JSON.stringify('test')); + }); + + it('should throw if origin is null and no MessagePort is available', () => { + const ev = { + origin: null, + ports: [], + postMessage: sinon.spy() + } + const reply = getReplier(ev); + expect(() => reply('test')).to.throw(); + }); + }); + describe('_sendAdToCreative', () => { beforeEach(function () { sinon.stub(utils, 'logError'); @@ -40,14 +80,10 @@ describe('secureCreatives', () => { cpm: '1.00', adUnitCode: 'some_dom_id' }; - const event = { - source: { postMessage: sinon.stub() }, - origin: 'origin.sf.com' - }; - - _sendAdToCreative(mockAdObject, event); - expect(JSON.parse(event.source.postMessage.args[0][0]).ad).to.equal(''); - expect(JSON.parse(event.source.postMessage.args[0][0]).adUrl).to.equal('http://creative.prebid.org/1.00'); + const reply = sinon.spy(); + _sendAdToCreative(mockAdObject, reply); + expect(reply.args[0][0].ad).to.equal(''); + expect(reply.args[0][0].adUrl).to.equal('http://creative.prebid.org/1.00'); window.googletag = oldVal; window.apntag = oldapntag; }); @@ -141,9 +177,9 @@ describe('secureCreatives', () => { message: 'Prebid Request' }; - const ev = { - data: JSON.stringify(data) - }; + const ev = makeEvent({ + data: JSON.stringify(data), + }); receiveMessage(ev); @@ -168,9 +204,9 @@ describe('secureCreatives', () => { message: 'Prebid Request' }; - const ev = { + const ev = makeEvent({ data: JSON.stringify(data) - }; + }); receiveMessage(ev); @@ -209,9 +245,9 @@ describe('secureCreatives', () => { message: 'Prebid Request' }; - const ev = { + const ev = makeEvent({ data: JSON.stringify(data) - }; + }); receiveMessage(ev); @@ -237,6 +273,38 @@ describe('secureCreatives', () => { configObj.setConfig({'auctionOptions': {}}); }); + + it('should emit AD_RENDER_FAILED if requested missing adId', () => { + const ev = makeEvent({ + data: JSON.stringify({ + message: 'Prebid Request', + adId: 'missing' + }) + }); + receiveMessage(ev); + sinon.assert.calledWith(stubEmit, CONSTANTS.EVENTS.AD_RENDER_FAILED, sinon.match({ + reason: CONSTANTS.AD_RENDER_FAILED_REASON.CANNOT_FIND_AD, + adId: 'missing' + })); + }); + + it('should emit AD_RENDER_FAILED if creative can\'t be sent to rendering frame', () => { + pushBidResponseToAuction({}); + const ev = makeEvent({ + source: { + postMessage: sinon.stub().callsFake(() => { throw new Error(); }) + }, + data: JSON.stringify({ + message: 'Prebid Request', + adId: bidId + }) + }); + receiveMessage(ev) + sinon.assert.calledWith(stubEmit, CONSTANTS.EVENTS.AD_RENDER_FAILED, sinon.match({ + reason: CONSTANTS.AD_RENDER_FAILED_REASON.EXCEPTION, + adId: bidId + })); + }); }); describe('Prebid Native', function() { @@ -249,13 +317,13 @@ describe('secureCreatives', () => { action: 'allAssetRequest' }; - const ev = { + const ev = makeEvent({ data: JSON.stringify(data), source: { postMessage: sinon.stub() }, origin: 'any origin' - }; + }); receiveMessage(ev); @@ -278,13 +346,13 @@ describe('secureCreatives', () => { action: 'allAssetRequest' }; - const ev = { + const ev = makeEvent({ data: JSON.stringify(data), source: { postMessage: sinon.stub() }, origin: 'any origin' - }; + }); receiveMessage(ev); @@ -322,13 +390,13 @@ describe('secureCreatives', () => { action: 'allAssetRequest' }; - const ev = { + const ev = makeEvent({ data: JSON.stringify(data), source: { postMessage: sinon.stub() }, origin: 'any origin' - }; + }); receiveMessage(ev); @@ -366,13 +434,13 @@ describe('secureCreatives', () => { action: 'click', }; - const ev = { + const ev = makeEvent({ data: JSON.stringify(data), source: { postMessage: sinon.stub() }, origin: 'any origin' - }; + }); receiveMessage(ev); @@ -395,5 +463,56 @@ describe('secureCreatives', () => { expect(adResponse).to.have.property('status', CONSTANTS.BID_STATUS.RENDERED); }); }); + + describe('Prebid Event', () => { + Object.entries({ + 'unrendered': [false, (bid) => { delete bid.status; }], + 'rendered': [true, (bid) => { bid.status = CONSTANTS.BID_STATUS.RENDERED }] + }).forEach(([test, [shouldEmit, prepBid]]) => { + describe(`for ${test} bids`, () => { + beforeEach(() => { + prepBid(adResponse); + pushBidResponseToAuction(adResponse); + }); + + it(`should${shouldEmit ? ' ' : ' not '}emit AD_RENDER_FAILED`, () => { + const event = makeEvent({ + data: JSON.stringify({ + message: 'Prebid Event', + event: CONSTANTS.EVENTS.AD_RENDER_FAILED, + adId: bidId, + info: { + reason: 'Fail reason', + message: 'Fail message', + }, + }) + }); + receiveMessage(event); + expect(stubEmit.calledWith(CONSTANTS.EVENTS.AD_RENDER_FAILED, { + adId: bidId, + bid: adResponse, + reason: 'Fail reason', + message: 'Fail message' + })).to.equal(shouldEmit); + }); + + it(`should${shouldEmit ? ' ' : ' not '}emit AD_RENDER_SUCCEEDED`, () => { + const event = makeEvent({ + data: JSON.stringify({ + message: 'Prebid Event', + event: CONSTANTS.EVENTS.AD_RENDER_SUCCEEDED, + adId: bidId, + }) + }); + receiveMessage(event); + expect(stubEmit.calledWith(CONSTANTS.EVENTS.AD_RENDER_SUCCEEDED, { + adId: bidId, + bid: adResponse, + doc: null + })).to.equal(shouldEmit); + }); + }); + }); + }); }); }); diff --git a/test/spec/utils_spec.js b/test/spec/utils_spec.js index 6494ead78e7..50a95b079c9 100644 --- a/test/spec/utils_spec.js +++ b/test/spec/utils_spec.js @@ -2,6 +2,7 @@ import { getAdServerTargeting } from 'test/fixtures/fixtures.js'; import { expect } from 'chai'; import CONSTANTS from 'src/constants.json'; import * as utils from 'src/utils.js'; +import {deepEqual, waitForElementToLoad} from 'src/utils.js'; var assert = require('assert'); @@ -1177,6 +1178,13 @@ describe('Utils', function () { } expect(utils.deepEqual(obj1, obj2)).to.equal(false); }); + it('should check types if {matchTypes: true}', () => { + function Typed(obj) { + Object.assign(this, obj); + } + const obj = {key: 'value'}; + expect(deepEqual({outer: obj}, {outer: new Typed(obj)}, {checkTypes: true})).to.be.false; + }); describe('cyrb53Hash', function() { it('should return the same hash for the same string', function() { @@ -1198,4 +1206,41 @@ describe('Utils', function () { }); }); }); + + describe('waitForElementToLoad', () => { + let element; + let callbacks; + + function callback() { + callbacks++; + } + + function delay(delay = 0) { + return new Promise((resolve) => { + window.setTimeout(resolve, delay); + }) + } + + beforeEach(() => { + callbacks = 0; + element = window.document.createElement('div'); + }); + + it('should respect timeout if set', () => { + waitForElementToLoad(element, 50).then(callback); + return delay(60).then(() => { + expect(callbacks).to.equal(1); + }); + }); + + ['load', 'error'].forEach((event) => { + it(`should complete on '${event} event'`, () => { + waitForElementToLoad(element).then(callback); + element.dispatchEvent(new Event(event)); + return delay().then(() => { + expect(callbacks).to.equal(1); + }) + }); + }); + }); }); diff --git a/test/spec/videoCache_spec.js b/test/spec/videoCache_spec.js index 554db3ebe4e..34e9bed04b6 100644 --- a/test/spec/videoCache_spec.js +++ b/test/spec/videoCache_spec.js @@ -2,6 +2,8 @@ import chai from 'chai'; import { getCacheUrl, store } from 'src/videoCache.js'; import { config } from 'src/config.js'; import { server } from 'test/mocks/xhr.js'; +import {auctionManager} from '../../src/auctionManager.js'; +import {AuctionIndex} from '../../src/auctionIndex.js'; const should = chai.should(); @@ -32,23 +34,6 @@ function getMockBid(bidder, auctionId, bidderRequestId) { }; } -function getMockBidRequest(bidder = 'appnexus', auctionId = '173afb6d132ba3', bidderRequestId = '3d1063078dfcc8') { - return { - 'bidderCode': bidder, - 'auctionId': auctionId, - 'bidderRequestId': bidderRequestId, - 'tid': '437fbbf5-33f5-487a-8e16-a7112903cfe5', - 'bids': [getMockBid(bidder, auctionId, bidderRequestId)], - 'auctionStart': 1510852447530, - 'timeout': 5000, - 'src': 's2s', - 'doneCbCallCount': 0, - 'refererInfo': { - 'referer': 'http://mytestpage.com' - } - } -} - describe('The video cache', function () { function assertError(callbackSpy) { callbackSpy.calledOnce.should.equal(true); @@ -240,7 +225,7 @@ describe('The video cache', function () { JSON.parse(request.requestBody).should.deep.equal(payload); }); - it('should include additional params in request payload should config.cache.vasttrack be true and bidderRequest argument was defined', () => { + it('should include additional params in request payload should config.cache.vasttrack be true - with timestamp', () => { config.setConfig({ cache: { url: 'https://prebid.adnxs.com/pbc/v1/cache', @@ -269,7 +254,21 @@ describe('The video cache', function () { auctionId: '1234-56789-abcde' }]; - store(bids, function () { }, getMockBidRequest()); + const stub = sinon.stub(auctionManager, 'index'); + stub.get(() => new AuctionIndex(() => [{ + getAuctionId() { + return '1234-56789-abcde'; + }, + getAuctionStart() { + return 1510852447530; + } + }])) + try { + store(bids, function () { }); + } finally { + stub.restore(); + } + const request = server.requests[0]; request.method.should.equal('POST'); request.url.should.equal('https://prebid.adnxs.com/pbc/v1/cache'); diff --git a/test/spec/video_spec.js b/test/spec/video_spec.js index 3ce8ba081da..61621c7ec42 100644 --- a/test/spec/video_spec.js +++ b/test/spec/video_spec.js @@ -1,113 +1,103 @@ import { isValidVideoBid } from 'src/video.js'; +import {hook} from '../../src/hook.js'; +import {stubAuctionIndex} from '../helpers/indexStub.js'; describe('video.js', function () { + before(() => { + hook.ready(); + }); + it('validates valid instream bids', function () { const bid = { adId: '456xyz', vastUrl: 'http://www.example.com/vastUrl', - requestId: '123abc' + transactionId: 'au' }; - const bidRequests = [{ - bids: [{ - bidId: '123abc', - bidder: 'appnexus', - mediaTypes: { - video: { context: 'instream' } - } - }] + const adUnits = [{ + transactionId: 'au', + mediaTypes: { + video: {context: 'instream'} + } }]; - const valid = isValidVideoBid(bid, bidRequests); + const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); expect(valid).to.equal(true); }); it('catches invalid instream bids', function () { const bid = { - requestId: '123abc' + transactionId: 'au' }; - const bidRequests = [{ - bids: [{ - bidId: '123abc', - bidder: 'appnexus', - mediaTypes: { - video: { context: 'instream' } - } - }] + const adUnits = [{ + transactionId: 'au', + mediaTypes: { + video: {context: 'instream'} + } }]; - const valid = isValidVideoBid(bid, bidRequests); + const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); expect(valid).to.equal(false); }); it('catches invalid bids when prebid-cache is disabled', function () { - const bidRequests = [{ - bids: [{ - bidder: 'vastOnlyVideoBidder', - mediaTypes: { video: {} }, - }] + const adUnits = [{ + transactionId: 'au', + bidder: 'vastOnlyVideoBidder', + mediaTypes: {video: {}}, }]; - const valid = isValidVideoBid({ vastXml: 'vast' }, bidRequests); + const valid = isValidVideoBid({ transactionId: 'au', vastXml: 'vast' }, {index: stubAuctionIndex({adUnits})}); expect(valid).to.equal(false); }); it('validates valid outstream bids', function () { const bid = { - requestId: '123abc', + transactionId: 'au', renderer: { url: 'render.url', render: () => true, } }; - const bidRequests = [{ - bids: [{ - bidId: '123abc', - bidder: 'appnexus', - mediaTypes: { - video: { context: 'outstream' } - } - }] + const adUnits = [{ + transactionId: 'au', + mediaTypes: { + video: {context: 'outstream'} + } }]; - const valid = isValidVideoBid(bid, bidRequests); + const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); expect(valid).to.equal(true); }); it('validates valid outstream bids with a publisher defined renderer', function () { const bid = { - requestId: '123abc', + transactionId: 'au', }; - const bidRequests = [{ - bids: [{ - bidId: '123abc', - bidder: 'appnexus', - mediaTypes: { - video: { - context: 'outstream', - renderer: { - url: 'render.url', - render: () => true, - } - } + const adUnits = [{ + transactionId: 'au', + mediaTypes: { + video: { + context: 'outstream', } - }] + }, + renderer: { + url: 'render.url', + render: () => true, + } }]; - const valid = isValidVideoBid(bid, bidRequests); + const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); expect(valid).to.equal(true); }); it('catches invalid outstream bids', function () { const bid = { - requestId: '123abc' + transactionId: 'au', }; - const bidRequests = [{ - bids: [{ - bidId: '123abc', - bidder: 'appnexus', - mediaTypes: { - video: { context: 'outstream' } - } - }] + const adUnits = [{ + transactionId: 'au', + mediaTypes: { + video: {context: 'outstream'} + } }]; - const valid = isValidVideoBid(bid, bidRequests); + const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); expect(valid).to.equal(false); }); }); diff --git a/test/test_deps.js b/test/test_deps.js new file mode 100644 index 00000000000..8b8c9fd3a0f --- /dev/null +++ b/test/test_deps.js @@ -0,0 +1,9 @@ +window.process = { + env: { + NODE_ENV: 'production' + } +}; + +require('test/helpers/prebidGlobal.js'); +require('test/mocks/adloaderStub.js'); +require('test/mocks/xhr.js'); diff --git a/test/test_index.js b/test/test_index.js index 53d75e36176..883f4d0590c 100644 --- a/test/test_index.js +++ b/test/test_index.js @@ -1,6 +1,4 @@ -require('test/helpers/prebidGlobal.js'); -require('test/mocks/adloaderStub.js'); -require('test/mocks/xhr.js'); +require('./test_deps.js'); var testsContext = require.context('.', true, /_spec$/); testsContext.keys().forEach(testsContext); diff --git a/webpack.conf.js b/webpack.conf.js index a738a2a0868..5269f5300f5 100644 --- a/webpack.conf.js +++ b/webpack.conf.js @@ -2,19 +2,11 @@ var prebid = require('./package.json'); var path = require('path'); var webpack = require('webpack'); var helpers = require('./gulpHelpers.js'); -var RequireEnsureWithoutJsonp = require('./plugins/RequireEnsureWithoutJsonp.js'); var { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); var argv = require('yargs').argv; -var allowedModules = require('./allowedModules.js'); - -// list of module names to never include in the common bundle chunk -var neverBundle = [ - 'AnalyticsAdapter.js' -]; var plugins = [ - new RequireEnsureWithoutJsonp(), - new webpack.EnvironmentPlugin(['LiveConnectMode']) + new webpack.EnvironmentPlugin({'LiveConnectMode': null}) ]; if (argv.analyze) { @@ -23,27 +15,8 @@ if (argv.analyze) { ) } -plugins.push( // this plugin must be last so it can be easily removed for karma unit tests - new webpack.optimize.CommonsChunkPlugin({ - name: 'prebid', - filename: 'prebid-core.js', - minChunks: function(module) { - return ( - ( - module.context && module.context.startsWith(path.resolve('./src')) && - !(module.resource && neverBundle.some(name => module.resource.includes(name))) - ) || - ( - module.resource && (allowedModules.src.concat(['core-js'])).some( - name => module.resource.includes(path.resolve('./node_modules/' + name)) - ) - ) - ); - } - }) -); - module.exports = { + mode: 'production', devtool: 'source-map', resolve: { modules: [ @@ -51,8 +24,26 @@ module.exports = { 'node_modules' ], }, + entry: (() => { + const entry = { + 'prebid-core': { + import: './src/prebid.js' + } + }; + const selectedModules = new Set(helpers.getArgModules()); + Object.entries(helpers.getModules()).forEach(([fn, mod]) => { + if (selectedModules.size === 0 || selectedModules.has(mod)) { + entry[mod] = { + import: fn, + dependOn: 'prebid-core' + } + } + }); + return entry; + })(), output: { - jsonpFunction: prebid.globalVarName + 'Chunk' + chunkLoadingGlobal: prebid.globalVarName + 'Chunk', + chunkLoading: 'jsonp', }, module: { rules: [ @@ -77,5 +68,9 @@ module.exports = { } ] }, + optimization: { + usedExports: true, + sideEffects: true, + }, plugins };